├── .rubocop_local.yml ├── .rspec ├── spec ├── data │ ├── yum │ │ ├── output_repoquery_single │ │ ├── output_repoquery_multiple │ │ ├── second.repo │ │ ├── first.repo │ │ └── output_repo_list │ ├── rhn │ │ ├── output_rhn-channel_list │ │ ├── output_rhn-channel_list_available │ │ ├── systemid │ │ └── systemid.missing_system_id │ ├── subscription_manager │ │ ├── output_orgs │ │ ├── output_repos │ │ ├── output_list_installed_not_subscribed │ │ ├── output_list_installed_subscribed │ │ └── output_list_all_available │ ├── rpm │ │ └── cmd_output_for_list_installed │ └── time_date │ │ └── timedatectl_output ├── system_spec.rb ├── partition_spec.rb ├── common_spec.rb ├── etc_issue_spec.rb ├── hardware_spec.rb ├── service_spec.rb ├── scap_spec.rb ├── deb_spec.rb ├── chrony_spec.rb ├── distro_spec.rb ├── registration_system_spec.rb ├── dns_spec.rb ├── ssh_spec.rb ├── time_date_spec.rb ├── volume_group_spec.rb ├── physical_volume_spec.rb ├── rpm_spec.rb ├── service │ ├── sys_v_init_service_spec.rb │ └── systemd_service_spec.rb ├── spec_helper.rb ├── ip_address_spec.rb ├── fstab_spec.rb ├── mountable_spec.rb ├── logical_volume_spec.rb ├── hosts_spec.rb ├── yum_spec.rb ├── network_interface │ └── network_interface_rh_spec.rb └── subscription_manager_spec.rb ├── .whitesource ├── lib ├── linux_admin │ ├── package.rb │ ├── version.rb │ ├── logging.rb │ ├── network_interface │ │ ├── network_interface_generic.rb │ │ ├── network_interface_darwin.rb │ │ └── network_interface_rh.rb │ ├── hardware.rb │ ├── null_logger.rb │ ├── yum │ │ └── repo_file.rb │ ├── system.rb │ ├── etc_issue.rb │ ├── exceptions.rb │ ├── homebrew.rb │ ├── common.rb │ ├── volume.rb │ ├── partition.rb │ ├── dns.rb │ ├── chrony.rb │ ├── deb.rb │ ├── service.rb │ ├── time_date.rb │ ├── mountable.rb │ ├── service │ │ ├── sys_v_init_service.rb │ │ └── systemd_service.rb │ ├── ip_address.rb │ ├── ssh_agent.rb │ ├── rpm.rb │ ├── distro.rb │ ├── registration_system.rb │ ├── physical_volume.rb │ ├── volume_group.rb │ ├── scap.rb │ ├── ssh.rb │ ├── logical_volume.rb │ ├── fstab.rb │ ├── hosts.rb │ ├── yum.rb │ ├── registration_system │ │ └── subscription_manager.rb │ ├── disk.rb │ └── network_interface.rb └── linux_admin.rb ├── .rubocop.yml ├── Gemfile ├── Rakefile ├── renovate.json ├── .gitignore ├── examples └── subscription_manager_hosted.rb ├── .github └── workflows │ └── ci.yaml ├── README.md ├── LICENSE.txt └── linux_admin.gemspec /.rubocop_local.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --color 3 | --order random 4 | -------------------------------------------------------------------------------- /spec/data/yum/output_repoquery_single: -------------------------------------------------------------------------------- 1 | subscription-manager 1.1.23.1 2 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "settingsInheritedFrom": "ManageIQ/whitesource-config@master" 3 | } -------------------------------------------------------------------------------- /lib/linux_admin/package.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class Package 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/linux_admin/version.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | VERSION = "4.0.0".freeze 3 | end 4 | -------------------------------------------------------------------------------- /spec/data/rhn/output_rhn-channel_list: -------------------------------------------------------------------------------- 1 | rhel-x86_64-server-6 2 | rhel-x86_64-server-6-cf-me-2 3 | -------------------------------------------------------------------------------- /spec/data/yum/output_repoquery_multiple: -------------------------------------------------------------------------------- 1 | curl 7.19.7 2 | subscription-manager 1.1.23.1 3 | wget 1.12 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | manageiq-style: ".rubocop_base.yml" 3 | inherit_from: 4 | - ".rubocop_local.yml" 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in linux_admin.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/linux_admin/logging.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | module Logging 3 | def logger 4 | LinuxAdmin.logger 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/linux_admin/network_interface/network_interface_generic.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class NetworkInterfaceGeneric < NetworkInterface 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require 'rspec/core/rake_task' 4 | RSpec::Core::RakeTask.new('spec') 5 | task :test => :spec 6 | task :default => :spec -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "inheritConfig": true, 4 | "inheritConfigRepoName": "manageiq/renovate-config" 5 | } 6 | -------------------------------------------------------------------------------- /spec/data/rhn/output_rhn-channel_list_available: -------------------------------------------------------------------------------- 1 | rhel-x86_64-server-6-cf-me-2 2 | rhel-x86_64-server-6-cf-me-2-beta 3 | rhel-x86_64-server-6-cf-me-3 4 | rhel-x86_64-server-6-cf-me-3-beta 5 | -------------------------------------------------------------------------------- /lib/linux_admin/hardware.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class Hardware 3 | def total_cores 4 | File.readlines("/proc/cpuinfo").count { |line| line =~ /^processor\s+:\s+\d+/ } 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/data/subscription_manager/output_orgs: -------------------------------------------------------------------------------- 1 | +-------------------------------------------+ 2 | myUser Organizations 3 | +-------------------------------------------+ 4 | 5 | Name: SomeOrg 6 | Key: 1234567 7 | -------------------------------------------------------------------------------- /lib/linux_admin/null_logger.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module LinuxAdmin 4 | class NullLogger < Logger 5 | def initialize(*_args) 6 | end 7 | 8 | def add(*_args, &_block) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | bin 19 | -------------------------------------------------------------------------------- /spec/data/yum/second.repo: -------------------------------------------------------------------------------- 1 | [my-local-repo-c] 2 | name=My Local Repo c 3 | baseurl=https://mirror.example.com/c/content/os_ver 4 | enabled=0 5 | cost=100 6 | gpgcheck=1 7 | gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-my-local-server 8 | sslverify=0 9 | metadata_expire=1 10 | -------------------------------------------------------------------------------- /lib/linux_admin/yum/repo_file.rb: -------------------------------------------------------------------------------- 1 | require 'inifile' 2 | 3 | module LinuxAdmin 4 | class Yum 5 | class RepoFile < IniFile 6 | def self.create(filename) 7 | File.write(filename, "") 8 | self.new(:filename => filename) 9 | end 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /lib/linux_admin/system.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class System 3 | def self.reboot! 4 | Common.run!(Common.cmd(:shutdown), 5 | :params => { "-r" => "now" }) 6 | end 7 | 8 | def self.shutdown! 9 | Common.run!(Common.cmd(:shutdown), 10 | :params => { "-h" => "0" }) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/linux_admin/etc_issue.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | module LinuxAdmin 4 | class EtcIssue 5 | include Singleton 6 | 7 | PATH = '/etc/issue' 8 | 9 | def include?(osname) 10 | data.downcase.include?(osname.to_s.downcase) 11 | end 12 | 13 | def data 14 | @data ||= File.exist?(PATH) ? File.read(PATH) : "" 15 | end 16 | 17 | def refresh 18 | @data = nil 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/linux_admin/exceptions.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class CredentialError < AwesomeSpawn::CommandResultError 3 | def initialize(result) 4 | super("Invalid username or password", result) 5 | end 6 | end 7 | 8 | class NetworkInterfaceError < AwesomeSpawn::CommandResultError; end 9 | 10 | class SubscriptionManagerError < AwesomeSpawn::CommandResultError; end 11 | 12 | class MissingConfigurationFileError < StandardError; end 13 | end 14 | -------------------------------------------------------------------------------- /spec/system_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::System do 2 | describe "#reboot!" do 3 | it "reboots the system" do 4 | expect(LinuxAdmin::Common).to receive(:run!).with(LinuxAdmin::Common.cmd(:shutdown), :params => {'-r' => 'now'}) 5 | LinuxAdmin::System.reboot! 6 | end 7 | end 8 | 9 | describe "#shutdown!" do 10 | it "shuts down the system" do 11 | expect(LinuxAdmin::Common).to receive(:run!).with(LinuxAdmin::Common.cmd(:shutdown), :params => {'-h' => '0'}) 12 | LinuxAdmin::System.shutdown! 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/linux_admin/homebrew.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class Homebrew < Package 3 | extend Logging 4 | 5 | def self.homebrew_cmd 6 | Common.cmd("brew") 7 | end 8 | 9 | def self.list_installed 10 | info(nil) 11 | end 12 | 13 | def self.info(pkg) 14 | out = Common.run!(homebrew_cmd, :params => ["list", :versions, pkg]).output 15 | out.split("\n").each_with_object({}) do |line, pkg_hash| 16 | name, ver = line.split[0..1] # only take the latest version 17 | pkg_hash[name] = ver 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/data/rpm/cmd_output_for_list_installed: -------------------------------------------------------------------------------- 1 | ruby193-rubygem-some_really_long_name 1.0.7-1.el6 2 | fipscheck-lib 1.2.0-7.el6 3 | aic94xx-firmware 30-2.el6 4 | latencytop-common 0.5-9.el6 5 | uuid 1.6.1-10.el6 6 | ConsoleKit 0.4.1-3.el6 7 | cpuspeed 1.5-19.el6 8 | mailcap 2.1.31-2.el6 9 | freetds 0.82-7.1.el6cf 10 | elinks 0.12-0.21.pre5.el6_3 11 | abrt-cli 2.0.8-15.el6 12 | libattr 2.4.44-7.el6 13 | passwd 0.77-4.el6_2.2 14 | vim-enhanced 7.2.411-1.8.el6 15 | popt 1.13-7.el6 16 | hesiod 3.1.0-19.el6 17 | pinfo 0.6.9-12.el6 18 | libpng 1.2.49-1.el6_2 19 | libdhash 0.4.2-9.el6 20 | zlib-devel 1.2.3-29.el6 21 | -------------------------------------------------------------------------------- /spec/data/time_date/timedatectl_output: -------------------------------------------------------------------------------- 1 | Local time: Thu 2015-09-24 17:54:02 EDT 2 | Universal time: Thu 2015-09-24 21:54:02 UTC 3 | RTC time: Thu 2015-09-24 21:54:02 4 | Time zone: America/New_York (EDT, -0400) 5 | NTP enabled: yes 6 | NTP synchronized: yes 7 | RTC in local TZ: no 8 | DST active: yes 9 | Last DST change: DST began at 10 | Sun 2015-03-08 01:59:59 EST 11 | Sun 2015-03-08 03:00:00 EDT 12 | Next DST change: DST ends (the clock jumps one hour backwards) at 13 | Sun 2015-11-01 01:59:59 EDT 14 | Sun 2015-11-01 01:00:00 EST 15 | -------------------------------------------------------------------------------- /spec/data/yum/first.repo: -------------------------------------------------------------------------------- 1 | [my-local-repo-a] 2 | name = My Local Repo A 3 | baseurl = https://mirror.example.com/a/content/os_ver 4 | enabled = 0 5 | gpgcheck = 1 6 | gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-my-local-server 7 | sslverify = 1 8 | sslcacert = /etc/rhsm/ca/my-loacl-server.pem 9 | sslclientkey = /etc/pki/entitlement/0123456789012345678-key.pem 10 | sslclientcert = /etc/pki/entitlement/0123456789012345678.pem 11 | metadata_expire = 86400 12 | 13 | [my-local-repo-b] 14 | name = My Local Repo B 15 | baseurl = https://mirror.example.com/b/content/os_ver 16 | enabled = 1 17 | gpgcheck = 0 18 | sslverify = 0 19 | metadata_expire = 86400 20 | -------------------------------------------------------------------------------- /lib/linux_admin/common.rb: -------------------------------------------------------------------------------- 1 | require 'awesome_spawn' 2 | 3 | module LinuxAdmin 4 | module Common 5 | include Logging 6 | 7 | BIN_DIRS = ENV["PATH"].split(File::PATH_SEPARATOR).freeze 8 | 9 | def self.cmd(name) 10 | BIN_DIRS.collect { |dir| "#{dir}/#{name}" }.detect { |cmd| File.exist?(cmd) } 11 | end 12 | 13 | def self.cmd?(name) 14 | !cmd(name).nil? 15 | end 16 | 17 | def self.run(cmd, options = {}) 18 | AwesomeSpawn.logger ||= logger 19 | AwesomeSpawn.run(cmd, options) 20 | end 21 | 22 | def self.run!(cmd, options = {}) 23 | AwesomeSpawn.logger ||= logger 24 | AwesomeSpawn.run!(cmd, options) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/linux_admin/volume.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class Volume 3 | private 4 | 5 | def self.process_volume_display_line(line) 6 | groups = VolumeGroup.scan 7 | fields = line.split(':') 8 | vgname = fields[1] 9 | vg = groups.find { |g| g.name == vgname } 10 | return fields, vg 11 | end 12 | 13 | protected 14 | 15 | def self.scan_volumes(cmd) 16 | volumes = [] 17 | 18 | out = Common.run!(cmd, :params => {'-c' => nil}).output 19 | 20 | out.each_line do |line| 21 | fields, vg = process_volume_display_line(line.lstrip) 22 | volumes << yield(fields, vg) 23 | end 24 | 25 | volumes 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/data/subscription_manager/output_repos: -------------------------------------------------------------------------------- 1 | +----------------------------------------------------------+ 2 | Available Repositories in /etc/yum.repos.d/redhat.repo 3 | +----------------------------------------------------------+ 4 | Repo ID: some-repo-source-rpms 5 | Repo Name: Some Repo (Source RPMs) 6 | Repo URL: https://my.host.example.com/repos/some-repo/source/rpms 7 | Enabled: 1 8 | 9 | Repo ID: some-repo-rpms 10 | Repo Name: Some Repo 11 | Repo URL: https://my.host.example.com/repos/some-repo/rpms 12 | Enabled: 1 13 | 14 | Repo ID: some-repo-2-beta-rpms 15 | Repo Name: Some Repo (Beta RPMs) 16 | Repo URL: https://my.host.example.com/repos/some-repo-2/beta/rpms 17 | Enabled: 0 18 | 19 | -------------------------------------------------------------------------------- /spec/data/subscription_manager/output_list_installed_not_subscribed: -------------------------------------------------------------------------------- 1 | +-------------------------------------------+ 2 | Installed Product Status 3 | +-------------------------------------------+ 4 | Product Name: Red Hat Enterprise Linux Server 5 | Product ID: 69 6 | Version: 6.3 7 | Arch: x86_64 8 | Status: Not Subscribed 9 | Starts: 09/27/2012 10 | Ends: 09/26/2013 11 | 12 | Product Name: Red Hat CloudForms 13 | Product ID: 167 14 | Version: 2.1 15 | Arch: x86_64 16 | Status: Subscribed 17 | Starts: 01/03/2013 18 | Ends: 19 | 20 | -------------------------------------------------------------------------------- /spec/data/subscription_manager/output_list_installed_subscribed: -------------------------------------------------------------------------------- 1 | +-------------------------------------------+ 2 | Installed Product Status 3 | +-------------------------------------------+ 4 | Product Name: Red Hat Enterprise Linux Server 5 | Product ID: 69 6 | Version: 6.3 7 | Arch: x86_64 8 | Status: Subscribed 9 | Starts: 09/27/2012 10 | Ends: 09/26/2013 11 | 12 | Product Name: Red Hat CloudForms 13 | Product ID: 167 14 | Version: 2.1 15 | Arch: x86_64 16 | Status: Subscribed 17 | Starts: 01/03/2013 18 | Ends: 01/03/2014 19 | 20 | -------------------------------------------------------------------------------- /examples/subscription_manager_hosted.rb: -------------------------------------------------------------------------------- 1 | $:.push("../lib") 2 | require 'linux_admin' 3 | 4 | username = "MyUsername" 5 | password = "MyPassword" 6 | 7 | 8 | reg_status = LinuxAdmin.registered? 9 | puts "Registration Status: #{reg_status.to_s}" 10 | 11 | unless reg_status 12 | puts "Registering to Subscription Manager..." 13 | LinuxAdmin::SubscriptionManager.register(:username => username, :password => password) 14 | end 15 | 16 | reg_type = LinuxAdmin.registration_type 17 | puts "Registration System: #{reg_type}" 18 | 19 | puts "Subscribing to channels..." 20 | reg_type.subscribe(reg_type.available_subscriptions.keys.first) 21 | puts "Checking for updates..." 22 | if LinuxAdmin::Yum.updates_available? 23 | puts "Updates Available \n Updating..." 24 | puts "Updates Applied" if LinuxAdmin::Yum.update 25 | end 26 | -------------------------------------------------------------------------------- /lib/linux_admin/network_interface/network_interface_darwin.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class NetworkInterfaceDarwin < NetworkInterface 3 | # Runs the command `ip -[4/6] route` and returns the output 4 | # 5 | # @param version [Fixnum] Version of IP protocol (4 or 6) 6 | # @return [String] The command output 7 | # @raise [NetworkInterfaceError] if the command fails 8 | # 9 | # macs use ip route get while others use ip route show 10 | def ip_route(version, route = "default") 11 | output = Common.run!(Common.cmd("ip"), :params => ["--json", "-#{version}", "route", "get", route]).output 12 | return {} if output.blank? 13 | 14 | JSON.parse(output).first 15 | rescue AwesomeSpawn::CommandResultError => e 16 | raise NetworkInterfaceError.new(e.message, e.result) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/partition_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Partition do 2 | before(:each) do 3 | @disk = LinuxAdmin::Disk.new :path => '/dev/sda' 4 | @partition = LinuxAdmin::Partition.new :disk => @disk, :id => 2 5 | end 6 | 7 | describe "#path" do 8 | it "returns partition path" do 9 | expect(@partition.path).to eq('/dev/sda2') 10 | end 11 | end 12 | 13 | describe "#mount" do 14 | context "mount_point not specified" do 15 | it "sets default mount_point" do 16 | expect(described_class).to receive(:mount_point_exists?).and_return(false) 17 | expect(File).to receive(:directory?).with('/mnt/sda2').and_return(true) 18 | expect(LinuxAdmin::Common).to receive(:run!) 19 | @partition.mount 20 | expect(@partition.mount_point).to eq('/mnt/sda2') 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/linux_admin/partition.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module LinuxAdmin 4 | class Partition 5 | include Mountable 6 | 7 | attr_accessor :id 8 | attr_accessor :partition_type 9 | attr_accessor :start_sector 10 | attr_accessor :end_sector 11 | attr_accessor :size 12 | attr_accessor :disk 13 | 14 | def initialize(args={}) 15 | @id = args[:id] 16 | @size = args[:size] 17 | @disk = args[:disk] 18 | @fs_type = args[:fs_type] 19 | @start_sector = args[:start_sector] 20 | @end_sector = args[:end_sector] 21 | @partition_type = args[:partition_type] 22 | end 23 | 24 | def path 25 | disk.partition_path(id) 26 | end 27 | 28 | def mount(mount_point=nil) 29 | mount_point ||= "/mnt/#{disk.path.split(File::SEPARATOR).last}#{id}" 30 | super(mount_point) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches-ignore: 6 | - dependabot/* 7 | - renovate/* 8 | schedule: 9 | - cron: 0 0 * * 0 10 | workflow_dispatch: 11 | concurrency: 12 | group: "${{ github.workflow }}-${{ github.ref }}" 13 | cancel-in-progress: true 14 | permissions: 15 | contents: read 16 | jobs: 17 | ci: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | ruby-version: 22 | - '2.6' 23 | - '2.7' 24 | - '3.0' 25 | - '3.1' 26 | - '3.2' 27 | - '3.3' 28 | steps: 29 | - uses: actions/checkout@v6 30 | - name: Set up Ruby 31 | uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: "${{ matrix.ruby-version }}" 34 | bundler-cache: true 35 | timeout-minutes: 30 36 | - name: Run tests 37 | run: bundle exec rake 38 | -------------------------------------------------------------------------------- /lib/linux_admin/dns.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class Dns 3 | attr_accessor :filename 4 | attr_accessor :nameservers 5 | attr_accessor :search_order 6 | 7 | def initialize(filename = "/etc/resolv.conf") 8 | @filename = filename 9 | reload 10 | end 11 | 12 | def reload 13 | @search_order = [] 14 | @nameservers = [] 15 | 16 | File.read(@filename).split("\n").each do |line| 17 | if line =~ /^search .*/ 18 | @search_order += line.split(/^search\s+/)[1].split 19 | elsif line =~ /^nameserver .*/ 20 | @nameservers.push(line.split[1]) 21 | end 22 | end 23 | end 24 | 25 | def save 26 | search = "search #{@search_order.join(" ")}\n" unless @search_order.empty? 27 | servers = @nameservers.collect { |ns| "nameserver #{ns}\n" }.join 28 | File.write(@filename, "#{search}#{servers}") 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/linux_admin/chrony.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class Chrony 3 | SERVICE_NAME = "chronyd".freeze 4 | 5 | def initialize(conf = "/etc/chrony.conf") 6 | raise MissingConfigurationFileError, "#{conf} does not exist" unless File.exist?(conf) 7 | @conf = conf 8 | end 9 | 10 | def clear_servers 11 | data = File.read(@conf) 12 | data.gsub!(/^server\s+.+\n/, "") 13 | data.gsub!(/^pool\s+.+\n/, "") 14 | File.write(@conf, data) 15 | end 16 | 17 | def add_servers(*servers) 18 | data = File.read(@conf) 19 | data << "\n" unless data.end_with?("\n") 20 | servers.each { |s| data << "server #{s} iburst\n" } 21 | File.write(@conf, data) 22 | restart_service_if_running 23 | end 24 | 25 | private 26 | 27 | def restart_service_if_running 28 | service = Service.new(SERVICE_NAME) 29 | service.restart if service.running? 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/linux_admin/deb.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class Deb < Package 3 | APT_CACHE_CMD = '/usr/bin/apt-cache' 4 | 5 | def self.from_line(apt_cache_line, in_description=false) 6 | tag,value = apt_cache_line.split(':') 7 | tag = tag.strip.downcase 8 | [tag, value] 9 | end 10 | 11 | def self.from_string(apt_cache_string) 12 | in_description = false 13 | apt_cache_string.split("\n").each.with_object({}) do |line,deb| 14 | tag,value = self.from_line(line) 15 | if tag == 'description-en' 16 | in_description = true 17 | elsif tag == 'homepage' 18 | in_description = false 19 | end 20 | 21 | if in_description && tag != 'description-en' 22 | deb['description-en'] << line 23 | else 24 | deb[tag] = value.strip 25 | end 26 | end 27 | end 28 | 29 | def self.info(pkg) 30 | from_string(Common.run!(APT_CACHE_CMD, :params => ["show", pkg]).output) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/linux_admin/service.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class Service 3 | include Logging 4 | 5 | def self.service_type(reload = false) 6 | return @service_type if @service_type && !reload 7 | @service_type = service_type_uncached 8 | end 9 | 10 | class << self 11 | private 12 | alias_method :orig_new, :new 13 | end 14 | 15 | def self.new(*args) 16 | if self == LinuxAdmin::Service 17 | service_type.new(*args) 18 | else 19 | orig_new(*args) 20 | end 21 | end 22 | 23 | attr_accessor :name 24 | 25 | def initialize(name) 26 | @name = name 27 | end 28 | 29 | alias_method :id, :name 30 | alias_method :id=, :name= 31 | 32 | private 33 | 34 | def self.service_type_uncached 35 | Common.cmd?(:systemctl) ? SystemdService : SysVInitService 36 | end 37 | private_class_method :service_type_uncached 38 | end 39 | end 40 | 41 | Dir.glob(File.join(File.dirname(__FILE__), "service", "*.rb")).each { |f| require f } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LinuxAdmin 2 | 3 | [![Gem Version](https://badge.fury.io/rb/linux_admin.svg)](http://badge.fury.io/rb/linux_admin) 4 | [![CI](https://github.com/ManageIQ/linux_admin/actions/workflows/ci.yaml/badge.svg)](https://github.com/ManageIQ/linux_admin/actions/workflows/ci.yaml) 5 | 6 | LinuxAdmin is a module to simplify management of linux systems. 7 | It should be a single place to manage various system level configurations, 8 | registration, updates, etc. 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | gem 'linux_admin' 15 | 16 | And then execute: 17 | 18 | $ bundle 19 | 20 | Or install it yourself as: 21 | 22 | $ gem install linux_admin 23 | 24 | ## Usage 25 | 26 | TODO: Write usage instructions here 27 | 28 | ## Contributing 29 | 30 | 1. Fork it 31 | 2. Create your feature branch (`git checkout -b my-new-feature`) 32 | 3. Commit your changes (`git commit -am 'Add some feature'`) 33 | 4. Push to the branch (`git push origin my-new-feature`) 34 | 5. Create new Pull Request 35 | -------------------------------------------------------------------------------- /spec/common_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Common do 2 | describe "#cmd" do 3 | it "looks up local command from id" do 4 | expect(described_class.cmd(:dd)).to match(%r{bin/dd}) 5 | end 6 | 7 | it "returns nil when it can't find the command" do 8 | expect(described_class.cmd(:kasgbdlcvjhals)).to be_nil 9 | end 10 | end 11 | 12 | describe "#cmd?" do 13 | it "returns true when the command exists" do 14 | expect(described_class.cmd?(:dd)).to be true 15 | end 16 | 17 | it "returns false when the command doesn't exist" do 18 | expect(described_class.cmd?(:kasgbdlcvjhals)).to be false 19 | end 20 | end 21 | 22 | describe ".run" do 23 | it "runs a command with AwesomeSpawn.run" do 24 | expect(AwesomeSpawn).to receive(:run).with("echo", {nil => "test"}) 25 | described_class.run("echo", nil => "test") 26 | end 27 | end 28 | 29 | describe ".run!" do 30 | it "runs a command with AwesomeSpawn.run!" do 31 | expect(AwesomeSpawn).to receive(:run!).with("echo", {nil => "test"}) 32 | described_class.run!("echo", nil => "test") 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Red Hat, Inc. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /spec/etc_issue_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::EtcIssue do 2 | subject { described_class.instance } 3 | before do 4 | # Reset the singleton so subsequent tests get a new instance 5 | subject.refresh 6 | end 7 | 8 | it "should not find the phrase when the file is missing" do 9 | expect(File).to receive(:exist?).with('/etc/issue').at_least(:once).and_return(false) 10 | expect(subject).not_to include("phrase") 11 | end 12 | 13 | it "should not find phrase when the file is empty" do 14 | etc_issue_contains("") 15 | expect(subject).not_to include("phrase") 16 | end 17 | 18 | it "should not find phrase when the file has a different phrase" do 19 | etc_issue_contains("something\nelse") 20 | expect(subject).not_to include("phrase") 21 | end 22 | 23 | it "should find phrase in same case" do 24 | etc_issue_contains("phrase") 25 | expect(subject).to include("phrase") 26 | end 27 | 28 | it "should find upper phrase in file" do 29 | etc_issue_contains("PHRASE\nother") 30 | expect(subject).to include("phrase") 31 | end 32 | 33 | it "should find phrase when searching with upper" do 34 | etc_issue_contains("other\nphrase") 35 | expect(subject).to include("PHRASE") 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/linux_admin/time_date.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class TimeDate 3 | COMMAND = 'timedatectl' 4 | 5 | TimeCommandError = Class.new(StandardError) 6 | 7 | def self.system_timezone_detailed 8 | result = Common.run(Common.cmd(COMMAND), :params => ["status"]) 9 | result.output.split("\n").each do |l| 10 | return l.split(':')[1].strip if l =~ /Time.*zone/ 11 | end 12 | end 13 | 14 | def self.system_timezone 15 | system_timezone_detailed.split[0] 16 | end 17 | 18 | def self.timezones 19 | result = Common.run!(Common.cmd(COMMAND), :params => ["list-timezones"]) 20 | result.output.split("\n") 21 | rescue AwesomeSpawn::CommandResultError => e 22 | raise TimeCommandError, e.message 23 | end 24 | 25 | def self.system_time=(time) 26 | Common.run!(Common.cmd(COMMAND), :params => ["set-time", "#{time.strftime("%F %T")}", :adjust_system_clock]) 27 | rescue AwesomeSpawn::CommandResultError => e 28 | raise TimeCommandError, e.message 29 | end 30 | 31 | def self.system_timezone=(zone) 32 | Common.run!(Common.cmd(COMMAND), :params => ["set-timezone", zone]) 33 | rescue AwesomeSpawn::CommandResultError => e 34 | raise TimeCommandError, e.message 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/linux_admin.rb: -------------------------------------------------------------------------------- 1 | require 'more_core_extensions/all' 2 | 3 | require 'linux_admin/logging' 4 | require 'linux_admin/null_logger' 5 | 6 | require 'linux_admin/common' 7 | require 'linux_admin/exceptions' 8 | require 'linux_admin/package' 9 | require 'linux_admin/registration_system' 10 | require 'linux_admin/rpm' 11 | require 'linux_admin/deb' 12 | require 'linux_admin/homebrew' 13 | require 'linux_admin/version' 14 | require 'linux_admin/yum' 15 | require 'linux_admin/ssh' 16 | require 'linux_admin/ssh_agent' 17 | require 'linux_admin/service' 18 | require 'linux_admin/mountable' 19 | require 'linux_admin/disk' 20 | require 'linux_admin/hardware' 21 | require 'linux_admin/hosts' 22 | require 'linux_admin/partition' 23 | require 'linux_admin/etc_issue' 24 | require 'linux_admin/distro' 25 | require 'linux_admin/system' 26 | require 'linux_admin/fstab' 27 | require 'linux_admin/volume' 28 | require 'linux_admin/logical_volume' 29 | require 'linux_admin/physical_volume' 30 | require 'linux_admin/volume_group' 31 | require 'linux_admin/scap' 32 | require 'linux_admin/time_date' 33 | require 'linux_admin/ip_address' 34 | require 'linux_admin/dns' 35 | require 'linux_admin/network_interface' 36 | require 'linux_admin/chrony' 37 | 38 | module LinuxAdmin 39 | class << self 40 | attr_writer :logger 41 | end 42 | 43 | def self.logger 44 | @logger ||= NullLogger.new 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/hardware_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Hardware do 2 | CONTENT = <<-EOF 3 | processor : 0 4 | vendor_id : GenuineIntel 5 | cpu family : 6 6 | model : 58 7 | model name : Intel(R) Core(TM) i7-3740QM CPU @ 2.70GHz 8 | stepping : 9 9 | microcode : 0x17 10 | cpu MHz : 2614.148 11 | cache size : 6144 KB 12 | physical id : 0 13 | siblings : 8 14 | core id : 0 15 | cpu cores : 4 16 | apicid : 0 17 | initial apicid : 0 18 | fpu : yes 19 | fpu_exception : yes 20 | cpuid level : 13 21 | wp : yes 22 | flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb pln pts dtherm tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms xsaveopt 23 | bugs : 24 | bogomips : 5387.35 25 | clflush size : 64 26 | cache_alignment : 64 27 | address sizes : 36 bits physical, 48 bits virtual 28 | power management: 29 | 30 | processor : 1 31 | 32 | processor : 2 33 | 34 | processor : 3 35 | processor : 4 36 | processor : 5 37 | processor : 6 38 | processor : 7 39 | EOF 40 | 41 | it "total_cores" do 42 | allow(File).to receive(:readlines).and_return(CONTENT.lines) 43 | 44 | expect(described_class.new.total_cores).to eq(8) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/linux_admin/mountable.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | module Mountable 3 | attr_accessor :fs_type 4 | attr_accessor :mount_point 5 | 6 | module ClassMethods 7 | def mount_point_exists?(mount_point) 8 | result = Common.run!(Common.cmd(:mount)) 9 | result.output.split("\n").any? { |line| line.split[2] == mount_point.to_s } 10 | end 11 | 12 | def mount_point_available?(mount_point) 13 | !mount_point_exists?(mount_point) 14 | end 15 | end 16 | 17 | def self.included(base) 18 | base.extend(ClassMethods) 19 | end 20 | 21 | def discover_mount_point 22 | result = Common.run!(Common.cmd(:mount)) 23 | mount_line = result.output.split("\n").find { |line| line.split[0] == path } 24 | @mount_point = mount_line.split[2] if mount_line 25 | end 26 | 27 | def format_to(filesystem) 28 | Common.run!(Common.cmd(:mke2fs), 29 | :params => {'-t' => filesystem, nil => path}) 30 | @fs_type = filesystem 31 | end 32 | 33 | def mount(mount_point) 34 | FileUtils.mkdir(mount_point) unless File.directory?(mount_point) 35 | 36 | if self.class.mount_point_exists?(mount_point) 37 | raise ArgumentError, "disk already mounted at #{mount_point}" 38 | end 39 | 40 | Common.run!(Common.cmd(:mount), :params => {nil => [path, mount_point]}) 41 | @mount_point = mount_point 42 | end 43 | 44 | def umount 45 | Common.run!(Common.cmd(:umount), :params => {nil => [@mount_point]}) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/data/rhn/systemid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | username 7 | SomeUsername 8 | 9 | 10 | operating_system 11 | redhat-release-server 12 | 13 | 14 | description 15 | Initial Registration Parameters: 16 | OS: redhat-release-server 17 | Release: 6Server 18 | CPU Arch: x86_64 19 | 20 | 21 | checksum 22 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 23 | 24 | 25 | profile_name 26 | SomeProfileName 27 | 28 | 29 | system_id 30 | ID-0123456789 31 | 32 | 33 | architecture 34 | x86_64 35 | 36 | 37 | os_release 38 | 6Server 39 | 40 | 41 | fields 42 | 43 | system_id 44 | os_release 45 | operating_system 46 | architecture 47 | username 48 | type 49 | 50 | 51 | 52 | type 53 | REAL 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /spec/data/rhn/systemid.missing_system_id: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | username 7 | SomeUsername 8 | 9 | 10 | operating_system 11 | redhat-release-server 12 | 13 | 14 | description 15 | Initial Registration Parameters: 16 | OS: redhat-release-server 17 | Release: 6Server 18 | CPU Arch: x86_64 19 | 20 | 21 | checksum 22 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 23 | 24 | 25 | profile_name 26 | SomeProfileName 27 | 28 | 29 | system_id 30 | 31 | 32 | 33 | architecture 34 | x86_64 35 | 36 | 37 | os_release 38 | 6Server 39 | 40 | 41 | fields 42 | 43 | system_id 44 | os_release 45 | operating_system 46 | architecture 47 | username 48 | type 49 | 50 | 51 | 52 | type 53 | REAL 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /lib/linux_admin/service/sys_v_init_service.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class SysVInitService < Service 3 | def running? 4 | Common.run(Common.cmd(:service), 5 | :params => {nil => [name, "status"]}).exit_status == 0 6 | end 7 | 8 | def enable 9 | Common.run!(Common.cmd(:chkconfig), 10 | :params => {nil => [name, "on"]}) 11 | self 12 | end 13 | 14 | def disable 15 | Common.run!(Common.cmd(:chkconfig), 16 | :params => {nil => [name, "off"]}) 17 | self 18 | end 19 | 20 | def start(enable = false) 21 | Common.run!(Common.cmd(:service), 22 | :params => {nil => [name, "start"]}) 23 | self.enable if enable 24 | self 25 | end 26 | 27 | def stop 28 | Common.run!(Common.cmd(:service), 29 | :params => {nil => [name, "stop"]}) 30 | self 31 | end 32 | 33 | def restart 34 | status = 35 | Common.run(Common.cmd(:service), 36 | :params => {nil => [name, "restart"]}).exit_status 37 | 38 | # attempt to manually stop/start if restart fails 39 | if status != 0 40 | self.stop 41 | self.start 42 | end 43 | 44 | self 45 | end 46 | 47 | def reload 48 | Common.run!(Common.cmd(:service), :params => [name, "reload"]) 49 | self 50 | end 51 | 52 | def status 53 | Common.run(Common.cmd(:service), :params => [name, "status"]).output 54 | end 55 | 56 | def show 57 | raise NotImplementedError 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/data/yum/output_repo_list: -------------------------------------------------------------------------------- 1 | Loaded plugins: product-id, rhnplugin, security, subscription-manager 2 | This system is receiving updates from Red Hat Subscription Management. 3 | This system is not registered with RHN Classic or RHN Satellite. 4 | You can use rhn_register to register. 5 | RHN Satellite or RHN Classic support will be disabled. 6 | rhel-6-server-rpms | 3.7 kB 00:00 7 | rhel-ha-for-rhel-6-server-rpms | 3.7 kB 00:00 8 | rhel-lb-for-rhel-6-server-rpms | 3.7 kB 00:00 9 | repo id repo name status 10 | rhel-6-server-rpms Red Hat Enterprise Linux 6 Server (RPMs) 11,016 11 | rhel-ha-for-rhel-6-server-rpms Red Hat Enterprise Linux High Availability (for RHEL 6 Server) (RPMs) 269 12 | rhel-lb-for-rhel-6-server-rpms Red Hat Enterprise Linux Load Balancer (for RHEL 6 Server) (RPMs) 0 13 | repolist: 11,909 14 | -------------------------------------------------------------------------------- /lib/linux_admin/ip_address.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddr' 2 | 3 | module LinuxAdmin 4 | class IpAddress 5 | def address 6 | address_list.detect { |ip| IPAddr.new(ip).ipv4? } 7 | end 8 | 9 | def address6 10 | address_list.detect { |ip| IPAddr.new(ip).ipv6? } 11 | end 12 | 13 | def mac_address(interface) 14 | result = Common.run(Common.cmd("ip"), :params => ["addr", "show", interface]) 15 | return nil if result.failure? 16 | 17 | parse_output(result.output, %r{link/ether}, 1) 18 | end 19 | 20 | def netmask(interface) 21 | result = Common.run(Common.cmd("ifconfig"), :params => [interface]) 22 | return nil if result.failure? 23 | 24 | parse_output(result.output, /netmask/, 3) 25 | end 26 | 27 | def gateway 28 | result = Common.run(Common.cmd("ip"), :params => ["route"]) 29 | return nil if result.failure? 30 | 31 | parse_output(result.output, /^default/, 2) 32 | end 33 | 34 | private 35 | 36 | def parse_output(output, regex, col) 37 | the_line = output.split("\n").detect { |l| l =~ regex } 38 | the_line.nil? ? nil : the_line.strip.split(' ')[col] 39 | end 40 | 41 | def address_list 42 | result = nil 43 | # Added retry to account for slow DHCP not assigning an IP quickly at boot; specifically: 44 | # https://github.com/ManageIQ/manageiq-appliance/commit/160d8ccbfbfd617bdb5445e56cdab66b9323b15b 45 | 5.times do 46 | result = Common.run(Common.cmd("hostname"), :params => ["-I"]) 47 | break if result.success? 48 | end 49 | 50 | result.success? ? result.output.split(' ') : [] 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/data/subscription_manager/output_list_all_available: -------------------------------------------------------------------------------- 1 | +-------------------------------------------+ 2 | Available Subscriptions 3 | +-------------------------------------------+ 4 | Subscription Name: Example Subscription 5 | SKU: SER0123 6 | Pool Id: 82c042fca983889b10178893f29b06e3 7 | Quantity: 1690 8 | Service Level: None 9 | Service Type: None 10 | Multi-Entitlement: No 11 | Ends: 01/01/2022 12 | System Type: Physical 13 | 14 | Subscription Name: My Private Subscription 15 | SKU: SER9876 16 | Pool Id: 4f738052ec866192c775c62f408ab868 17 | Quantity: Unlimited 18 | Service Level: None 19 | Service Type: None 20 | Multi-Entitlement: No 21 | Ends: 06/04/2013 22 | System Type: Virtual 23 | 24 | Subscription Name: Shared Subscription - With other characters, (2 sockets) (Up to 1 guest) 25 | SKU: RH0123456 26 | Pool Id: 3d81297f352305b9a3521981029d7d83 27 | Quantity: 1 28 | Service Level: Self-support 29 | Service Type: L1-L3 30 | Multi-Entitlement: No 31 | Ends: 05/15/2013 32 | System Type: Virtual 33 | 34 | Subscription Name: Example Subscription, Premium (up to 2 sockets) 3 year 35 | SKU: MCT0123A9 36 | Pool Id: 87cefe63b67984d5c7e401d833d2f87f 37 | Quantity: 1 38 | Service Level: Premium 39 | Service Type: L1-L3 40 | Multi-Entitlement: No 41 | Ends: 07/05/2013 42 | System Type: Virtual 43 | -------------------------------------------------------------------------------- /lib/linux_admin/ssh_agent.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | module LinuxAdmin 3 | class SSHAgent 4 | attr_accessor :pid 5 | attr_reader :socket 6 | 7 | def initialize(ssh_private_key, agent_socket = nil) 8 | @socket = agent_socket 9 | @private_key = ssh_private_key 10 | end 11 | 12 | def start 13 | if @socket 14 | FileUtils.mkdir_p(File.dirname(@socket)) 15 | agent_details = `ssh-agent -a #{@socket}` 16 | else 17 | agent_details = `ssh-agent` 18 | @socket = parse_ssh_agent_socket(agent_details) 19 | end 20 | @pid = parse_ssh_agent_pid(agent_details) 21 | IO.popen({'SSH_AUTH_SOCK' => @socket, 'SSH_AGENT_PID' => @pid}, ['ssh-add', '-'], :mode => 'w') do |f| 22 | f.puts(@private_key) 23 | end 24 | raise StandardError, "Couldn't add key to agent" if $CHILD_STATUS.to_i != 0 25 | end 26 | 27 | def with_service 28 | start 29 | yield @socket 30 | ensure 31 | stop 32 | end 33 | 34 | def stop 35 | system({'SSH_AGENT_PID' => @pid}, '(ssh-agent -k) >/dev/null 2>&1') if process_exists?(@pid) 36 | File.delete(@socket) if File.exist?(@socket) 37 | @socket = nil 38 | @pid = nil 39 | end 40 | 41 | private 42 | 43 | def process_exists?(process_pid) 44 | Process.kill(0, process_pid) == 1 45 | rescue 46 | false 47 | end 48 | 49 | def parse_ssh_agent_socket(output) 50 | parse_ssh_agent_output(output, 1) 51 | end 52 | 53 | def parse_ssh_agent_pid(output) 54 | parse_ssh_agent_output(output, 2) 55 | end 56 | 57 | def parse_ssh_agent_output(output, index) 58 | output.split('=')[index].split(' ')[0].chop 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/linux_admin/rpm.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class Rpm < Package 3 | extend Logging 4 | 5 | def self.rpm_cmd 6 | Common.cmd(:rpm) 7 | end 8 | 9 | def self.list_installed 10 | out = Common.run!("#{rpm_cmd} -qa --qf \"%{NAME} %{VERSION}-%{RELEASE}\n\"").output 11 | out.split("\n").each_with_object({}) do |line, pkg_hash| 12 | name, ver = line.split(" ") 13 | pkg_hash[name] = ver 14 | end 15 | end 16 | 17 | # Import a GPG file for use with RPM 18 | # 19 | # Rpm.import_key("/etc/pki/my-gpg-key") 20 | def self.import_key(file) 21 | logger.info("#{self.class.name}##{__method__} Importing RPM-GPG-KEY: #{file}") 22 | Common.run!("rpm", :params => {"--import" => file}) 23 | end 24 | 25 | def self.info(pkg) 26 | params = { "-qi" => pkg} 27 | in_description = false 28 | out = Common.run!(rpm_cmd, :params => params).output 29 | # older versions of rpm may have multiple fields per line, 30 | # split up lines with multiple tags/values: 31 | out.gsub!(/(^.*:.*)\s\s+(.*:.*)$/, "\\1\n\\2") 32 | out.split("\n").each.with_object({}) do |line, rpm| 33 | next if !line || line.empty? 34 | tag,value = line.split(':') 35 | tag = tag.strip 36 | if tag == 'Description' 37 | in_description = true 38 | elsif in_description 39 | rpm['description'] ||= "" 40 | rpm['description'] << line + " " 41 | else 42 | tag = tag.downcase.gsub(/\s/, '_') 43 | rpm[tag] = value.strip 44 | end 45 | end 46 | end 47 | 48 | def self.upgrade(pkg) 49 | cmd = "rpm -U" 50 | params = { nil => pkg } 51 | 52 | Common.run(cmd, :params => params).exit_status == 0 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/service_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Service do 2 | context ".service_type" do 3 | it "on systemctl systems" do 4 | stub_to_service_type(:systemd_service) 5 | expect(described_class.service_type).to eq(LinuxAdmin::SystemdService) 6 | end 7 | 8 | it "on sysv systems" do 9 | stub_to_service_type(:sys_v_init_service) 10 | expect(described_class.service_type).to eq(LinuxAdmin::SysVInitService) 11 | end 12 | 13 | it "should memoize results" do 14 | expect(described_class).to receive(:service_type_uncached).once.and_return("anything_non_nil") 15 | described_class.service_type 16 | described_class.service_type 17 | end 18 | 19 | it "with reload should refresh results" do 20 | expect(described_class).to receive(:service_type_uncached).twice.and_return("anything_non_nil") 21 | described_class.service_type 22 | described_class.service_type(true) 23 | end 24 | end 25 | 26 | context ".new" do 27 | it "on systemctl systems" do 28 | stub_to_service_type(:systemd_service) 29 | expect(described_class.new("xxx")).to be_kind_of(LinuxAdmin::SystemdService) 30 | end 31 | 32 | it "on sysv systems" do 33 | stub_to_service_type(:sys_v_init_service) 34 | expect(described_class.new("xxx")).to be_kind_of(LinuxAdmin::SysVInitService) 35 | end 36 | end 37 | 38 | it "#id / #id=" do 39 | s = described_class.new("xxx") 40 | expect(s.id).to eq("xxx") 41 | 42 | s.id = "yyy" 43 | expect(s.id).to eq("yyy") 44 | expect(s.name).to eq("yyy") 45 | 46 | s.name = "zzz" 47 | expect(s.id).to eq("zzz") 48 | expect(s.name).to eq("zzz") 49 | end 50 | 51 | def stub_to_service_type(system) 52 | allow(LinuxAdmin::Common).to receive(:cmd?).with(:systemctl).and_return(system == :systemd_service) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/linux_admin/distro.rb: -------------------------------------------------------------------------------- 1 | require 'linux_admin/etc_issue' 2 | 3 | module LinuxAdmin 4 | module Distros 5 | def self.generic 6 | @generic ||= Distro.new(:generic) 7 | end 8 | 9 | def self.rhel 10 | @rhel ||= Distro.new(:rhel, '/etc/redhat-release', ['red hat', 'centos'], LinuxAdmin::Rpm) 11 | end 12 | 13 | def self.fedora 14 | @fedora ||= Distro.new(:fedora, "/etc/fedora-release", ['Fedora'], LinuxAdmin::Rpm) 15 | end 16 | 17 | def self.ubuntu 18 | @ubuntu ||= Distro.new(:ubuntu, nil, ['ubuntu'], LinuxAdmin::Deb) 19 | end 20 | 21 | def self.darwin 22 | @darwin ||= Distro.new(:darwin, "/System/Library/CoreServices/SystemVersion.plist", ['darwin'], LinuxAdmin::Homebrew) 23 | end 24 | 25 | def self.all 26 | @distros ||= [rhel, fedora, ubuntu, darwin, generic] 27 | end 28 | 29 | def self.local 30 | @local ||= begin 31 | Distros.all.detect(&:detected?) || Distros.generic 32 | end 33 | end 34 | 35 | class Distro 36 | attr_accessor :release_file, :etc_issue_keywords, :info_class 37 | 38 | def initialize(id, release_file = nil, etc_issue_keywords = [], info_class = nil) 39 | @id = id 40 | @release_file = release_file 41 | @etc_issue_keywords = etc_issue_keywords 42 | @info_class = info_class 43 | end 44 | 45 | def detected? 46 | detected_by_etc_issue? || detected_by_etc_release? 47 | end 48 | 49 | def detected_by_etc_issue? 50 | etc_issue_keywords && etc_issue_keywords.any? { |k| EtcIssue.instance.include?(k) } 51 | end 52 | 53 | def detected_by_etc_release? 54 | release_file && File.exist?(release_file) 55 | end 56 | 57 | def info(pkg) 58 | info_class ? info_class.info(pkg) : nil 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/linux_admin/registration_system.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class RegistrationSystem 3 | include Logging 4 | 5 | def self.registration_type(reload = false) 6 | @registration_type ||= nil 7 | return @registration_type if @registration_type && !reload 8 | @registration_type = registration_type_uncached 9 | end 10 | 11 | def self.method_missing(meth, *args, &block) 12 | if white_list_methods.include?(meth) 13 | r = self.registration_type.new 14 | raise NotImplementedError, "#{meth} not implemented for #{self.name}" unless r.respond_to?(meth) 15 | r.send(meth, *args, &block) 16 | else 17 | super 18 | end 19 | end 20 | 21 | def registered?(_options = nil) 22 | false 23 | end 24 | 25 | private 26 | 27 | def self.respond_to_missing?(method_name, _include_private = false) 28 | white_list_methods.include?(method_name) 29 | end 30 | private_class_method :respond_to_missing? 31 | 32 | def self.registration_type_uncached 33 | if SubscriptionManager.new.registered? 34 | SubscriptionManager 35 | else 36 | self 37 | end 38 | end 39 | private_class_method :registration_type_uncached 40 | 41 | def self.white_list_methods 42 | @white_list_methods ||= begin 43 | all_methods = RegistrationSystem.instance_methods(false) + SubscriptionManager.instance_methods(false) 44 | all_methods.uniq 45 | end 46 | end 47 | private_class_method :white_list_methods 48 | 49 | def install_server_certificate(server, cert_path) 50 | require 'uri' 51 | host = server.start_with?("http") ? ::URI.parse(server).host : server 52 | 53 | LinuxAdmin::Rpm.upgrade("http://#{host}/#{cert_path}") 54 | end 55 | end 56 | end 57 | 58 | Dir.glob(File.join(File.dirname(__FILE__), "registration_system", "*.rb")).each { |f| require f } 59 | -------------------------------------------------------------------------------- /lib/linux_admin/physical_volume.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class PhysicalVolume < Volume 3 | # physical volume device name 4 | attr_accessor :device_name 5 | 6 | # volume group name 7 | attr_accessor :volume_group 8 | 9 | # physical volume size in kilobytes 10 | attr_accessor :size 11 | 12 | # other fields available 13 | # internal physical volume number (obsolete) 14 | # physical volume status 15 | # physical volume (not) allocatable 16 | # current number of logical volumes on this physical volume 17 | # physical extent size in kilobytes 18 | # total number of physical extents 19 | # free number of physical extents 20 | # allocated number of physical extents 21 | 22 | def initialize(args = {}) 23 | @device_name = args[:device_name] 24 | @volume_group = args[:volume_group] 25 | @size = args[:size] 26 | end 27 | 28 | def attach_to(vg) 29 | Common.run!(Common.cmd(:vgextend), 30 | :params => [vg.name, @device_name]) 31 | self.volume_group = vg 32 | self 33 | end 34 | 35 | # specify disk or partition instance to create physical volume on 36 | def self.create(device) 37 | self.scan # initialize local physical volumes 38 | Common.run!(Common.cmd(:pvcreate), 39 | :params => { nil => device.path}) 40 | pv = PhysicalVolume.new(:device_name => device.path, 41 | :volume_group => nil, 42 | :size => device.size) 43 | @pvs << pv 44 | pv 45 | end 46 | 47 | def self.scan 48 | @pvs ||= begin 49 | scan_volumes(Common.cmd(:pvdisplay)) do |fields, vg| 50 | PhysicalVolume.new(:device_name => fields[0], 51 | :volume_group => vg, 52 | :size => fields[2].to_i) 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/linux_admin/volume_group.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class VolumeGroup 3 | # volume group name 4 | attr_accessor :name 5 | 6 | # other fields available 7 | # volume group access 8 | # volume group status 9 | # internal volume group number 10 | # maximum number of logical volumes 11 | # current number of logical volumes 12 | # open count of all logical volumes in this volume group 13 | # maximum logical volume size 14 | # maximum number of physical volumes 15 | # current number of physical volumes 16 | # actual number of physical volumes 17 | # size of volume group in kilobytes 18 | # physical extent size 19 | # total number of physical extents for this volume group 20 | # allocated number of physical extents for this volume group 21 | # free number of physical extents for this volume group 22 | # uuid of volume group 23 | 24 | def initialize(args = {}) 25 | @name = args[:name] 26 | end 27 | 28 | def attach_to(lv) 29 | Common.run!(Common.cmd(:lvextend), 30 | :params => [lv.name, name]) 31 | self 32 | end 33 | 34 | def extend_with(pv) 35 | Common.run!(Common.cmd(:vgextend), 36 | :params => [@name, pv.device_name]) 37 | pv.volume_group = self 38 | self 39 | end 40 | 41 | def self.create(name, pv) 42 | self.scan # initialize local volume groups 43 | Common.run!(Common.cmd(:vgcreate), 44 | :params => [name, pv.device_name]) 45 | vg = VolumeGroup.new :name => name 46 | pv.volume_group = vg 47 | @vgs << vg 48 | vg 49 | end 50 | 51 | def self.scan 52 | @vgs ||= begin 53 | vgs = [] 54 | 55 | out = Common.run!(Common.cmd(:vgdisplay), :params => {'-c' => nil}).output 56 | 57 | out.each_line do |line| 58 | fields = line.lstrip.split(':') 59 | vgs << VolumeGroup.new(:name => fields[0]) 60 | end 61 | 62 | vgs 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/scap_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Scap do 2 | subject { described_class.new("rhel7") } 3 | 4 | describe "#lockdown" do 5 | it "raises if given no rules" do 6 | allow(described_class).to receive(:openscap_available?).and_return(true) 7 | allow(described_class).to receive(:ssg_available?).and_return(true) 8 | 9 | expect { subject.lockdown("value1" => true) }.to raise_error(RuntimeError) 10 | end 11 | end 12 | 13 | describe "#profile_xml (private)" do 14 | it "creates a Profile tag" do 15 | profile_xml = subject.send(:profile_xml, "test-profile", [], {}) 16 | expect(profile_xml).to match(%r{.*}m) 17 | end 18 | 19 | it "creates a title tag" do 20 | profile_xml = subject.send(:profile_xml, "test-profile", [], {}) 21 | expect(profile_xml).to match(%r{test-profile}m) 22 | end 23 | 24 | it "creates a description tag" do 25 | profile_xml = subject.send(:profile_xml, "test-profile", [], {}) 26 | expect(profile_xml).to match(%r{test-profile}m) 27 | end 28 | 29 | it "creates a select tag for each rule" do 30 | profile_xml = subject.send(:profile_xml, "test-profile", %w(rule1 rule2), {}) 31 | expect(profile_xml).to match(%r{}m) 33 | end 34 | 35 | it "creates a refine-value tag for each value" do 36 | profile_xml = subject.send(:profile_xml, "test-profile", [], "key1" => "val1", "key2" => "val2") 37 | expect(profile_xml).to match(%r{}m) 38 | expect(profile_xml).to match(%r{}m) 39 | end 40 | end 41 | 42 | describe ".ds_file" do 43 | it "returns the platform ds file path" do 44 | file = described_class.ds_file("rhel7") 45 | expect(file.to_s).to eq("/usr/share/xml/scap/ssg/content/ssg-rhel7-ds.xml") 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /linux_admin.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'linux_admin/version' 5 | 6 | Gem::Specification.new do |spec| 7 | 8 | # Dynamically create the authors information {name => e-mail} 9 | authors_hash = Hash[`git log --no-merges --reverse --format='%an,%ae'`.split("\n").uniq.collect {|i| i.split(",")}] 10 | 11 | spec.name = "linux_admin" 12 | spec.version = LinuxAdmin::VERSION 13 | spec.authors = authors_hash.keys 14 | spec.email = authors_hash.values 15 | spec.description = %q{ 16 | LinuxAdmin is a module to simplify management of linux systems. 17 | It should be a single place to manage various system level configurations, 18 | registration, updates, etc. 19 | } 20 | spec.summary = %q{LinuxAdmin is a module to simplify management of linux systems.} 21 | spec.homepage = "http://github.com/ManageIQ/linux_admin" 22 | spec.license = "MIT" 23 | 24 | spec.files = `git ls-files -- lib/*`.split("\n") 25 | spec.files += %w[README.md LICENSE.txt] 26 | spec.executables = `git ls-files -- bin/*`.split("\n") 27 | spec.require_paths = ["lib"] 28 | 29 | spec.required_ruby_version = Gem::Requirement.new(">= 2.6") 30 | 31 | spec.add_development_dependency "manageiq-style" 32 | spec.add_development_dependency "rake" 33 | spec.add_development_dependency "rspec", "~> 3.0" 34 | spec.add_development_dependency "rubocop" 35 | 36 | spec.add_dependency "awesome_spawn", "~> 1.6" 37 | spec.add_dependency "bcrypt_pbkdf", ">= 1.0", "< 2.0" 38 | spec.add_dependency "ed25519", ">= 1.2", "< 2.0" 39 | spec.add_dependency "inifile" 40 | spec.add_dependency "more_core_extensions", "~> 4.5", ">= 4.5.1" 41 | spec.add_dependency "net-ssh", "~> 7.2.3" 42 | spec.add_dependency "nokogiri", ">= 1.8.5", "!=1.10.0", "!=1.10.1", "!=1.10.2", "<2" 43 | spec.add_dependency "openscap" 44 | spec.add_development_dependency "simplecov", ">= 0.21.2" 45 | end 46 | -------------------------------------------------------------------------------- /lib/linux_admin/service/systemd_service.rb: -------------------------------------------------------------------------------- 1 | require "time" 2 | 3 | module LinuxAdmin 4 | class SystemdService < Service 5 | def running? 6 | Common.run(command_path, :params => ["status", name]).success? 7 | end 8 | 9 | def enable 10 | Common.run!(command_path, :params => ["enable", name]) 11 | self 12 | end 13 | 14 | def disable 15 | Common.run!(command_path, :params => ["disable", name]) 16 | self 17 | end 18 | 19 | def start(enable = false) 20 | if enable 21 | Common.run!(command_path, :params => ["enable", "--now", name]) 22 | else 23 | Common.run!(command_path, :params => ["start", name]) 24 | end 25 | self 26 | end 27 | 28 | def stop 29 | Common.run!(command_path, :params => ["stop", name]) 30 | self 31 | end 32 | 33 | def restart 34 | status = Common.run(command_path, :params => ["restart", name]).exit_status 35 | 36 | # attempt to manually stop/start if restart fails 37 | if status != 0 38 | stop 39 | start 40 | end 41 | 42 | self 43 | end 44 | 45 | def reload 46 | Common.run!(command_path, :params => ["reload", name]) 47 | self 48 | end 49 | 50 | def status 51 | Common.run(command_path, :params => ["status", name]).output 52 | end 53 | 54 | def show 55 | output = Common.run!(command_path, :params => ["show", name]).output 56 | output.split("\n").each_with_object({}) do |line, h| 57 | k, v = line.split("=", 2) 58 | h[k] = cast_show_value(k, v) 59 | end 60 | end 61 | 62 | private 63 | 64 | def command_path 65 | Common.cmd(:systemctl) 66 | end 67 | 68 | def cast_show_value(key, value) 69 | return value.to_i if value =~ /^\d+$/ 70 | 71 | case key 72 | when /^.*Timestamp$/ 73 | Time.parse(value) 74 | when /Exec(Start|Stop)/ 75 | parse_exec_value(value) 76 | else 77 | value 78 | end 79 | end 80 | 81 | def parse_exec_value(value) 82 | value[1..-2].strip.split(" ; ").map { |s| s.split("=", 2) }.to_h 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/deb_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Deb do 2 | describe "#info" do 3 | it "returns package metadata" do 4 | # as output w/ apt-cache show ruby on ubuntu 13.04 5 | data = < 11 | Original-Maintainer: akira yamada 12 | Architecture: all 13 | Source: ruby-defaults 14 | Version: 4.9 15 | Replaces: irb, rdoc 16 | Provides: irb, rdoc 17 | Depends: ruby1.9.1 (>= 1.9.3.194-1) 18 | Suggests: ri, ruby-dev 19 | Conflicts: irb, rdoc 20 | Filename: pool/main/r/ruby-defaults/ruby_4.9_all.deb 21 | Size: 4896 22 | MD5sum: b1991f2e0eafb04f5930ed242cfe1476 23 | SHA1: a7c55fbb83dd8382631ea771b5555d989351f840 24 | SHA256: 84d042e0273bd2f0082dd9e7dda0246267791fd09607041a35485bfff92f38d9 25 | Description-en: Interpreter of object-oriented scripting language Ruby (default version) 26 | Ruby is the interpreted scripting language for quick and easy 27 | object-oriented programming. It has many features to process text 28 | files and to do system management tasks (as in perl). It is simple, 29 | straight-forward, and extensible. 30 | . 31 | This package is a dependency package, which depends on Debian's default Ruby 32 | version (currently v1.9.3). 33 | Homepage: http://www.ruby-lang.org/ 34 | Description-md5: da2991b37e3991230d79ba70f9c01682 35 | Bugs: https://bugs.launchpad.net/ubuntu/+filebug 36 | Origin: Ubuntu 37 | Supported: 9m 38 | Task: kubuntu-desktop, kubuntu-full, kubuntu-active, kubuntu-active-desktop, kubuntu-active-full, kubuntu-active, edubuntu-desktop-gnome, ubuntustudio-font-meta 39 | EOS 40 | expect(LinuxAdmin::Common).to receive(:run!) 41 | .with(described_class::APT_CACHE_CMD, :params => %w(show ruby)) 42 | .and_return(double(:output => data)) 43 | metadata = described_class.info("ruby") 44 | expect(metadata['package']).to eq('ruby') 45 | expect(metadata['priority']).to eq('optional') 46 | expect(metadata['section']).to eq('interpreters') 47 | expect(metadata['architecture']).to eq('all') 48 | expect(metadata['version']).to eq('4.9') 49 | expect(metadata['origin']).to eq('Ubuntu') 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/chrony_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Chrony do 2 | CHRONY_CONF = <<-EOF 3 | # commented server baz.example.net 4 | pool bar.example.net iburst 5 | server foo.example.net 6 | server bar.example.net iburst 7 | driftfile /var/lib/chrony/drift 8 | makestep 10 3 9 | rtcsync 10 | EOF 11 | 12 | subject do 13 | allow(File).to receive(:exist?).and_return(true) 14 | described_class.new 15 | end 16 | 17 | describe ".new" do 18 | it "raises when the given config file doesn't exist" do 19 | expect { described_class.new("nonsense/file") }.to raise_error(LinuxAdmin::MissingConfigurationFileError) 20 | end 21 | end 22 | 23 | describe "#clear_servers" do 24 | it "removes all the server lines from the conf file" do 25 | allow(File).to receive(:read).and_return(CHRONY_CONF.dup) 26 | expect(File).to receive(:write) do |_file, contents| 27 | expect(contents).to eq "# commented server baz.example.net\ndriftfile /var/lib/chrony/drift\nmakestep 10 3\nrtcsync\n" 28 | end 29 | subject.clear_servers 30 | end 31 | end 32 | 33 | describe "#add_servers" do 34 | it "adds server lines to the conf file" do 35 | allow(File).to receive(:read).and_return(CHRONY_CONF.dup) 36 | expect(File).to receive(:write) do |_file, contents| 37 | expect(contents).to eq(CHRONY_CONF + "server baz.example.net iburst\nserver foo.bar.example.com iburst\n") 38 | end 39 | allow(subject).to receive(:restart_service_if_running) 40 | subject.add_servers("baz.example.net", "foo.bar.example.com") 41 | end 42 | 43 | it "restarts the service if it is running" do 44 | allow(File).to receive(:read).and_return(CHRONY_CONF.dup) 45 | allow(File).to receive(:write) 46 | 47 | chronyd_service = double 48 | expect(LinuxAdmin::Service).to receive(:new).with("chronyd").and_return(chronyd_service) 49 | expect(chronyd_service).to receive(:running?).and_return true 50 | expect(chronyd_service).to receive(:restart) 51 | subject.add_servers("baz.example.net", "foo.bar.example.com") 52 | end 53 | 54 | it "doesn't restart the service if it is not running" do 55 | allow(File).to receive(:read).and_return(CHRONY_CONF.dup) 56 | allow(File).to receive(:write) 57 | 58 | chronyd_service = double 59 | expect(LinuxAdmin::Service).to receive(:new).with("chronyd").and_return(chronyd_service) 60 | expect(chronyd_service).to receive(:running?).and_return false 61 | expect(chronyd_service).not_to receive(:restart) 62 | subject.add_servers("baz.example.net", "foo.bar.example.com") 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/linux_admin/scap.rb: -------------------------------------------------------------------------------- 1 | require 'nokogiri' 2 | 3 | module LinuxAdmin 4 | class Scap 5 | PROFILE_ID = "xccdf_org.ssgproject.content_profile_linux-admin-scap".freeze 6 | SSG_XML_PATH = Pathname.new("/usr/share/xml/scap/ssg/content/") 7 | 8 | attr_reader :platform 9 | 10 | def self.openscap_available? 11 | require 'openscap' 12 | true 13 | rescue LoadError 14 | false 15 | end 16 | 17 | def self.ssg_available?(platform) 18 | ds_file(platform).exist? 19 | end 20 | 21 | def self.ds_file(platform) 22 | SSG_XML_PATH.join("ssg-#{platform}-ds.xml") 23 | end 24 | 25 | def initialize(platform) 26 | @platform = platform 27 | end 28 | 29 | def lockdown(*args) 30 | raise "OpenSCAP not available" unless self.class.openscap_available? 31 | raise "SCAP Security Guide not available" unless self.class.ssg_available?(platform) 32 | 33 | values = args.last.kind_of?(Hash) ? args.pop : {} 34 | rules = args 35 | 36 | raise "No SCAP rules provided" if rules.empty? 37 | 38 | with_ds_file(rules, values) do |path| 39 | lockdown_profile(path, PROFILE_ID) 40 | end 41 | end 42 | 43 | def lockdown_profile(ds_path, profile_id) 44 | raise "OpenSCAP not available" unless self.class.openscap_available? 45 | 46 | session = OpenSCAP::Xccdf::Session.new(ds_path) 47 | session.load 48 | session.profile = profile_id 49 | session.evaluate 50 | session.remediate 51 | ensure 52 | session.destroy if session 53 | end 54 | 55 | private 56 | 57 | def with_ds_file(rules, values) 58 | Tempfile.create("scap_ds") do |f| 59 | write_ds_xml(f, profile_xml(PROFILE_ID, rules, values)) 60 | f.close 61 | yield f.path 62 | end 63 | end 64 | 65 | def profile_xml(profile_id, rules, values) 66 | builder = Nokogiri::XML::Builder.new do |xml| 67 | xml.Profile(:id => profile_id) do 68 | xml.title(profile_id) 69 | xml.description(profile_id) 70 | rules.each { |r| xml.select(:idref => r, :selected => "true") } 71 | values.each { |k, v| xml.send("refine-value", :idref => k, :selector => v) } 72 | end 73 | end 74 | builder.doc.root.to_xml 75 | end 76 | 77 | def write_ds_xml(io, profile_xml) 78 | File.open(self.class.ds_file(platform)) do |f| 79 | doc = Nokogiri::XML(f) 80 | model_xml_element(doc).add_next_sibling("\n#{profile_xml}") 81 | io.write(doc.root.to_xml) 82 | end 83 | end 84 | 85 | def model_xml_element(doc) 86 | doc.xpath("//ns10:model").first 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/linux_admin/ssh.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class SSH 3 | attr_reader :ip 4 | attr_reader :username 5 | attr_reader :private_key 6 | attr_reader :agent 7 | 8 | def initialize(ip, username, private_key = nil, password = nil) 9 | @ip = ip 10 | @private_key = private_key 11 | @username = username 12 | @password = password 13 | end 14 | 15 | def perform_commands(commands = [], agent_socket = nil, stdin = nil) 16 | require 'net/ssh' 17 | if block_given? 18 | execute_commands(commands, agent_socket, stdin, &Proc.new) 19 | else 20 | execute_commands(commands, agent_socket, stdin) 21 | end 22 | end 23 | 24 | private 25 | 26 | def execute_commands(commands, agent_socket, stdin) 27 | result = nil 28 | args = {:verify_host_key => false, :number_of_password_prompts => 0} 29 | if agent_socket 30 | args.merge!(:forward_agent => true, 31 | :agent_socket_factory => -> { UNIXSocket.open(agent_socket) }) 32 | elsif @private_key 33 | args[:key_data] = [@private_key] 34 | elsif @password 35 | args[:password] = @password 36 | end 37 | Net::SSH.start(@ip, @username, args) do |ssh| 38 | if block_given? 39 | result = yield ssh 40 | else 41 | commands.each do |cmd| 42 | result = ssh_exec!(ssh, cmd, stdin) 43 | result[:last_command] = cmd 44 | break if result[:exit_status] != 0 45 | end 46 | end 47 | end 48 | result 49 | end 50 | 51 | def ssh_exec!(ssh, command, stdin) 52 | stdout_data = '' 53 | stderr_data = '' 54 | exit_status = nil 55 | exit_signal = nil 56 | 57 | ssh.open_channel do |channel| 58 | channel.request_pty unless stdin 59 | channel.exec(command) do |_, success| 60 | channel.send_data(stdin) if stdin 61 | channel.eof! 62 | raise StandardError, "Command \"#{command}\" was unable to execute" unless success 63 | channel.on_data do |_, data| 64 | stdout_data << data 65 | end 66 | channel.on_extended_data do |_, _, data| 67 | stderr_data << data 68 | end 69 | channel.on_request('exit-status') do |_, data| 70 | exit_status = data.read_long 71 | end 72 | 73 | channel.on_request('exit-signal') do |_, data| 74 | exit_signal = data.read_long 75 | end 76 | end 77 | end 78 | ssh.loop 79 | { 80 | :stdout => stdout_data, 81 | :stderr => stderr_data, 82 | :exit_status => exit_status, 83 | :exit_signal => exit_signal 84 | } 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/distro_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Distros::Distro do 2 | let(:subject) { LinuxAdmin::Distros.local } 3 | describe "#local" do 4 | before do 5 | allow(LinuxAdmin::Distros).to receive(:local).and_call_original 6 | end 7 | 8 | [['ubuntu', :ubuntu], 9 | ['Fedora', :fedora], 10 | ['red hat', :rhel], 11 | ['CentOS', :rhel], 12 | ['centos', :rhel]].each do |i, d| 13 | context "/etc/issue contains '#{i}'" do 14 | before(:each) do 15 | etc_issue_contains(i) 16 | exists("/etc/fedora-release" => false, "/etc/redhat-release" => false, "/System/Library/CoreServices/SystemVersion.plist" => false) 17 | end 18 | 19 | it "returns Distros.#{d}" do 20 | distro = LinuxAdmin::Distros.send(d) 21 | expect(subject).to eq(distro) 22 | end 23 | end 24 | end 25 | 26 | context "/etc/issue did not match" do 27 | before(:each) do 28 | etc_issue_contains('') 29 | end 30 | 31 | context "/etc/redhat-release exists" do 32 | it "returns Distros.rhel" do 33 | exists("/etc/fedora-release" => false, "/etc/redhat-release" => true) 34 | expect(subject).to eq(LinuxAdmin::Distros.rhel) 35 | end 36 | end 37 | 38 | context "/etc/fedora-release exists" do 39 | it "returns Distros.fedora" do 40 | exists("/etc/fedora-release" => true, "/etc/redhat-release" => false) 41 | expect(subject).to eq(LinuxAdmin::Distros.fedora) 42 | end 43 | end 44 | end 45 | 46 | it "returns Distro.darwin" do 47 | etc_issue_contains('') 48 | exists("/etc/fedora-release" => false, "/etc/redhat-release" => false, "/System/Library/CoreServices/SystemVersion.plist" => true) 49 | expect(subject).to eq(LinuxAdmin::Distros.darwin) 50 | end 51 | 52 | it "returns Distros.generic" do 53 | etc_issue_contains('') 54 | exists("/etc/fedora-release" => false, "/etc/redhat-release" => false, "/System/Library/CoreServices/SystemVersion.plist" => false) 55 | expect(subject).to eq(LinuxAdmin::Distros.generic) 56 | end 57 | end 58 | 59 | describe "#info" do 60 | it "dispatches to redhat lookup mechanism" do 61 | stub_distro(LinuxAdmin::Distros.rhel) 62 | expect(LinuxAdmin::Rpm).to receive(:info).with('ruby') 63 | LinuxAdmin::Distros.local.info 'ruby' 64 | end 65 | 66 | it "dispatches to ubuntu lookup mechanism" do 67 | stub_distro(LinuxAdmin::Distros.ubuntu) 68 | expect(LinuxAdmin::Deb).to receive(:info).with('ruby') 69 | LinuxAdmin::Distros.local.info 'ruby' 70 | end 71 | 72 | it "dispatches to ubuntu lookup mechanism" do 73 | stub_distro(LinuxAdmin::Distros.generic) 74 | expect { LinuxAdmin::Distros.local.info 'ruby' }.not_to raise_error 75 | end 76 | end 77 | 78 | private 79 | 80 | def exists(files) 81 | files.each_pair { |file, value| allow(File).to receive(:exist?).with(file).and_return(value) } 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/registration_system_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::RegistrationSystem do 2 | context ".registration_type" do 3 | it "when registered Subscription Manager" do 4 | stub_registered_to_system(:sm) 5 | expect(described_class.registration_type).to eq(LinuxAdmin::SubscriptionManager) 6 | end 7 | 8 | it "when unregistered" do 9 | stub_registered_to_system(nil) 10 | expect(described_class.registration_type).to eq(described_class) 11 | end 12 | 13 | it "should memoize results" do 14 | expect(described_class).to receive(:registration_type_uncached).once.and_return("anything_non_nil") 15 | described_class.registration_type 16 | described_class.registration_type 17 | end 18 | 19 | it "with reload should refresh results" do 20 | expect(described_class).to receive(:registration_type_uncached).twice.and_return("anything_non_nil") 21 | described_class.registration_type 22 | described_class.registration_type(true) 23 | end 24 | end 25 | 26 | context "#registered?" do 27 | it "when unregistered" do 28 | stub_registered_to_system(nil) 29 | expect(described_class.registered?).to be_falsey 30 | end 31 | 32 | context "SubscriptionManager" do 33 | it "with no args" do 34 | expect(LinuxAdmin::Common).to receive(:run).with("subscription-manager identity").and_return(double(:exit_status => 0)) 35 | 36 | LinuxAdmin::SubscriptionManager.new.registered? 37 | end 38 | 39 | it "with a proxy" do 40 | expect(LinuxAdmin::Common).to receive(:run).with( 41 | "subscription-manager identity", { 42 | :params => { 43 | "--proxy=" => "https://example.com", 44 | "--proxyuser=" => "user", 45 | "--proxypassword=" => "pass" 46 | } 47 | } 48 | ).and_return(double(:exit_status => 0)) 49 | 50 | LinuxAdmin::SubscriptionManager.new.registered?( 51 | :proxy_address => "https://example.com", 52 | :proxy_username => "user", 53 | :proxy_password => "pass" 54 | ) 55 | end 56 | end 57 | end 58 | 59 | context ".method_missing" do 60 | before do 61 | stub_registered_to_system(:sm) 62 | end 63 | 64 | it "exists on the subclass" do 65 | expect(LinuxAdmin::RegistrationSystem.registered?).to be_truthy 66 | end 67 | 68 | it "is an unknown method" do 69 | expect { LinuxAdmin::RegistrationSystem.method_does_not_exist }.to raise_error(NoMethodError) 70 | end 71 | 72 | it "responds to whitelisted methods" do 73 | expect(LinuxAdmin::RegistrationSystem).to respond_to(:refresh) 74 | end 75 | 76 | it "does not respond to methods that were not whitelisted" do 77 | expect(LinuxAdmin::RegistrationSystem).to_not respond_to(:method_does_not_exist) 78 | end 79 | end 80 | 81 | def stub_registered_to_system(*system) 82 | allow_any_instance_of(LinuxAdmin::SubscriptionManager).to receive_messages(:registered? => (system.include?(:sm))) 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/dns_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Dns do 2 | RESOLV_CONF = < false, :forward_agent => true, :number_of_password_prompts => 0, :agent_socket_factory => Proc}).and_return(true) 38 | ssh_agent = LinuxAdmin::SSHAgent.new(@example_ssh_key) 39 | ssh_agent.with_service do |socket| 40 | ssh = LinuxAdmin::SSH.new("127.0.0.1", "root") 41 | ssh.perform_commands(%w("ls", "pwd"), socket) 42 | end 43 | end 44 | 45 | it "should preform command using private key" do 46 | expect(Net::SSH).to receive(:start).with("127.0.0.1", "root", {:verify_host_key => false, :number_of_password_prompts => 0, :key_data => [@example_ssh_key]}).and_return(true) 47 | LinuxAdmin::SSH.new("127.0.0.1", "root", @example_ssh_key).perform_commands(%w("ls", "pwd")) 48 | end 49 | 50 | it "should preform command using password" do 51 | expect(Net::SSH).to receive(:start).with("127.0.0.1", "root", {:verify_host_key => false, :number_of_password_prompts => 0, :password => "password"}).and_return(true) 52 | LinuxAdmin::SSH.new("127.0.0.1", "root", nil, "password").perform_commands(%w("ls", "pwd")) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/linux_admin/logical_volume.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module LinuxAdmin 4 | class LogicalVolume < Volume 5 | include Mountable 6 | 7 | DEVICE_PATH = Pathname.new('/dev/') 8 | 9 | # logical volume name 10 | attr_reader :name 11 | 12 | # volume group name 13 | attr_accessor :volume_group 14 | 15 | # logical volume size in sectors 16 | attr_accessor :sectors 17 | 18 | # other fields available: 19 | # logical volume access 20 | # logical volume status 21 | # internal logical volume number 22 | # open count of logical volume 23 | # current logical extents associated to logical volume 24 | # allocated logical extents of logical volume 25 | # allocation policy of logical volume 26 | # read ahead sectors of logical volume 27 | # major device number of logical volume 28 | # minor device number of logical volume 29 | 30 | # path to logical volume 31 | def path 32 | "/dev/#{self.volume_group.name}/#{self.name}" 33 | end 34 | 35 | def path=(value) 36 | @path = value.include?(File::SEPARATOR) ? value : DEVICE_PATH.join(@volume_group.name, value) 37 | end 38 | 39 | def name=(value) 40 | @name = value.include?(File::SEPARATOR) ? value.split(File::SEPARATOR).last : value 41 | end 42 | 43 | def initialize(args = {}) 44 | @volume_group = args[:volume_group] 45 | @sectors = args[:sectors] 46 | provided_name = args[:name].to_s 47 | self.path = provided_name 48 | self.name = provided_name 49 | end 50 | 51 | def extend_with(vg) 52 | Common.run!(Common.cmd(:lvextend), 53 | :params => [self.name, vg.name]) 54 | self 55 | end 56 | 57 | private 58 | 59 | def self.bytes_to_string(bytes) 60 | if bytes > 1_073_741_824 # 1.gigabytes 61 | (bytes / 1_073_741_824).to_s + "G" 62 | elsif bytes > 1_048_576 # 1.megabytes 63 | (bytes / 1_048_576).to_s + "M" 64 | elsif bytes > 1_024 # 1.kilobytes 65 | (bytes / 1_024).to_s + "K" 66 | else 67 | bytes.to_s 68 | end 69 | end 70 | 71 | public 72 | 73 | def self.create(name, vg, value) 74 | self.scan # initialize local logical volumes 75 | params = { '-n' => name, nil => vg.name} 76 | size = nil 77 | if value <= 100 78 | # size = # TODO size from extents 79 | params.merge!({'-l' => "#{value}%FREE"}) 80 | else 81 | size = value 82 | params.merge!({'-L' => bytes_to_string(size)}) 83 | end 84 | Common.run!(Common.cmd(:lvcreate), :params => params) 85 | 86 | lv = LogicalVolume.new(:name => name, 87 | :volume_group => vg, 88 | :sectors => size) 89 | @lvs << lv 90 | lv 91 | end 92 | 93 | def self.scan 94 | @lvs ||= begin 95 | scan_volumes(Common.cmd(:lvdisplay)) do |fields, vg| 96 | LogicalVolume.new(:name => fields[0], 97 | :volume_group => vg, 98 | :sectors => fields[6].to_i) 99 | end 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/linux_admin/fstab.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | module LinuxAdmin 4 | class FSTabEntry 5 | attr_accessor :device 6 | attr_accessor :mount_point 7 | attr_accessor :fs_type 8 | attr_accessor :mount_options 9 | attr_accessor :dumpable 10 | attr_accessor :fsck_order 11 | attr_accessor :comment 12 | 13 | def initialize(args = {}) 14 | @device = args[:device] 15 | @mount_point = args[:mount_point] 16 | @fs_type = args[:fs_type] 17 | @mount_options = args[:mount_options] 18 | @dumpable = args[:dumpable].to_i unless args[:dumpable].nil? 19 | @fsck_order = args[:fsck_order].to_i unless args[:fsck_order].nil? 20 | @comment = args[:comment] 21 | end 22 | 23 | def self.from_line(fstab_line) 24 | columns, comment = fstab_line.split('#') 25 | comment = "##{comment}" unless comment.blank? 26 | columns = columns.chomp.split 27 | 28 | FSTabEntry.new(:device => columns[0], 29 | :mount_point => columns[1], 30 | :fs_type => columns[2], 31 | :mount_options => columns[3], 32 | :dumpable => columns[4], 33 | :fsck_order => columns[5], 34 | :comment => comment) 35 | end 36 | 37 | def has_content? 38 | !self.columns.first.nil? 39 | end 40 | 41 | def columns 42 | [self.device, self.mount_point, self.fs_type, 43 | self.mount_options, self.dumpable, self.fsck_order, self.comment] 44 | end 45 | 46 | def column_lengths 47 | columns.collect { |c| c ? c.to_s.size : 0 } 48 | end 49 | 50 | def formatted_columns(max_lengths) 51 | self.columns.collect. 52 | with_index { |col, i| col.to_s.rjust(max_lengths[i]) }.join(" ").rstrip 53 | end 54 | end 55 | 56 | class FSTab 57 | include Singleton 58 | 59 | def initialize 60 | refresh 61 | end 62 | 63 | def entries 64 | @entries ||= LinuxAdmin::FSTab::EntryCollection.new 65 | end 66 | 67 | def maximum_column_lengths 68 | entries.maximum_column_lengths 69 | end 70 | 71 | def write! 72 | content = '' 73 | entries.each do |entry| 74 | if entry.has_content? 75 | content << entry.formatted_columns(entries.maximum_column_lengths) << "\n" 76 | else 77 | content << "#{entry.comment}" 78 | end 79 | end 80 | 81 | File.write('/etc/fstab', content) 82 | self 83 | end 84 | 85 | private 86 | 87 | def read 88 | File.read('/etc/fstab').lines 89 | end 90 | 91 | def refresh 92 | @entries = nil 93 | read.each do |line| 94 | entry = FSTabEntry.from_line(line) 95 | entries << entry 96 | end 97 | end 98 | 99 | class EntryCollection < Array 100 | attr_reader :maximum_column_lengths 101 | 102 | def initialize 103 | @maximum_column_lengths = Array.new(7, 0) # # of columns 104 | end 105 | 106 | def <<(entry) 107 | lengths = entry.column_lengths 108 | lengths.each_index do |i| 109 | maximum_column_lengths[i] = [lengths[i], maximum_column_lengths[i]].max 110 | end 111 | 112 | super 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/linux_admin/hosts.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class Hosts 3 | attr_accessor :filename 4 | attr_accessor :raw_lines 5 | attr_accessor :parsed_file 6 | 7 | def initialize(filename = "/etc/hosts") 8 | @filename = filename 9 | self.reload 10 | end 11 | 12 | def reload 13 | @raw_lines = File.read(@filename).split("\n") 14 | parse_file 15 | end 16 | 17 | def save 18 | cleanup_empty 19 | @raw_lines = assemble_lines 20 | File.write(@filename, @raw_lines.join("\n") + "\n") 21 | end 22 | 23 | def add_alias(address, hostname, comment = nil) 24 | add_name(address, hostname, false, comment) 25 | end 26 | 27 | alias_method :update_entry, :add_alias 28 | 29 | def set_loopback_hostname(hostname, comment = nil) 30 | ["::1", "127.0.0.1"].each { |address| add_name(address, hostname, true, comment, false) } 31 | end 32 | 33 | def set_canonical_hostname(address, hostname, comment = nil) 34 | add_name(address, hostname, true, comment) 35 | end 36 | 37 | def hostname=(name) 38 | if Common.cmd?("hostnamectl") 39 | Common.run!(Common.cmd('hostnamectl'), :params => ['set-hostname', name]) 40 | else 41 | File.write("/etc/hostname", name) 42 | Common.run!(Common.cmd('hostname'), :params => {:file => "/etc/hostname"}) 43 | end 44 | end 45 | 46 | def hostname 47 | result = Common.run(Common.cmd("hostname")) 48 | result.success? ? result.output.strip : nil 49 | end 50 | 51 | private 52 | 53 | def add_name(address, hostname, fqdn, comment, remove_existing = true) 54 | # Delete entries for this hostname first 55 | @parsed_file.each { |i| i[:hosts].to_a.delete(hostname) } if remove_existing 56 | 57 | # Add entry 58 | line_number = @parsed_file.find_path(address).first 59 | 60 | if line_number.blank? 61 | @parsed_file.push(:address => address, :hosts => [hostname], :comment => comment) 62 | else 63 | if fqdn 64 | new_hosts = @parsed_file.fetch_path(line_number, :hosts).to_a.unshift(hostname) 65 | else 66 | new_hosts = @parsed_file.fetch_path(line_number, :hosts).to_a.push(hostname) 67 | end 68 | @parsed_file.store_path(line_number, :hosts, new_hosts) 69 | @parsed_file.store_path(line_number, :comment, comment) if comment 70 | end 71 | end 72 | 73 | def parse_file 74 | @parsed_file = [] 75 | @raw_lines.each { |line| @parsed_file.push(parse_line(line.strip)) } 76 | @parsed_file.delete_blank_paths 77 | end 78 | 79 | def parse_line(line) 80 | data, comment = line.split("#", 2) 81 | address, hosts = data.to_s.split(" ", 2) 82 | hostnames = hosts.to_s.split(" ") 83 | 84 | { :address => address.to_s, :hosts => hostnames, :comment => comment.to_s.strip, :blank => line.blank?} 85 | end 86 | 87 | def cleanup_empty 88 | @parsed_file.each do |h| 89 | h.delete(:hosts) if h[:address].blank? 90 | h.delete(:address) if h[:hosts].blank? 91 | end 92 | 93 | @parsed_file.delete_blank_paths 94 | end 95 | 96 | def assemble_lines 97 | @parsed_file.each_with_object([]) { |l, a| a.push(l[:blank] ? "" : build_line(l[:address], l[:hosts], l[:comment])) } 98 | end 99 | 100 | def build_line(address, hosts, comment) 101 | line = [address.to_s.ljust(16), hosts.to_a.uniq] 102 | line.push("##{comment}") if comment 103 | line.flatten.join(" ").strip 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/time_date_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::TimeDate do 2 | RUN_COMMAND = LinuxAdmin::Common.cmd("timedatectl") 3 | 4 | def timedatectl_result 5 | output = File.read(Pathname.new(data_file_path("time_date/timedatectl_output"))) 6 | AwesomeSpawn::CommandResult.new("", output, "", 55, 0) 7 | end 8 | 9 | describe ".system_timezone_detailed" do 10 | it "returns the correct timezone" do 11 | awesome_spawn_args = [ 12 | RUN_COMMAND, 13 | :params => ["status"] 14 | ] 15 | expect(AwesomeSpawn).to receive(:run).with(*awesome_spawn_args).and_return(timedatectl_result) 16 | tz = described_class.system_timezone_detailed 17 | expect(tz).to eq("America/New_York (EDT, -0400)") 18 | end 19 | end 20 | 21 | describe ".system_timezone" do 22 | it "returns the correct timezone" do 23 | awesome_spawn_args = [ 24 | RUN_COMMAND, 25 | :params => ["status"] 26 | ] 27 | expect(AwesomeSpawn).to receive(:run).with(*awesome_spawn_args).and_return(timedatectl_result) 28 | tz = described_class.system_timezone 29 | expect(tz).to eq("America/New_York") 30 | end 31 | end 32 | 33 | describe ".timezones" do 34 | let(:timezones) do 35 | <<-EOS 36 | Africa/Bangui 37 | Africa/Banjul 38 | Africa/Bissau 39 | Africa/Blantyre 40 | Africa/Brazzaville 41 | Africa/Bujumbura 42 | Africa/Cairo 43 | America/Havana 44 | America/Hermosillo 45 | America/Indiana/Indianapolis 46 | America/Indiana/Knox 47 | America/Argentina/San_Juan 48 | America/Argentina/San_Luis 49 | America/Argentina/Tucuman 50 | America/Argentina/Ushuaia 51 | EOS 52 | end 53 | 54 | it "returns the correct list" do 55 | awesome_spawn_args = [ 56 | RUN_COMMAND, 57 | :params => ["list-timezones"] 58 | ] 59 | result = AwesomeSpawn::CommandResult.new("", timezones, "", 55, 0) 60 | expect(AwesomeSpawn).to receive(:run!).with(*awesome_spawn_args).and_return(result) 61 | expect(described_class.timezones).to eq(timezones.split("\n")) 62 | end 63 | end 64 | 65 | describe ".system_time=" do 66 | it "sets the time" do 67 | time = Time.new(2015, 1, 1, 1, 1, 1) 68 | awesome_spawn_args = [ 69 | RUN_COMMAND, 70 | :params => ["set-time", "2015-01-01 01:01:01", :adjust_system_clock] 71 | ] 72 | expect(AwesomeSpawn).to receive(:run!).with(*awesome_spawn_args) 73 | described_class.system_time = time 74 | end 75 | 76 | it "raises when the command fails" do 77 | time = Time.new(2015, 1, 1, 1, 1, 1) 78 | err = AwesomeSpawn::CommandResultError.new("message", nil) 79 | allow(AwesomeSpawn).to receive(:run!).and_raise(err) 80 | expect do 81 | described_class.send(:system_time=, time) 82 | end.to raise_error(described_class::TimeCommandError, "message") 83 | end 84 | end 85 | 86 | describe ".system_timezone=" do 87 | it "sets the timezone" do 88 | zone = "Location/City" 89 | awesome_spawn_args = [ 90 | RUN_COMMAND, 91 | :params => ["set-timezone", zone] 92 | ] 93 | expect(AwesomeSpawn).to receive(:run!).with(*awesome_spawn_args) 94 | described_class.system_timezone = zone 95 | end 96 | 97 | it "raises when the command fails" do 98 | zone = "Location/City" 99 | err = AwesomeSpawn::CommandResultError.new("message", nil) 100 | allow(AwesomeSpawn).to receive(:run!).and_raise(err) 101 | expect do 102 | described_class.send(:system_timezone=, zone) 103 | end.to raise_error(described_class::TimeCommandError, "message") 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/volume_group_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::VolumeGroup do 2 | before(:each) do 3 | @groups = < 'vg' 18 | lv = LinuxAdmin::LogicalVolume.new :name => 'lv', :volume_group => vg 19 | expect(LinuxAdmin::Common).to receive(:run!) 20 | .with(LinuxAdmin::Common.cmd(:lvextend), :params => %w(lv vg)) 21 | vg.attach_to(lv) 22 | end 23 | 24 | it "returns self" do 25 | vg = described_class.new :name => 'vg' 26 | lv = LinuxAdmin::LogicalVolume.new :name => 'lv', :volume_group => vg 27 | allow(LinuxAdmin::Common).to receive(:run!) 28 | expect(vg.attach_to(lv)).to eq(vg) 29 | end 30 | end 31 | 32 | describe "#extend_with" do 33 | it "uses vgextend" do 34 | vg = described_class.new :name => 'vg' 35 | pv = LinuxAdmin::PhysicalVolume.new :device_name => '/dev/hda' 36 | expect(LinuxAdmin::Common).to receive(:run!) 37 | .with(LinuxAdmin::Common.cmd(:vgextend), :params => ['vg', '/dev/hda']) 38 | vg.extend_with(pv) 39 | end 40 | 41 | it "assigns volume group to physical volume" do 42 | vg = described_class.new :name => 'vg' 43 | pv = LinuxAdmin::PhysicalVolume.new :device_name => '/dev/hda' 44 | allow(LinuxAdmin::Common).to receive(:run!) 45 | vg.extend_with(pv) 46 | expect(pv.volume_group).to eq(vg) 47 | end 48 | 49 | it "returns self" do 50 | vg = described_class.new :name => 'vg' 51 | pv = LinuxAdmin::PhysicalVolume.new :device_name => '/dev/hda' 52 | allow(LinuxAdmin::Common).to receive(:run!) 53 | expect(vg.extend_with(pv)).to eq(vg) 54 | end 55 | end 56 | 57 | describe "#create" do 58 | before(:each) do 59 | @pv = LinuxAdmin::PhysicalVolume.new :device_name => '/dev/hda' 60 | end 61 | 62 | it "uses vgcreate" do 63 | described_class.instance_variable_set(:@vgs, []) 64 | expect(LinuxAdmin::Common).to receive(:run!) 65 | .with(LinuxAdmin::Common.cmd(:vgcreate), :params => ['vg', '/dev/hda']) 66 | described_class.create 'vg', @pv 67 | end 68 | 69 | it "returns new volume group" do 70 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 71 | vg = described_class.create 'vg', @pv 72 | expect(vg).to be_an_instance_of(described_class) 73 | expect(vg.name).to eq('vg') 74 | end 75 | 76 | it "adds volume group to local registry" do 77 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 78 | vg = described_class.create 'vg', @pv 79 | expect(described_class.scan).to include(vg) 80 | end 81 | end 82 | 83 | describe "#scan" do 84 | it "uses vgdisplay" do 85 | expect(LinuxAdmin::Common).to receive(:run!) 86 | .with(LinuxAdmin::Common.cmd(:vgdisplay), :params => {'-c' => nil}) 87 | .and_return(double(:output => @groups)) 88 | described_class.scan 89 | end 90 | 91 | it "returns local volume groups" do 92 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @groups)) 93 | vgs = described_class.scan 94 | 95 | expect(vgs[0]).to be_an_instance_of(described_class) 96 | expect(vgs[0].name).to eq('vg_foobar') 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/physical_volume_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::PhysicalVolume do 2 | before(:each) do 3 | @physical_volumes = < 'vg' 22 | pv = described_class.new :device_name => '/dev/hda' 23 | expect(LinuxAdmin::Common).to receive(:run!) 24 | .with(LinuxAdmin::Common.cmd(:vgextend), 25 | :params => ['vg', '/dev/hda']) 26 | pv.attach_to(vg) 27 | end 28 | 29 | it "assigns volume group to physical volume" do 30 | vg = LinuxAdmin::VolumeGroup.new :name => 'vg' 31 | pv = described_class.new :device_name => '/dev/hda' 32 | allow(LinuxAdmin::Common).to receive(:run!) 33 | pv.attach_to(vg) 34 | expect(pv.volume_group).to eq(vg) 35 | end 36 | 37 | it "returns self" do 38 | vg = LinuxAdmin::VolumeGroup.new :name => 'vg' 39 | pv = described_class.new :device_name => '/dev/hda' 40 | allow(LinuxAdmin::Common).to receive(:run!) 41 | expect(pv.attach_to(vg)).to eq(pv) 42 | end 43 | end 44 | 45 | describe "#create" do 46 | before do 47 | @disk = LinuxAdmin::Disk.new :path => '/dev/hda' 48 | allow(@disk).to receive(:size) 49 | end 50 | 51 | let(:disk) {@disk} 52 | 53 | it "uses pvcreate" do 54 | described_class.instance_variable_set(:@pvs, []) 55 | expect(LinuxAdmin::Common).to receive(:run!) 56 | .with(LinuxAdmin::Common.cmd(:pvcreate), 57 | :params => {nil => '/dev/hda'}) 58 | described_class.create disk 59 | end 60 | 61 | it "returns new physical volume" do 62 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 63 | pv = described_class.create disk 64 | expect(pv).to be_an_instance_of(described_class) 65 | expect(pv.device_name).to eq('/dev/hda') 66 | end 67 | 68 | it "adds physical volume to local registry" do 69 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 70 | pv = described_class.create disk 71 | expect(described_class.scan).to include(pv) 72 | end 73 | end 74 | 75 | describe "#scan" do 76 | it "uses pvdisplay" do 77 | expect(LinuxAdmin::Common).to receive(:run!) 78 | .with(LinuxAdmin::Common.cmd(:pvdisplay), 79 | :params => {'-c' => nil}) 80 | .and_return(double(:output => @physical_volumes)) 81 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @groups)) # stub out call to vgdisplay 82 | described_class.scan 83 | end 84 | 85 | it "returns local physical volumes" do 86 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @physical_volumes), double(:output => @groups)) 87 | pvs = described_class.scan 88 | 89 | expect(pvs[0]).to be_an_instance_of(described_class) 90 | expect(pvs[0].device_name).to eq('/dev/vda2') 91 | expect(pvs[0].size).to eq(24139776) 92 | end 93 | 94 | it "resolves volume group references" do 95 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @physical_volumes), double(:output => @groups)) 96 | pvs = described_class.scan 97 | expect(pvs[0].volume_group).to be_an_instance_of(LinuxAdmin::VolumeGroup) 98 | expect(pvs[0].volume_group.name).to eq('vg_foobar') 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/rpm_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Rpm do 2 | it ".list_installed" do 3 | allow(LinuxAdmin::Common).to receive(:run!) 4 | .and_return(double(:output => sample_output("rpm/cmd_output_for_list_installed"))) 5 | expect(described_class.list_installed).to eq({ 6 | "ruby193-rubygem-some_really_long_name" =>"1.0.7-1.el6", 7 | "fipscheck-lib" =>"1.2.0-7.el6", 8 | "aic94xx-firmware" =>"30-2.el6", 9 | "latencytop-common" =>"0.5-9.el6", 10 | "uuid" =>"1.6.1-10.el6", 11 | "ConsoleKit" =>"0.4.1-3.el6", 12 | "cpuspeed" =>"1.5-19.el6", 13 | "mailcap" =>"2.1.31-2.el6", 14 | "freetds" =>"0.82-7.1.el6cf", 15 | "elinks" =>"0.12-0.21.pre5.el6_3", 16 | "abrt-cli" =>"2.0.8-15.el6", 17 | "libattr" =>"2.4.44-7.el6", 18 | "passwd" =>"0.77-4.el6_2.2", 19 | "vim-enhanced" =>"7.2.411-1.8.el6", 20 | "popt" =>"1.13-7.el6", 21 | "hesiod" =>"3.1.0-19.el6", 22 | "pinfo" =>"0.6.9-12.el6", 23 | "libpng" =>"1.2.49-1.el6_2", 24 | "libdhash" =>"0.4.2-9.el6", 25 | "zlib-devel" =>"1.2.3-29.el6", 26 | }) 27 | end 28 | 29 | it ".import_key" do 30 | expect(LinuxAdmin::Common).to receive(:run!).with("rpm", :params => {"--import" => "abc"}) 31 | expect { described_class.import_key("abc") }.to_not raise_error 32 | end 33 | 34 | describe "#info" do 35 | it "returns package metadata" do 36 | # as output w/ rpm -qi ruby on F19 37 | data = < {"-qi" => "ruby"}] 62 | result = AwesomeSpawn::CommandResult.new("", data, "", 55, 0) 63 | expect(LinuxAdmin::Common).to receive(:run!).with(*arguments).and_return(result) 64 | metadata = described_class.info("ruby") 65 | expect(metadata['name']).to eq('ruby') 66 | expect(metadata['version']).to eq('2.0.0.247') 67 | expect(metadata['release']).to eq('15.fc19') 68 | expect(metadata['architecture']).to eq('x86_64') 69 | expect(metadata['group']).to eq('Development/Languages') 70 | expect(metadata['size']).to eq('64473') 71 | expect(metadata['license']).to eq('(Ruby or BSD) and Public Domain') 72 | expect(metadata['source_rpm']).to eq('ruby-2.0.0.247-15.fc19.src.rpm') 73 | expect(metadata['build_host']).to eq('buildvm-16.phx2.fedoraproject.org') 74 | expect(metadata['packager']).to eq('Fedora Project') 75 | expect(metadata['vendor']).to eq('Fedora Project') 76 | expect(metadata['summary']).to eq('An interpreter of object-oriented scripting language') 77 | end 78 | end 79 | 80 | it ".upgrade" do 81 | expect(LinuxAdmin::Common).to receive(:run).with("rpm -U", :params => {nil => "abc"}) 82 | .and_return(double(:exit_status => 0)) 83 | expect(described_class.upgrade("abc")).to be_truthy 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/linux_admin/yum.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'inifile' 3 | 4 | module LinuxAdmin 5 | class Yum 6 | def self.create_repo(path, options = {}) 7 | raise ArgumentError, "path is required" unless path 8 | options = {:database => true, :unique_file_names => true}.merge(options) 9 | 10 | FileUtils.mkdir_p(path) 11 | 12 | cmd = "createrepo" 13 | params = {nil => path} 14 | params["--database"] = nil if options[:database] 15 | params["--unique-md-filenames"] = nil if options[:unique_file_names] 16 | 17 | Common.run!(cmd, :params => params) 18 | end 19 | 20 | def self.download_packages(path, packages, options = {}) 21 | raise ArgumentError, "path is required" unless path 22 | raise ArgumentError, "packages are required" unless packages 23 | options = {:mirror_type => :package}.merge(options) 24 | 25 | FileUtils.mkdir_p(path) 26 | 27 | cmd = case options[:mirror_type] 28 | when :package; "repotrack" 29 | else; raise ArgumentError, "mirror_type unsupported" 30 | end 31 | params = {"-p" => path} 32 | params["-a"] = options[:arch] if options[:arch] 33 | params[nil] = packages 34 | 35 | Common.run!(cmd, :params => params) 36 | end 37 | 38 | def self.repo_settings 39 | self.parse_repo_dir("/etc/yum.repos.d") 40 | end 41 | 42 | def self.updates_available?(*packages) 43 | cmd = "yum check-update" 44 | params = {nil => packages} unless packages.blank? 45 | 46 | spawn = Common.run(cmd, :params => params) 47 | case spawn.exit_status 48 | when 0; false 49 | when 100; true 50 | else raise "Error: #{cmd} returns '#{spawn.exit_status}', '#{spawn.error}'" 51 | end 52 | end 53 | 54 | def self.update(*packages) 55 | cmd = "yum -y update" 56 | params = {nil => packages} unless packages.blank? 57 | 58 | out = Common.run!(cmd, :params => params) 59 | 60 | # Handle errors that exit 0 https://bugzilla.redhat.com/show_bug.cgi?id=1141318 61 | raise AwesomeSpawn::CommandResultError.new(out.error, out) if out.error.include?("No Match for argument") 62 | 63 | out 64 | end 65 | 66 | def self.version_available(*packages) 67 | raise ArgumentError, "packages requires at least one package name" if packages.blank? 68 | 69 | cmd = "repoquery --qf=\"%{name} %{version}\"" 70 | params = {nil => packages} 71 | 72 | out = Common.run!(cmd, :params => params).output 73 | 74 | out.split("\n").each_with_object({}) do |i, versions| 75 | name, version = i.split(" ", 2) 76 | versions[name.strip] = version.strip 77 | end 78 | end 79 | 80 | def self.repo_list(scope = "enabled") 81 | # Scopes could be "enabled", "all" 82 | 83 | cmd = "yum repolist" 84 | params = {nil => scope} 85 | output = Common.run!(cmd, :params => params).output 86 | 87 | parse_repo_list_output(output) 88 | end 89 | 90 | private 91 | 92 | def self.parse_repo_dir(dir) 93 | repo_files = Dir.glob(File.join(dir, '*.repo')) 94 | repo_files.each_with_object({}) do |file, content| 95 | content[file] = self.parse_repo_file(file) 96 | end 97 | end 98 | 99 | def self.parse_repo_file(file) 100 | int_keys = ["enabled", "cost", "gpgcheck", "sslverify", "metadata_expire"] 101 | content = IniFile.load(file).to_h 102 | content.each do |name, data| 103 | int_keys.each { |k| data[k] = data[k].to_i if data.has_key?(k) } 104 | end 105 | end 106 | 107 | def self.parse_repo_list_output(content) 108 | collect_content = false 109 | index_start = "repo id" 110 | index_end = "repolist:" 111 | 112 | content.split("\n").each_with_object([]) do |line, array| 113 | collect_content = false if line.start_with?(index_end) 114 | collect_content = true if line.start_with?(index_start) 115 | next if line.start_with?(index_start) 116 | next if !collect_content 117 | 118 | repo_id, _repo_name, _status = line.split(/\s{2,}/) 119 | array.push(repo_id) 120 | end 121 | end 122 | end 123 | end 124 | 125 | Dir.glob(File.join(File.dirname(__FILE__), "yum", "*.rb")).each { |f| require f } 126 | -------------------------------------------------------------------------------- /spec/service/sys_v_init_service_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::SysVInitService do 2 | before do 3 | @service = described_class.new 'foo' 4 | end 5 | 6 | describe "#running?" do 7 | it "checks service" do 8 | expect(LinuxAdmin::Common).to receive(:run) 9 | .with(LinuxAdmin::Common.cmd(:service), 10 | :params => {nil => %w(foo status)}).and_return(double(:exit_status => 0)) 11 | @service.running? 12 | end 13 | 14 | context "service is running" do 15 | it "returns true" do 16 | @service = described_class.new :id => :foo 17 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:exit_status => 0)) 18 | expect(@service).to be_running 19 | end 20 | end 21 | 22 | context "service is not running" do 23 | it "returns false" do 24 | @service = described_class.new :id => :foo 25 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:exit_status => 1)) 26 | expect(@service).not_to be_running 27 | end 28 | end 29 | end 30 | 31 | describe "#enable" do 32 | it "enables service" do 33 | expect(LinuxAdmin::Common).to receive(:run!) 34 | .with(LinuxAdmin::Common.cmd(:chkconfig), 35 | :params => {nil => %w(foo on)}) 36 | @service.enable 37 | end 38 | 39 | it "returns self" do 40 | expect(LinuxAdmin::Common).to receive(:run!) # stub out cmd invocation 41 | expect(@service.enable).to eq(@service) 42 | end 43 | end 44 | 45 | describe "#disable" do 46 | it "disable service" do 47 | expect(LinuxAdmin::Common).to receive(:run!) 48 | .with(LinuxAdmin::Common.cmd(:chkconfig), 49 | :params => {nil => %w(foo off)}) 50 | @service.disable 51 | end 52 | 53 | it "returns self" do 54 | expect(LinuxAdmin::Common).to receive(:run!) 55 | expect(@service.disable).to eq(@service) 56 | end 57 | end 58 | 59 | describe "#start" do 60 | it "starts service" do 61 | expect(LinuxAdmin::Common).to receive(:run!) 62 | .with(LinuxAdmin::Common.cmd(:service), 63 | :params => {nil => %w(foo start)}) 64 | @service.start 65 | end 66 | 67 | it "also enables the service if passed true" do 68 | expect(LinuxAdmin::Common).to receive(:run!) 69 | .with(LinuxAdmin::Common.cmd(:service), 70 | :params => {nil => %w(foo start)}) 71 | expect(LinuxAdmin::Common).to receive(:run!) 72 | .with(LinuxAdmin::Common.cmd(:chkconfig), 73 | :params => {nil => %w(foo on)}) 74 | @service.start(true) 75 | end 76 | 77 | it "returns self" do 78 | expect(LinuxAdmin::Common).to receive(:run!) 79 | expect(@service.start).to eq(@service) 80 | end 81 | end 82 | 83 | describe "#stop" do 84 | it "stops service" do 85 | expect(LinuxAdmin::Common).to receive(:run!) 86 | .with(LinuxAdmin::Common.cmd(:service), 87 | :params => {nil => %w(foo stop)}) 88 | @service.stop 89 | end 90 | 91 | it "returns self" do 92 | expect(LinuxAdmin::Common).to receive(:run!) 93 | expect(@service.stop).to eq(@service) 94 | end 95 | end 96 | 97 | describe "#restart" do 98 | it "stops service" do 99 | expect(LinuxAdmin::Common).to receive(:run) 100 | .with(LinuxAdmin::Common.cmd(:service), 101 | :params => {nil => %w(foo restart)}).and_return(double(:exit_status => 0)) 102 | @service.restart 103 | end 104 | 105 | context "service restart fails" do 106 | it "manually stops/starts service" do 107 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:exit_status => 1)) 108 | expect(@service).to receive(:stop) 109 | expect(@service).to receive(:start) 110 | @service.restart 111 | end 112 | end 113 | 114 | it "returns self" do 115 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:exit_status => 0)) 116 | expect(@service.restart).to eq(@service) 117 | end 118 | end 119 | 120 | describe "#reload" do 121 | it "reloads service" do 122 | expect(LinuxAdmin::Common).to receive(:run!) 123 | .with(LinuxAdmin::Common.cmd(:service), :params => %w(foo reload)) 124 | expect(@service.reload).to eq(@service) 125 | end 126 | end 127 | 128 | describe "#status" do 129 | it "returns the service status" do 130 | status = "service status here" 131 | expect(LinuxAdmin::Common).to receive(:run) 132 | .with(LinuxAdmin::Common.cmd(:service), 133 | :params => %w(foo status)).and_return(double(:output => status)) 134 | expect(@service.status).to eq(status) 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['CI'] 2 | require 'simplecov' 3 | SimpleCov.start 4 | end 5 | 6 | # This file was generated by the `rspec --init` command. Conventionally, all 7 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 8 | # The generated `.rspec` file contains `--require spec_helper` which will cause this 9 | # file to always be loaded, without a need to explicitly require it in any files. 10 | # 11 | # Given that it is always loaded, you are encouraged to keep this file as 12 | # light-weight as possible. Requiring heavyweight dependencies from this file 13 | # will add to the boot time of your test suite on EVERY test run, even for an 14 | # individual file that may not need all of that loaded. Instead, make a 15 | # separate helper file that requires this one and then use it only in the specs 16 | # that actually need it. 17 | # 18 | # The `.rspec` file also contains a few flags that are not defaults but that 19 | # users commonly want. 20 | # 21 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 22 | RSpec.configure do |config| 23 | # The settings below are suggested to provide a good initial experience 24 | # with RSpec, but feel free to customize to your heart's content. 25 | 26 | # These two settings work together to allow you to limit a spec run 27 | # to individual examples or groups you care about by tagging them with 28 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 29 | # get run. 30 | config.filter_run :focus 31 | config.run_all_when_everything_filtered = true 32 | 33 | # Many RSpec users commonly either run the entire suite or an individual 34 | # file, and it's useful to allow more verbose output when running an 35 | # individual spec file. 36 | if config.files_to_run.one? 37 | # Use the documentation formatter for detailed output, 38 | # unless a formatter has already been configured 39 | # (e.g. via a command-line flag). 40 | config.default_formatter = 'doc' 41 | end 42 | 43 | # Print the 10 slowest examples and example groups at the 44 | # end of the spec run, to help surface which specs are running 45 | # particularly slow. 46 | # config.profile_examples = 10 47 | 48 | # Run specs in random order to surface order dependencies. If you find an 49 | # order dependency and want to debug it, you can fix the order by providing 50 | # the seed, which is printed after each run. 51 | # --seed 1234 52 | config.order = :random 53 | 54 | # Seed global randomization in this process using the `--seed` CLI option. 55 | # Setting this allows you to use `--seed` to deterministically reproduce 56 | # test failures related to randomization by passing the same `--seed` value 57 | # as the one that triggered the failure. 58 | Kernel.srand config.seed 59 | 60 | # rspec-expectations config goes here. You can use an alternate 61 | # assertion/expectation library such as wrong or the stdlib/minitest 62 | # assertions if you prefer. 63 | config.expect_with :rspec do |expectations| 64 | # Enable only the newer, non-monkey-patching expect syntax. 65 | # For more details, see: 66 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 67 | expectations.syntax = :expect 68 | end 69 | 70 | # rspec-mocks config goes here. You can use an alternate test double 71 | # library (such as bogus or mocha) by changing the `mock_with` option here. 72 | config.mock_with :rspec do |mocks| 73 | # Enable only the newer, non-monkey-patching expect syntax. 74 | # For more details, see: 75 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 76 | mocks.syntax = :expect 77 | 78 | # Prevents you from mocking or stubbing a method that does not exist on 79 | # a real object. This is generally recommended. 80 | mocks.verify_partial_doubles = true 81 | end 82 | 83 | config.after do 84 | clear_caches 85 | end 86 | end 87 | 88 | require 'rspec/support/differ' 89 | require 'linux_admin' 90 | 91 | def etc_issue_contains(contents) 92 | LinuxAdmin::EtcIssue.instance.send(:refresh) 93 | allow(File).to receive(:exist?).with('/etc/issue').at_least(:once).and_return(true) 94 | allow(File).to receive(:read).with('/etc/issue').at_least(:once).and_return(contents) 95 | end 96 | 97 | def stub_distro(distro = LinuxAdmin::Distros.rhel) 98 | # simply alias test distro to redhat distro for time being 99 | allow(LinuxAdmin::Distros).to receive_messages(:local => distro) 100 | end 101 | 102 | def data_file_path(to) 103 | File.expand_path(to, File.join(File.dirname(__FILE__), "data")) 104 | end 105 | 106 | def sample_output(to) 107 | File.read(data_file_path(to)) 108 | end 109 | 110 | def clear_caches 111 | LinuxAdmin::RegistrationSystem.instance_variable_set(:@registration_type, nil) 112 | LinuxAdmin::Service.instance_variable_set(:@service_type, nil) 113 | 114 | # reset the distro, tested in various placed & used extensively 115 | LinuxAdmin::Distros.instance_variable_set(:@local, nil) 116 | end 117 | -------------------------------------------------------------------------------- /spec/service/systemd_service_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::SystemdService do 2 | let(:command) { LinuxAdmin::Common.cmd(:systemctl) } 3 | 4 | before do 5 | @service = described_class.new 'foo' 6 | end 7 | 8 | describe "#running?" do 9 | it "checks service" do 10 | expect(LinuxAdmin::Common).to receive(:run) 11 | .with(command, :params => %w(status foo)).and_return(double(:success? => true)) 12 | @service.running? 13 | end 14 | 15 | it "returns true when service is running" do 16 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:success? => true)) 17 | expect(@service).to be_running 18 | end 19 | 20 | it "returns false when service is not running" do 21 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:success? => false)) 22 | expect(@service).not_to be_running 23 | end 24 | end 25 | 26 | describe "#enable" do 27 | it "enables service" do 28 | expect(LinuxAdmin::Common).to receive(:run!) .with(command, :params => %w(enable foo)) 29 | @service.enable 30 | end 31 | 32 | it "returns self" do 33 | expect(LinuxAdmin::Common).to receive(:run!) # stub out cmd invocation 34 | expect(@service.enable).to eq(@service) 35 | end 36 | end 37 | 38 | describe "#disable" do 39 | it "disables service" do 40 | expect(LinuxAdmin::Common).to receive(:run!).with(command, :params => %w(disable foo)) 41 | @service.disable 42 | end 43 | 44 | it "returns self" do 45 | expect(LinuxAdmin::Common).to receive(:run!) 46 | expect(@service.disable).to eq(@service) 47 | end 48 | end 49 | 50 | describe "#start" do 51 | it "starts service" do 52 | expect(LinuxAdmin::Common).to receive(:run!).with(command, :params => %w(start foo)) 53 | @service.start 54 | end 55 | 56 | it "enables the service if passed true" do 57 | expect(LinuxAdmin::Common).to receive(:run!).with(command, :params => %w(enable --now foo)) 58 | @service.start(true) 59 | end 60 | 61 | it "returns self" do 62 | expect(LinuxAdmin::Common).to receive(:run!) 63 | expect(@service.start).to eq(@service) 64 | end 65 | end 66 | 67 | describe "#stop" do 68 | it "stops service" do 69 | expect(LinuxAdmin::Common).to receive(:run!).with(command, :params => %w(stop foo)) 70 | @service.stop 71 | end 72 | 73 | it "returns self" do 74 | expect(LinuxAdmin::Common).to receive(:run!) 75 | expect(@service.stop).to eq(@service) 76 | end 77 | end 78 | 79 | describe "#restart" do 80 | it "restarts service" do 81 | expect(LinuxAdmin::Common).to receive(:run).with(command, :params => %w(restart foo)).and_return(double(:exit_status => 0)) 82 | @service.restart 83 | end 84 | 85 | it "manually stops then starts service when restart fails" do 86 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:exit_status => 1)) 87 | expect(@service).to receive(:stop) 88 | expect(@service).to receive(:start) 89 | @service.restart 90 | end 91 | 92 | it "returns self" do 93 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:exit_status => 0)) 94 | expect(@service.restart).to eq(@service) 95 | end 96 | end 97 | 98 | describe "#reload" do 99 | it "reloads service" do 100 | expect(LinuxAdmin::Common).to receive(:run!).with(command, :params => %w(reload foo)) 101 | expect(@service.reload).to eq(@service) 102 | end 103 | end 104 | 105 | describe "#status" do 106 | it "returns the service status" do 107 | status = "service status here" 108 | expect(LinuxAdmin::Common).to receive(:run) 109 | .with(command, :params => %w(status foo)).and_return(double(:output => status)) 110 | expect(@service.status).to eq(status) 111 | end 112 | end 113 | 114 | describe "#show" do 115 | it "returns a hash of runtime information" do 116 | output = <<-EOS 117 | MainPID=29189 118 | ExecMainStartTimestamp=Wed 2017-02-08 13:49:57 EST 119 | ExecStart={ path=/bin/sh ; argv[]=/bin/sh -c /bin/evmserver.sh start ; status=0/0 } 120 | ExecStop={ path=/bin/sh ; argv[]=/bin/sh -c /bin/evmserver.sh stop ; status=0/0 } 121 | ControlGroup=/system.slice/evmserverd.service 122 | MemoryCurrent=2865373184 123 | EOS 124 | 125 | hash = { 126 | "MainPID" => 29_189, 127 | "ExecMainStartTimestamp" => Time.new(2017, 2, 8, 13, 49, 57, "-05:00"), 128 | "ExecStart" => {"path" => "/bin/sh", "argv[]" => "/bin/sh -c /bin/evmserver.sh start", "status" => "0/0"}, 129 | "ExecStop" => {"path" => "/bin/sh", "argv[]" => "/bin/sh -c /bin/evmserver.sh stop", "status" => "0/0"}, 130 | "ControlGroup" => "/system.slice/evmserverd.service", 131 | "MemoryCurrent" => 2_865_373_184 132 | } 133 | expect(LinuxAdmin::Common).to receive(:run!) 134 | .with(command, :params => %w(show foo)).and_return(double(:output => output)) 135 | expect(@service.show).to eq(hash) 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /lib/linux_admin/registration_system/subscription_manager.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | 3 | module LinuxAdmin 4 | class SubscriptionManager < RegistrationSystem 5 | def run!(cmd, options = {}) 6 | Common.run!(cmd, options) 7 | rescue AwesomeSpawn::CommandResultError => err 8 | raise CredentialError.new(err.result) if err.result.error.downcase.include?("invalid username or password") 9 | raise SubscriptionManagerError.new(err.message, err.result) 10 | end 11 | 12 | SATELLITE6_SERVER_CERT_PATH = "pub/katello-ca-consumer-latest.noarch.rpm" 13 | 14 | def validate_credentials(options) 15 | !!organizations(options) 16 | end 17 | 18 | def registered?(options = nil) 19 | args = ["subscription-manager identity"] 20 | args << {:params => proxy_params(options)} if options 21 | Common.run(*args).exit_status.zero? 22 | end 23 | 24 | def refresh 25 | run!("subscription-manager refresh") 26 | end 27 | 28 | def organizations(options) 29 | raise ArgumentError, "username and password are required" unless options[:username] && options[:password] 30 | 31 | install_server_certificate(options[:server_url], SATELLITE6_SERVER_CERT_PATH) if options[:server_url] 32 | 33 | cmd = "subscription-manager orgs" 34 | 35 | params = {"--username=" => options[:username], "--password=" => options[:password]} 36 | params.merge!(proxy_params(options)) 37 | 38 | result = run!(cmd, :params => params) 39 | parse_output(result.output).each_with_object({}) { |i, h| h[i[:name]] = i } 40 | end 41 | 42 | def register(options) 43 | raise ArgumentError, "username and password are required" unless options[:username] && options[:password] 44 | 45 | install_server_certificate(options[:server_url], SATELLITE6_SERVER_CERT_PATH) if options[:server_url] 46 | 47 | cmd = "subscription-manager register" 48 | 49 | params = {"--username=" => options[:username], "--password=" => options[:password]} 50 | params.merge!(proxy_params(options)) 51 | params["--environment="] = options[:environment] if options[:environment] 52 | params["--org="] = options[:org] if options[:server_url] && options[:org] 53 | 54 | run!(cmd, :params => params) 55 | end 56 | 57 | def subscribe(options) 58 | cmd = "subscription-manager attach" 59 | params = proxy_params(options) 60 | 61 | if options[:pools].blank? 62 | params.merge!({"--auto" => nil}) 63 | else 64 | pools = options[:pools].collect {|pool| ["--pool", pool]} 65 | params = params.to_a + pools 66 | end 67 | 68 | run!(cmd, :params => params) 69 | end 70 | 71 | def subscribed_products 72 | cmd = "subscription-manager list --installed" 73 | output = run!(cmd).output 74 | 75 | parse_output(output).select {|p| p[:status].downcase == "subscribed"}.collect {|p| p[:product_id]} 76 | end 77 | 78 | def available_subscriptions 79 | cmd = "subscription-manager list --all --available" 80 | output = run!(cmd).output 81 | parse_output(output).each_with_object({}) { |i, h| h[i[:pool_id]] = i } 82 | end 83 | 84 | def enable_repo(repo, options = nil) 85 | cmd = "subscription-manager repos" 86 | params = {"--enable=" => repo} 87 | 88 | logger.info("#{self.class.name}##{__method__} Enabling repository: #{repo}") 89 | run!(cmd, :params => params) 90 | end 91 | 92 | def disable_repo(repo, options = nil) 93 | cmd = "subscription-manager repos" 94 | params = {"--disable=" => repo} 95 | 96 | run!(cmd, :params => params) 97 | end 98 | 99 | def all_repos(options = nil) 100 | cmd = "subscription-manager repos" 101 | output = run!(cmd).output 102 | 103 | parse_output(output) 104 | end 105 | 106 | def enabled_repos 107 | all_repos.select { |i| i[:enabled] }.collect { |r| r[:repo_id] } 108 | end 109 | 110 | private 111 | 112 | def parse_output(output) 113 | # Strip the 3 line header off the top 114 | content = output.split("\n")[3..-1].join("\n") 115 | parse_content(content) 116 | end 117 | 118 | def parse_content(content) 119 | # Break into content groupings by "\n\n" then process each grouping 120 | content.split("\n\n").each_with_object([]) do |group, group_array| 121 | group = group.split("\n").each_with_object({}) do |line, hash| 122 | next if line.blank? 123 | key, value = line.split(":", 2) 124 | hash[key.strip.downcase.tr(" -", "_").to_sym] = value.strip unless value.blank? 125 | end 126 | group_array.push(format_values(group)) 127 | end 128 | end 129 | 130 | def format_values(content_group) 131 | content_group[:enabled] = content_group[:enabled].to_i == 1 if content_group[:enabled] 132 | content_group[:ends] = Date.strptime(content_group[:ends], "%m/%d/%Y") if content_group[:ends] 133 | content_group[:starts] = Date.strptime(content_group[:starts], "%m/%d/%Y") if content_group[:starts] 134 | content_group 135 | end 136 | 137 | def proxy_params(options) 138 | config = {} 139 | config["--proxy="] = options[:proxy_address] if options[:proxy_address] 140 | config["--proxyuser="] = options[:proxy_username] if options[:proxy_username] 141 | config["--proxypassword="] = options[:proxy_password] if options[:proxy_password] 142 | config 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /spec/ip_address_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::IpAddress do 2 | let(:ip) { described_class.new } 3 | 4 | ADDR_SPAWN_ARGS = [ 5 | LinuxAdmin::Common.cmd("hostname"), 6 | :params => ["-I"] 7 | ] 8 | 9 | MAC_SPAWN_ARGS = [ 10 | LinuxAdmin::Common.cmd("ip"), 11 | :params => %w(addr show eth0) 12 | ] 13 | 14 | MASK_SPAWN_ARGS = [ 15 | LinuxAdmin::Common.cmd("ifconfig"), 16 | :params => %w(eth0) 17 | ] 18 | 19 | GW_SPAWN_ARGS = [ 20 | LinuxAdmin::Common.cmd("ip"), 21 | :params => %w(route) 22 | ] 23 | 24 | IP_ADDR_SHOW_ETH0 = <<-IP_OUT 25 | 2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 26 | link/ether 00:0c:29:ed:0e:8b brd ff:ff:ff:ff:ff:ff 27 | inet 192.168.1.9/24 brd 192.168.1.255 scope global dynamic eth0 28 | valid_lft 1297sec preferred_lft 1297sec 29 | inet6 fe80::20c:29ff:feed:e8b/64 scope link 30 | valid_lft forever preferred_lft forever 31 | 32 | IP_OUT 33 | 34 | IFCFG = <<-IP_OUT 35 | eth0: flags=4163 mtu 1500 36 | inet 192.168.1.9 netmask 255.255.255.0 broadcast 192.168.1.255 37 | inet6 fe80::20c:29ff:feed:e8b prefixlen 64 scopeid 0x20 38 | ether 00:0c:29:ed:0e:8b txqueuelen 1000 (Ethernet) 39 | RX packets 10171 bytes 8163955 (7.7 MiB) 40 | RX errors 0 dropped 0 overruns 0 frame 0 41 | TX packets 2871 bytes 321915 (314.3 KiB) 42 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 43 | 44 | IP_OUT 45 | 46 | IP_ROUTE = <<-IP_OUT 47 | default via 192.168.1.1 dev eth0 proto static metric 100 48 | 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.9 metric 100 49 | IP_OUT 50 | 51 | def result(output, exit_status) 52 | AwesomeSpawn::CommandResult.new("", output, "", 55, exit_status) 53 | end 54 | 55 | describe "#address" do 56 | it "returns an address" do 57 | ip_addr = "192.168.1.2" 58 | expect(AwesomeSpawn).to receive(:run).with(*ADDR_SPAWN_ARGS).and_return(result(ip_addr, 0)) 59 | expect(ip.address).to eq(ip_addr) 60 | end 61 | 62 | it "returns nil when no address is found" do 63 | ip_addr = "" 64 | expect(AwesomeSpawn).to receive(:run).at_least(5).times.with(*ADDR_SPAWN_ARGS).and_return(result(ip_addr, 1)) 65 | expect(ip.address).to be_nil 66 | end 67 | 68 | it "returns only IPv4 addresses" do 69 | ip_addr = "fd12:3456:789a:1::1 192.168.1.2" 70 | expect(AwesomeSpawn).to receive(:run).with(*ADDR_SPAWN_ARGS).and_return(result(ip_addr, 0)) 71 | expect(ip.address).to eq("192.168.1.2") 72 | end 73 | end 74 | 75 | describe "#address6" do 76 | it "returns an address" do 77 | ip_addr = "fd12:3456:789a:1::1" 78 | expect(AwesomeSpawn).to receive(:run).with(*ADDR_SPAWN_ARGS).and_return(result(ip_addr, 0)) 79 | expect(ip.address6).to eq(ip_addr) 80 | end 81 | 82 | it "returns nil when no address is found" do 83 | ip_addr = "" 84 | expect(AwesomeSpawn).to receive(:run).at_least(5).times.with(*ADDR_SPAWN_ARGS).and_return(result(ip_addr, 1)) 85 | expect(ip.address6).to be_nil 86 | end 87 | 88 | it "returns only IPv6 addresses" do 89 | ip_addr = "192.168.1.2 fd12:3456:789a:1::1" 90 | expect(AwesomeSpawn).to receive(:run).with(*ADDR_SPAWN_ARGS).and_return(result(ip_addr, 0)) 91 | expect(ip.address6).to eq("fd12:3456:789a:1::1") 92 | end 93 | end 94 | 95 | describe "#mac_address" do 96 | it "returns the correct MAC address" do 97 | expect(AwesomeSpawn).to receive(:run).with(*MAC_SPAWN_ARGS).and_return(result(IP_ADDR_SHOW_ETH0, 0)) 98 | expect(ip.mac_address("eth0")).to eq("00:0c:29:ed:0e:8b") 99 | end 100 | 101 | it "returns nil when the command fails" do 102 | expect(AwesomeSpawn).to receive(:run).with(*MAC_SPAWN_ARGS).and_return(result("", 1)) 103 | expect(ip.mac_address("eth0")).to be_nil 104 | end 105 | 106 | it "returns nil if the link/ether line is not present" do 107 | bad_output = IP_ADDR_SHOW_ETH0.gsub(%r{link/ether}, "") 108 | expect(AwesomeSpawn).to receive(:run).with(*MAC_SPAWN_ARGS).and_return(result(bad_output, 0)) 109 | expect(ip.mac_address("eth0")).to be_nil 110 | end 111 | end 112 | 113 | describe "#netmask" do 114 | it "returns the correct netmask" do 115 | expect(AwesomeSpawn).to receive(:run).with(*MASK_SPAWN_ARGS).and_return(result(IFCFG, 0)) 116 | expect(ip.netmask("eth0")).to eq("255.255.255.0") 117 | end 118 | 119 | it "returns nil when the command fails" do 120 | expect(AwesomeSpawn).to receive(:run).with(*MASK_SPAWN_ARGS).and_return(result("", 1)) 121 | expect(ip.netmask("eth0")).to be_nil 122 | end 123 | 124 | it "returns nil if the netmask line is not present" do 125 | bad_output = IFCFG.gsub(/netmask/, "") 126 | expect(AwesomeSpawn).to receive(:run).with(*MASK_SPAWN_ARGS).and_return(result(bad_output, 0)) 127 | expect(ip.netmask("eth0")).to be_nil 128 | end 129 | end 130 | 131 | describe "#gateway" do 132 | it "returns the correct gateway address" do 133 | expect(AwesomeSpawn).to receive(:run).with(*GW_SPAWN_ARGS).and_return(result(IP_ROUTE, 0)) 134 | expect(ip.gateway).to eq("192.168.1.1") 135 | end 136 | 137 | it "returns nil when the command fails" do 138 | expect(AwesomeSpawn).to receive(:run).with(*GW_SPAWN_ARGS).and_return(result("", 1)) 139 | expect(ip.gateway).to be_nil 140 | end 141 | 142 | it "returns nil if the default line is not present" do 143 | bad_output = IP_ROUTE.gsub(/default/, "") 144 | expect(AwesomeSpawn).to receive(:run).with(*GW_SPAWN_ARGS).and_return(result(bad_output, 0)) 145 | expect(ip.gateway).to be_nil 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /spec/fstab_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::FSTab do 2 | subject { described_class.dup } 3 | 4 | it "has newline, single spaces, tab" do 5 | fstab = < '/dev/sda1', 53 | :mount_point => '/', 54 | :fs_type => 'ext4', 55 | :mount_options => 'defaults', 56 | :dumpable => 1, 57 | :fsck_order => 1, 58 | :comment => "# more" 59 | ) 60 | 61 | expect(File).to receive(:write).with('/etc/fstab', "/dev/sda1 / ext4 defaults 1 1 # more\n") 62 | 63 | subject.instance.write! 64 | end 65 | end 66 | 67 | describe "#entries" do 68 | it "#<< updates maximum_column_lengths" do 69 | expect(File).to receive(:read).with("/etc/fstab").and_return("") 70 | 71 | subject.instance.entries << LinuxAdmin::FSTabEntry.new( 72 | :device => '/dev/sda1', 73 | :mount_point => '/', 74 | :fs_type => 'ext4', 75 | :mount_options => 'defaults', 76 | :dumpable => 1, 77 | :fsck_order => 1, 78 | :comment => "# more" 79 | ) 80 | 81 | expect(subject.instance.entries.maximum_column_lengths).to eq([9, 1, 4, 8, 1, 1, 6]) 82 | end 83 | end 84 | 85 | describe "integration test" do 86 | it "input equals output, just alignment changed" do 87 | original_fstab = <<~END_OF_FSTAB 88 | 89 | # 90 | # /etc/fstab 91 | # Created by anaconda on Wed May 29 12:37:40 2019 92 | # 93 | # Accessible filesystems, by reference, are maintained under '/dev/disk' 94 | # See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info 95 | # 96 | /dev/mapper/VG--MIQ-lv_os / xfs defaults 0 0 97 | UUID=02bf07b5-2404-4779-b93c-d8eb7f2eedea /boot xfs defaults 0 0 98 | /dev/mapper/VG--MIQ-lv_home /home xfs defaults 0 0 99 | /dev/mapper/VG--MIQ-lv_tmp /tmp xfs defaults 0 0 100 | /dev/mapper/VG--MIQ-lv_var /var xfs defaults 0 0 101 | /dev/mapper/VG--MIQ-lv_var_log /var/log xfs defaults 0 0 102 | /dev/mapper/VG--MIQ-lv_var_log_audit /var/log/audit xfs defaults 0 0 103 | /dev/mapper/VG--MIQ-lv_log /var/www/miq/vmdb/log xfs defaults 0 0 104 | /dev/mapper/VG--MIQ-lv_swap swap swap defaults 0 0 105 | END_OF_FSTAB 106 | 107 | new_fstab = <<~END_OF_FSTAB 108 | 109 | # /etc/fstab 110 | # Created by anaconda on Wed May 29 12:37:40 2019 111 | 112 | # Accessible filesystems, by reference, are maintained under '/dev/disk' 113 | # See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info 114 | 115 | /dev/mapper/VG--MIQ-lv_os / xfs defaults 0 0 116 | UUID=02bf07b5-2404-4779-b93c-d8eb7f2eedea /boot xfs defaults 0 0 117 | /dev/mapper/VG--MIQ-lv_home /home xfs defaults 0 0 118 | /dev/mapper/VG--MIQ-lv_tmp /tmp xfs defaults 0 0 119 | /dev/mapper/VG--MIQ-lv_var /var xfs defaults 0 0 120 | /dev/mapper/VG--MIQ-lv_var_log /var/log xfs defaults 0 0 121 | /dev/mapper/VG--MIQ-lv_var_log_audit /var/log/audit xfs defaults 0 0 122 | /dev/mapper/VG--MIQ-lv_log /var/www/miq/vmdb/log xfs defaults 0 0 123 | /dev/mapper/VG--MIQ-lv_swap swap swap defaults 0 0 124 | END_OF_FSTAB 125 | 126 | expect(File).to receive(:read).with("/etc/fstab").and_return(original_fstab) 127 | expect(File).to receive(:write).with("/etc/fstab", new_fstab) 128 | 129 | subject.instance.write! 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /spec/mountable_spec.rb: -------------------------------------------------------------------------------- 1 | class TestMountable 2 | include LinuxAdmin::Mountable 3 | 4 | def path 5 | "/dev/foo" 6 | end 7 | end 8 | 9 | describe LinuxAdmin::Mountable do 10 | before(:each) do 11 | @mountable = TestMountable.new 12 | 13 | # stub out calls that modify system 14 | allow(FileUtils).to receive(:mkdir) 15 | allow(LinuxAdmin::Common).to receive(:run!) 16 | 17 | @mount_out1 = < "")) 36 | TestMountable.mount_point_exists?('/mnt/usb') 37 | end 38 | 39 | context "disk mounted at specified location" do 40 | before do 41 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @mount_out1)) 42 | end 43 | 44 | it "returns true" do 45 | expect(TestMountable.mount_point_exists?('/mnt/usb')).to be_truthy 46 | end 47 | 48 | it "returns true when using a pathname" do 49 | path = Pathname.new("/mnt/usb") 50 | expect(TestMountable.mount_point_exists?(path)).to be_truthy 51 | end 52 | end 53 | 54 | context "no disk mounted at specified location" do 55 | before do 56 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @mount_out2)) 57 | end 58 | 59 | it "returns false" do 60 | expect(TestMountable.mount_point_exists?('/mnt/usb')).to be_falsey 61 | end 62 | 63 | it "returns false when using a pathname" do 64 | path = Pathname.new("/mnt/usb") 65 | expect(TestMountable.mount_point_exists?(path)).to be_falsey 66 | end 67 | end 68 | end 69 | 70 | describe "#mount_point_available?" do 71 | it "uses mount" do 72 | expect(LinuxAdmin::Common).to receive(:run!).with(LinuxAdmin::Common.cmd(:mount)) 73 | .and_return(double(:output => "")) 74 | TestMountable.mount_point_available?('/mnt/usb') 75 | end 76 | 77 | context "disk mounted at specified location" do 78 | before do 79 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @mount_out1)) 80 | end 81 | 82 | it "returns false" do 83 | expect(TestMountable.mount_point_available?('/mnt/usb')).to be_falsey 84 | end 85 | 86 | it "returns false when using a pathname" do 87 | path = Pathname.new("/mnt/usb") 88 | expect(TestMountable.mount_point_available?(path)).to be_falsey 89 | end 90 | end 91 | 92 | context "no disk mounted at specified location" do 93 | before do 94 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @mount_out2)) 95 | end 96 | 97 | it "returns true" do 98 | expect(TestMountable.mount_point_available?('/mnt/usb')).to be_truthy 99 | end 100 | 101 | it "returns true when using a pathname" do 102 | path = Pathname.new("/mnt/usb") 103 | expect(TestMountable.mount_point_available?(path)).to be_truthy 104 | end 105 | end 106 | end 107 | 108 | describe "#discover_mount_point" do 109 | it "sets the correct mountpoint when the path is mounted" do 110 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @mount_out3)) 111 | @mountable.discover_mount_point 112 | expect(@mountable.mount_point).to eq("/tmp") 113 | end 114 | 115 | it "sets mount_point to nil when the path is not mounted" do 116 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @mount_out1)) 117 | @mountable.discover_mount_point 118 | expect(@mountable.mount_point).to be_nil 119 | end 120 | end 121 | 122 | describe "#format_to" do 123 | it "uses mke2fs" do 124 | expect(LinuxAdmin::Common).to receive(:run!) 125 | .with(LinuxAdmin::Common.cmd(:mke2fs), 126 | :params => {'-t' => 'ext4', nil => '/dev/foo'}) 127 | @mountable.format_to('ext4') 128 | end 129 | 130 | it "sets fs type" do 131 | expect(LinuxAdmin::Common).to receive(:run!) # ignore actual formatting cmd 132 | @mountable.format_to('ext4') 133 | expect(@mountable.fs_type).to eq('ext4') 134 | end 135 | end 136 | 137 | describe "#mount" do 138 | it "sets mount point" do 139 | # ignore actual mount cmds 140 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => "")) 141 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => "")) 142 | 143 | expect(@mountable.mount('/mnt/sda2')).to eq('/mnt/sda2') 144 | expect(@mountable.mount_point).to eq('/mnt/sda2') 145 | end 146 | 147 | context "mountpoint does not exist" do 148 | it "creates mountpoint" do 149 | expect(TestMountable).to receive(:mount_point_exists?).and_return(false) 150 | expect(File).to receive(:directory?).with('/mnt/sda2').and_return(false) 151 | expect(FileUtils).to receive(:mkdir).with('/mnt/sda2') 152 | expect(LinuxAdmin::Common).to receive(:run!) # ignore actual mount cmd 153 | @mountable.mount '/mnt/sda2' 154 | end 155 | end 156 | 157 | context "disk mounted at mountpoint" do 158 | it "raises argument error" do 159 | expect(TestMountable).to receive(:mount_point_exists?).and_return(true) 160 | expect(File).to receive(:directory?).with('/mnt/sda2').and_return(true) 161 | expect { @mountable.mount '/mnt/sda2' }.to raise_error(ArgumentError, "disk already mounted at /mnt/sda2") 162 | end 163 | end 164 | 165 | it "mounts partition" do 166 | expect(TestMountable).to receive(:mount_point_exists?).and_return(false) 167 | expect(LinuxAdmin::Common).to receive(:run!) 168 | .with(LinuxAdmin::Common.cmd(:mount), 169 | :params => {nil => ['/dev/foo', '/mnt/sda2']}) 170 | @mountable.mount '/mnt/sda2' 171 | end 172 | end 173 | 174 | describe "#umount" do 175 | it "unmounts partition" do 176 | @mountable.mount_point = '/mnt/sda2' 177 | expect(LinuxAdmin::Common).to receive(:run!).with(LinuxAdmin::Common.cmd(:umount), 178 | :params => {nil => ['/mnt/sda2']}) 179 | @mountable.umount 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /lib/linux_admin/disk.rb: -------------------------------------------------------------------------------- 1 | require 'linux_admin/partition' 2 | 3 | module LinuxAdmin 4 | class Disk 5 | PARTED_FIELDS = 6 | [:id, :start_sector, :end_sector, 7 | :size, :partition_type, :fs_type] 8 | 9 | attr_accessor :path, :model 10 | 11 | # Collect local disk information via the lsblk command. Only disks with a 12 | # size greater than zero are returned. 13 | # 14 | def self.local 15 | result = Common.run!(Common.cmd("lsblk"), :params => {:b => nil, :d => nil, :n => nil, :p => nil, :o => "NAME,SIZE,TYPE,MODEL"}) 16 | result.output.split("\n").collect do |string| 17 | path, size, type, *model = string.split 18 | if type.casecmp?('disk') && size.to_i > 0 19 | self.new(:path => path, :size => size.to_i, :model => model.join(' ')) 20 | end 21 | end.compact 22 | end 23 | 24 | def initialize(args = {}) 25 | @path = args[:path] 26 | @model = args[:model] || "unknown" 27 | @size = args[:size] 28 | end 29 | 30 | def size 31 | @size ||= begin 32 | size = nil 33 | out = Common.run!(Common.cmd(:fdisk), :params => {"-l" => nil}).output 34 | out.each_line do |l| 35 | /Disk #{path}: .*B, (\d+) bytes/.match(l) do |m| 36 | size = m[1].to_i 37 | break 38 | end 39 | end 40 | size 41 | end 42 | end 43 | 44 | def partitions 45 | @partitions ||= 46 | parted_output.collect { |disk| 47 | partition_from_parted(disk) 48 | } 49 | end 50 | 51 | def create_partition_table(type = "msdos") 52 | Common.run!(Common.cmd(:parted), :params => {nil => parted_options_array("mklabel", type)}) 53 | end 54 | 55 | def has_partition_table? 56 | result = Common.run(Common.cmd(:parted), :params => {nil => parted_options_array("print")}) 57 | 58 | result_indicates_partition_table?(result) 59 | end 60 | 61 | def create_partition(partition_type, *args) 62 | create_partition_table unless has_partition_table? 63 | 64 | start = finish = size = nil 65 | case args.length 66 | when 1 then 67 | start = partitions.empty? ? 0 : partitions.last.end_sector 68 | size = args.first 69 | finish = start + size 70 | 71 | when 2 then 72 | start = args[0] 73 | finish = args[1] 74 | 75 | else 76 | raise ArgumentError, "must specify start/finish or size" 77 | end 78 | 79 | id = partitions.empty? ? 1 : (partitions.last.id + 1) 80 | options = parted_options_array('mkpart', '-a', 'opt', partition_type, start, finish) 81 | Common.run!(Common.cmd(:parted), :params => {nil => options}) 82 | 83 | partition = Partition.new(:disk => self, 84 | :id => id, 85 | :start_sector => start, 86 | :end_sector => finish, 87 | :size => size, 88 | :partition_type => partition_type) 89 | partitions << partition 90 | partition 91 | end 92 | 93 | def create_partitions(partition_type, *args) 94 | check_if_partitions_overlap(args) 95 | 96 | args.each { |arg| 97 | self.create_partition(partition_type, arg[:start], arg[:end]) 98 | } 99 | end 100 | 101 | def clear! 102 | @partitions = [] 103 | 104 | # clear partition table 105 | Common.run!(Common.cmd(:dd), 106 | :params => { 'if=' => '/dev/zero', 'of=' => @path, 107 | 'bs=' => 512, 'count=' => 1}) 108 | 109 | self 110 | end 111 | 112 | def partition_path(id) 113 | case model 114 | when "nvme" 115 | "#{path}p#{id}" 116 | else 117 | "#{path}#{id}" 118 | end 119 | end 120 | 121 | private 122 | 123 | def str_to_bytes(val, unit) 124 | case unit 125 | when 'K', 'k' then 126 | val.to_f * 1_024 # 1.kilobytes 127 | when 'M' then 128 | val.to_f * 1_048_576 # 1.megabyte 129 | when 'G' then 130 | val.to_f * 1_073_741_824 # 1.gigabytes 131 | end 132 | end 133 | 134 | def overlapping_ranges?(ranges) 135 | ranges.find do |range1| 136 | ranges.any? do |range2| 137 | range1 != range2 && 138 | ranges_overlap?(range1, range2) 139 | end 140 | end 141 | end 142 | 143 | def ranges_overlap?(range1, range2) # copied from activesupport Range#overlaps? 144 | range1.cover?(range2.first) || range2.cover?(range1.first) 145 | end 146 | 147 | def check_if_partitions_overlap(partitions) 148 | ranges = 149 | partitions.collect do |partition| 150 | start = partition[:start] 151 | finish = partition[:end] 152 | start.delete('%') 153 | finish.delete('%') 154 | start.to_f..finish.to_f 155 | end 156 | 157 | if overlapping_ranges?(ranges) 158 | raise ArgumentError, "overlapping partitions" 159 | end 160 | end 161 | 162 | def parted_output 163 | # TODO: Should this really catch non-zero RC, set output to the default "" and silently return [] ? 164 | # If so, should other calls to parted also do the same? 165 | # requires sudo 166 | out = Common.run(Common.cmd(:parted), 167 | :params => { nil => parted_options_array('print') }).output 168 | split = [] 169 | out.each_line do |l| 170 | if l =~ /^ [0-9].*/ 171 | split << l.split 172 | elsif l =~ /^Model:.*/ 173 | parse_model(l) 174 | end 175 | end 176 | split 177 | end 178 | 179 | def parse_model(parted_line) 180 | matches = parted_line.match(/^Model:.*\((?\w+)\)$/) 181 | @model = matches[:model] if matches 182 | end 183 | 184 | def partition_from_parted(output_disk) 185 | args = {:disk => self} 186 | PARTED_FIELDS.each_index do |i| 187 | val = output_disk[i] 188 | case PARTED_FIELDS[i] 189 | when :start_sector, :end_sector, :size 190 | if val =~ /([0-9\.]*)([kKMG])B/ 191 | val = str_to_bytes($1, $2) 192 | end 193 | 194 | when :id 195 | val = val.to_i 196 | 197 | end 198 | args[PARTED_FIELDS[i]] = val 199 | end 200 | 201 | Partition.new(args) 202 | end 203 | 204 | def parted_options_array(*args) 205 | args = args.first if args.first.kind_of?(Array) 206 | parted_default_options + args 207 | end 208 | 209 | def parted_default_options 210 | @parted_default_options ||= ['--script', path].freeze 211 | end 212 | 213 | def result_indicates_partition_table?(result) 214 | # parted exits with 1 but writes this oddly spelled error to stdout. 215 | missing = (result.exit_status == 1 && result.output.include?("unrecognised disk label")) 216 | !missing 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /spec/logical_volume_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::LogicalVolume do 2 | before(:each) do 3 | @logical_volumes = < 'vg' 23 | lv = described_class.new :name => 'lv', :volume_group => vg 24 | expect(LinuxAdmin::Common).to receive(:run!) 25 | .with(LinuxAdmin::Common.cmd(:lvextend), 26 | :params => %w(lv vg)) 27 | lv.extend_with(vg) 28 | end 29 | 30 | it "returns self" do 31 | vg = LinuxAdmin::VolumeGroup.new :name => 'vg' 32 | lv = described_class.new :name => 'lv', :volume_group => vg 33 | allow(LinuxAdmin::Common).to receive(:run!) 34 | expect(lv.extend_with(vg)).to eq(lv) 35 | end 36 | end 37 | 38 | describe "#path" do 39 | it "returns /dev/vgname/lvname" do 40 | vg = LinuxAdmin::VolumeGroup.new :name => 'vg' 41 | lv = described_class.new :name => 'lv', :volume_group => vg 42 | expect(lv.path).to eq('/dev/vg/lv') 43 | end 44 | end 45 | 46 | describe "#create" do 47 | before(:each) do 48 | @vg = LinuxAdmin::VolumeGroup.new :name => 'vg' 49 | end 50 | 51 | it "uses lvcreate" do 52 | described_class.instance_variable_set(:@lvs, []) 53 | expect(LinuxAdmin::Common).to receive(:run!) 54 | .with(LinuxAdmin::Common.cmd(:lvcreate), 55 | :params => {'-n' => 'lv', 56 | nil => 'vg', 57 | '-L' => '256G'}) 58 | described_class.create 'lv', @vg, 274_877_906_944 # 256.gigabytes 59 | end 60 | 61 | context "size is specified" do 62 | it "passes -L option to lvcreate" do 63 | described_class.instance_variable_set(:@lvs, []) 64 | expect(LinuxAdmin::Common).to receive(:run!) 65 | .with(LinuxAdmin::Common.cmd(:lvcreate), 66 | :params => {'-n' => 'lv', 67 | nil => 'vg', 68 | '-L' => '256G'}) 69 | described_class.create 'lv', @vg, 274_877_906_944 # 256.gigabytes 70 | end 71 | end 72 | 73 | context "extents is specified" do 74 | it "passes -l option to lvcreate" do 75 | described_class.instance_variable_set(:@lvs, []) 76 | expect(LinuxAdmin::Common).to receive(:run!) 77 | .with(LinuxAdmin::Common.cmd(:lvcreate), 78 | :params => {'-n' => 'lv', 79 | nil => 'vg', 80 | '-l' => '100%FREE'}) 81 | described_class.create 'lv', @vg, 100 82 | end 83 | end 84 | 85 | it "returns new logical volume" do 86 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 87 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 88 | lv = described_class.create 'lv', @vg, 274_877_906_944 # 256.gigabytes 89 | expect(lv).to be_an_instance_of(described_class) 90 | expect(lv.name).to eq('lv') 91 | end 92 | 93 | context "name is specified" do 94 | it "sets path under volume group" do 95 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 96 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 97 | lv = described_class.create 'lv', @vg, 274_877_906_944 # 256.gigabytes 98 | expect(lv.path.to_s).to eq("#{described_class::DEVICE_PATH}#{@vg.name}/lv") 99 | end 100 | end 101 | 102 | context "path is specified" do 103 | it "sets name" do 104 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 105 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 106 | lv = described_class.create '/dev/lv', @vg, 274_877_906_944 # 256.gigabytes 107 | expect(lv.name).to eq("lv") 108 | end 109 | end 110 | 111 | context "path is specified as Pathname" do 112 | it "sets name" do 113 | require 'pathname' 114 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 115 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 116 | lv = described_class.create Pathname.new("/dev/#{@vg.name}/lv"), @vg, 274_877_906_944 # 256.gigabytes 117 | expect(lv.name).to eq("lv") 118 | expect(lv.path).to eq("/dev/vg/lv") 119 | end 120 | end 121 | 122 | it "adds logical volume to local registry" do 123 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 124 | allow(LinuxAdmin::Common).to receive_messages(:run! => double(:output => "")) 125 | lv = described_class.create 'lv', @vg, 274_877_906_944 # 256.gigabytes 126 | expect(described_class.scan).to include(lv) 127 | end 128 | end 129 | 130 | describe "#scan" do 131 | it "uses lvdisplay" do 132 | expect(LinuxAdmin::Common).to receive(:run!) 133 | .with(LinuxAdmin::Common.cmd(:lvdisplay), 134 | :params => {'-c' => nil}) 135 | .and_return(double(:output => @logical_volumes)) 136 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @groups)) # stub out call to vgdisplay 137 | described_class.scan 138 | end 139 | 140 | it "returns local logical volumes" do 141 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @logical_volumes)) 142 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @groups)) 143 | lvs = described_class.scan 144 | 145 | expect(lvs[0]).to be_an_instance_of(described_class) 146 | expect(lvs[0].path).to eq('/dev/vg_foobar/lv_swap') 147 | expect(lvs[0].name).to eq('lv_swap') 148 | expect(lvs[0].sectors).to eq(4128768) 149 | 150 | expect(lvs[1]).to be_an_instance_of(described_class) 151 | expect(lvs[1].path).to eq('/dev/vg_foobar/lv_root') 152 | expect(lvs[1].name).to eq('lv_root') 153 | expect(lvs[1].sectors).to eq(19988480) 154 | end 155 | 156 | it "resolves volume group references" do 157 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @logical_volumes)) 158 | expect(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => @groups)) 159 | lvs = described_class.scan 160 | expect(lvs[0].volume_group).to be_an_instance_of(LinuxAdmin::VolumeGroup) 161 | expect(lvs[0].volume_group.name).to eq('vg_foobar') 162 | expect(lvs[1].volume_group).to be_an_instance_of(LinuxAdmin::VolumeGroup) 163 | expect(lvs[1].volume_group.name).to eq('vg_foobar') 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /spec/hosts_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Hosts do 2 | TEST_HOSTNAME = "test-hostname" 3 | etc_hosts = "\n #Some Comment\n127.0.0.1\tlocalhost localhost.localdomain # with a comment\n127.0.1.1 my.domain.local" 4 | before do 5 | allow(File).to receive(:read).and_return(etc_hosts) 6 | @instance = LinuxAdmin::Hosts.new 7 | end 8 | 9 | describe "#reload" do 10 | it "sets raw_lines" do 11 | expected_array = ["", " #Some Comment", "127.0.0.1\tlocalhost localhost.localdomain # with a comment", "127.0.1.1 my.domain.local"] 12 | expect(@instance.raw_lines).to eq(expected_array) 13 | end 14 | 15 | it "sets parsed_file" do 16 | expected_hash = [{:blank=>true}, {:comment=>"Some Comment"}, {:address=>"127.0.0.1", :hosts=>["localhost", "localhost.localdomain"], :comment=>"with a comment"}, {:address=>"127.0.1.1", :hosts=>["my.domain.local"]}] 17 | expect(@instance.parsed_file).to eq(expected_hash) 18 | end 19 | end 20 | 21 | describe "#update_entry" do 22 | it "removes an existing entry and creates a new one" do 23 | expected_hash = [{:blank=>true}, {:comment=>"Some Comment"}, {:address=>"127.0.0.1", :hosts=>["localhost", "localhost.localdomain"], :comment=>"with a comment"}, {:address=>"127.0.1.1", :hosts=>[]}, {:address=>"1.2.3.4", :hosts=>["my.domain.local"], :comment=>nil}] 24 | @instance.update_entry("1.2.3.4", "my.domain.local") 25 | expect(@instance.parsed_file).to eq(expected_hash) 26 | end 27 | 28 | it "updates an existing entry" do 29 | expected_hash = [{:blank=>true}, {:comment=>"Some Comment"}, {:address=>"127.0.0.1", :hosts=>["localhost", "localhost.localdomain", "new.domain.local"], :comment=>"with a comment"}, {:address=>"127.0.1.1", :hosts=>["my.domain.local"]}] 30 | @instance.update_entry("127.0.0.1", "new.domain.local") 31 | expect(@instance.parsed_file).to eq(expected_hash) 32 | end 33 | end 34 | 35 | describe "#set_loopback_hostname" do 36 | etc_hosts_v6_loopback = <<-EOT 37 | 38 | #Some Comment 39 | ::1\tlocalhost localhost.localdomain # with a comment 40 | 127.0.0.1\tlocalhost localhost.localdomain # with a comment 41 | 127.0.1.1 my.domain.local 42 | EOT 43 | 44 | before do 45 | allow(File).to receive(:read).and_return(etc_hosts_v6_loopback) 46 | @instance_v6_loopback = LinuxAdmin::Hosts.new 47 | end 48 | 49 | it "adds the hostname to the start of the hosts list for the loopback addresses" do 50 | expected_hash = [{:blank => true}, 51 | {:comment => "Some Comment"}, 52 | {:address => "::1", 53 | :hosts => ["examplehost.example.com", "localhost", "localhost.localdomain"], 54 | :comment => "with a comment"}, 55 | {:address => "127.0.0.1", 56 | :hosts => ["examplehost.example.com", "localhost", "localhost.localdomain"], 57 | :comment => "with a comment"}, 58 | {:address => "127.0.1.1", :hosts => ["my.domain.local"]}] 59 | @instance_v6_loopback.set_loopback_hostname("examplehost.example.com") 60 | expect(@instance_v6_loopback.parsed_file).to eq(expected_hash) 61 | end 62 | end 63 | 64 | describe "#set_canonical_hostname" do 65 | it "removes an existing entry and creates a new one" do 66 | expected_hash = [{:blank => true}, 67 | {:comment => "Some Comment"}, 68 | {:address => "127.0.0.1", :hosts => ["localhost", "localhost.localdomain"], :comment => "with a comment"}, 69 | {:address => "127.0.1.1", :hosts => []}, 70 | {:address => "1.2.3.4", :hosts => ["my.domain.local"], :comment => nil}] 71 | @instance.set_canonical_hostname("1.2.3.4", "my.domain.local") 72 | expect(@instance.parsed_file).to eq(expected_hash) 73 | end 74 | 75 | it "adds the hostname to the start of the hosts list" do 76 | expected_hash = [{:blank => true}, 77 | {:comment => "Some Comment"}, 78 | {:address => "127.0.0.1", :hosts => ["examplehost.example.com", "localhost", "localhost.localdomain"], :comment => "with a comment"}, 79 | {:address => "127.0.1.1", :hosts => ["my.domain.local"]}] 80 | @instance.set_canonical_hostname("127.0.0.1", "examplehost.example.com") 81 | expect(@instance.parsed_file).to eq(expected_hash) 82 | end 83 | end 84 | 85 | describe "#save" do 86 | it "properly generates file with new content" do 87 | allow(File).to receive(:write) 88 | expected_array = ["", "#Some Comment", "127.0.0.1 localhost localhost.localdomain #with a comment", "127.0.1.1 my.domain.local", "1.2.3.4 test"] 89 | @instance.update_entry("1.2.3.4", "test") 90 | @instance.save 91 | expect(@instance.raw_lines).to eq(expected_array) 92 | end 93 | 94 | it "properly generates file with removed content" do 95 | allow(File).to receive(:write) 96 | expected_array = ["", "#Some Comment", "127.0.0.1 localhost localhost.localdomain my.domain.local #with a comment"] 97 | @instance.update_entry("127.0.0.1", "my.domain.local") 98 | @instance.save 99 | expect(@instance.raw_lines).to eq(expected_array) 100 | end 101 | 102 | it "ends the file with a new line" do 103 | expect(File).to receive(:write) do |_file, contents| 104 | expect(contents).to end_with("\n") 105 | end 106 | @instance.save 107 | end 108 | end 109 | 110 | describe "#hostname=" do 111 | it "sets the hostname using hostnamectl when the command exists" do 112 | spawn_args = [ 113 | LinuxAdmin::Common.cmd('hostnamectl'), 114 | :params => ['set-hostname', TEST_HOSTNAME] 115 | ] 116 | expect(LinuxAdmin::Common).to receive(:cmd?).with("hostnamectl").and_return(true) 117 | expect(AwesomeSpawn).to receive(:run!).with(*spawn_args) 118 | @instance.hostname = TEST_HOSTNAME 119 | end 120 | 121 | it "sets the hostname with hostname when hostnamectl does not exist" do 122 | spawn_args = [ 123 | LinuxAdmin::Common.cmd('hostname'), 124 | :params => {:file => "/etc/hostname"} 125 | ] 126 | expect(LinuxAdmin::Common).to receive(:cmd?).with("hostnamectl").and_return(false) 127 | expect(File).to receive(:write).with("/etc/hostname", TEST_HOSTNAME) 128 | expect(AwesomeSpawn).to receive(:run!).with(*spawn_args) 129 | @instance.hostname = TEST_HOSTNAME 130 | end 131 | end 132 | 133 | describe "#hostname" do 134 | let(:spawn_args) do 135 | [LinuxAdmin::Common.cmd('hostname'), {}] 136 | end 137 | 138 | it "returns the hostname" do 139 | result = AwesomeSpawn::CommandResult.new("", TEST_HOSTNAME, "", 55, 0) 140 | expect(AwesomeSpawn).to receive(:run).with(*spawn_args).and_return(result) 141 | expect(@instance.hostname).to eq(TEST_HOSTNAME) 142 | end 143 | 144 | it "returns nil when the command fails" do 145 | result = AwesomeSpawn::CommandResult.new("", "", "An error has happened", 55, 1) 146 | expect(AwesomeSpawn).to receive(:run).with(*spawn_args).and_return(result) 147 | expect(@instance.hostname).to be_nil 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /spec/yum_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Yum do 2 | before(:each) do 3 | allow(FileUtils).to receive_messages(:mkdir_p => true) 4 | end 5 | 6 | context ".create_repo" do 7 | it "default arguments" do 8 | expect(LinuxAdmin::Common).to receive(:run!).once 9 | .with("createrepo", :params => {nil => "some/path", "--database" => nil, "--unique-md-filenames" => nil}) 10 | described_class.create_repo("some/path") 11 | end 12 | 13 | it "bare create" do 14 | expect(LinuxAdmin::Common).to receive(:run!).once.with("createrepo", :params => {nil => "some/path"}) 15 | described_class.create_repo("some/path", :database => false, :unique_file_names => false) 16 | end 17 | end 18 | 19 | context ".download_packages" do 20 | it "with valid input" do 21 | expect(LinuxAdmin::Common).to receive(:run!).once 22 | .with("repotrack", :params => {"-p" => "some/path", nil => "pkg_a pkg_b"}) 23 | described_class.download_packages("some/path", "pkg_a pkg_b") 24 | end 25 | 26 | it "without mirror type" do 27 | expect { described_class.download_packages("some/path", "pkg_a pkg_b", :mirror_type => nil) }.to raise_error(ArgumentError) 28 | end 29 | end 30 | 31 | it ".repo_settings" do 32 | expect(described_class).to receive(:parse_repo_dir).once.with("/etc/yum.repos.d").and_return(true) 33 | expect(described_class.repo_settings).to be_truthy 34 | end 35 | 36 | it ".parse_repo_dir" do 37 | expect(described_class.parse_repo_dir(data_file_path("yum"))).to eq({ 38 | File.join(data_file_path("yum"), "first.repo") => 39 | { "my-local-repo-a" => 40 | { "name" =>"My Local Repo A", 41 | "baseurl" =>"https://mirror.example.com/a/content/os_ver", 42 | "enabled" =>0, 43 | "gpgcheck" =>1, 44 | "gpgkey" =>"file:///etc/pki/rpm-gpg/RPM-GPG-KEY-my-local-server", 45 | "sslverify" =>1, 46 | "sslcacert" =>"/etc/rhsm/ca/my-loacl-server.pem", 47 | "sslclientkey" =>"/etc/pki/entitlement/0123456789012345678-key.pem", 48 | "sslclientcert" =>"/etc/pki/entitlement/0123456789012345678.pem", 49 | "metadata_expire" =>86400}, 50 | "my-local-repo-b" => 51 | { "name" =>"My Local Repo B", 52 | "baseurl" =>"https://mirror.example.com/b/content/os_ver", 53 | "enabled" =>1, 54 | "gpgcheck" =>0, 55 | "sslverify" =>0, 56 | "metadata_expire" =>86400}}, 57 | File.join(data_file_path("yum"), "second.repo") => 58 | { "my-local-repo-c" => 59 | { "name" =>"My Local Repo c", 60 | "baseurl" =>"https://mirror.example.com/c/content/os_ver", 61 | "enabled" =>0, 62 | "cost" =>100, 63 | "gpgcheck" =>1, 64 | "gpgkey" =>"file:///etc/pki/rpm-gpg/RPM-GPG-KEY-my-local-server", 65 | "sslverify" =>0, 66 | "metadata_expire" =>1}},}) 67 | end 68 | 69 | context ".updates_available?" do 70 | it "check updates for a specific package" do 71 | expect(LinuxAdmin::Common).to receive(:run).once.with("yum check-update", :params => {nil => ["abc"]}) 72 | .and_return(double(:exit_status => 100)) 73 | expect(described_class.updates_available?("abc")).to be_truthy 74 | end 75 | 76 | it "updates are available" do 77 | allow(LinuxAdmin::Common).to receive_messages(:run => double(:exit_status => 100)) 78 | expect(described_class.updates_available?).to be_truthy 79 | end 80 | 81 | it "updates not available" do 82 | allow(LinuxAdmin::Common).to receive_messages(:run => double(:exit_status => 0)) 83 | expect(described_class.updates_available?).to be_falsey 84 | end 85 | 86 | it "other exit code" do 87 | allow(LinuxAdmin::Common).to receive_messages(:run => double(:exit_status => 255, :error => 'test')) 88 | expect { described_class.updates_available? }.to raise_error(RuntimeError) 89 | end 90 | 91 | it "other error" do 92 | allow(LinuxAdmin::Common).to receive(:run).and_raise(RuntimeError) 93 | expect { described_class.updates_available? }.to raise_error(RuntimeError) 94 | end 95 | end 96 | 97 | context ".update" do 98 | it "no arguments" do 99 | expect(LinuxAdmin::Common).to receive(:run!).once.with("yum -y update", :params => nil) 100 | .and_return(AwesomeSpawn::CommandResult.new("", "", "", 55, 0)) 101 | described_class.update 102 | end 103 | 104 | it "with arguments" do 105 | expect(LinuxAdmin::Common).to receive(:run!).once.with("yum -y update", :params => {nil => ["1 2", "3"]}) 106 | .and_return(AwesomeSpawn::CommandResult.new("", "", "", 55, 0)) 107 | described_class.update("1 2", "3") 108 | end 109 | 110 | it "with bad arguments" do 111 | error = AwesomeSpawn::CommandResult.new("", "Loaded plugins: product-id\nNo Packages marked for Update\n", "Blah blah ...\nNo Match for argument: \n", 55, 0) 112 | expect(LinuxAdmin::Common).to receive(:run!).once 113 | .with("yum -y update", :params => {nil => [""]}).and_return(error) 114 | expect { described_class.update("") }.to raise_error(AwesomeSpawn::CommandResultError) 115 | end 116 | end 117 | 118 | context ".version_available" do 119 | it "no packages" do 120 | expect { described_class.version_available }.to raise_error(ArgumentError) 121 | end 122 | 123 | it "with one package" do 124 | expect(LinuxAdmin::Common).to receive(:run!).once 125 | .with("repoquery --qf=\"%{name} %{version}\"", :params => {nil => ["subscription-manager"]}) 126 | .and_return(double(:output => sample_output("yum/output_repoquery_single"))) 127 | expect(described_class.version_available("subscription-manager")).to eq({"subscription-manager" => "1.1.23.1"}) 128 | end 129 | 130 | it "with multiple packages" do 131 | expect(LinuxAdmin::Common).to receive(:run!).once 132 | .with("repoquery --qf=\"%{name} %{version}\"", :params => {nil => ["curl", "subscription-manager", "wget"]}) 133 | .and_return(double(:output => sample_output("yum/output_repoquery_multiple"))) 134 | expect(described_class.version_available("curl", "subscription-manager", "wget")).to eq({ 135 | "curl" => "7.19.7", 136 | "subscription-manager" => "1.1.23.1", 137 | "wget" => "1.12" 138 | }) 139 | end 140 | end 141 | 142 | context ".repo_list" do 143 | it "with no arguments" do 144 | expect(LinuxAdmin::Common).to receive(:run!).with("yum repolist", :params => {nil => "enabled"}) 145 | .and_return(double(:output => sample_output("yum/output_repo_list"))) 146 | expect(described_class.repo_list).to eq(["rhel-6-server-rpms", "rhel-ha-for-rhel-6-server-rpms", "rhel-lb-for-rhel-6-server-rpms"]) 147 | end 148 | 149 | it "with argument" do 150 | expect(LinuxAdmin::Common).to receive(:run!).with("yum repolist", :params => {nil => "enabled"}) 151 | .and_return(double(:output => sample_output("yum/output_repo_list"))) 152 | expect(described_class.repo_list("enabled")).to eq(["rhel-6-server-rpms", "rhel-ha-for-rhel-6-server-rpms", "rhel-lb-for-rhel-6-server-rpms"]) 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /lib/linux_admin/network_interface/network_interface_rh.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddr' 2 | require 'pathname' 3 | 4 | module LinuxAdmin 5 | class NetworkInterfaceRH < NetworkInterface 6 | IFACE_DIR = "/etc/sysconfig/network-scripts" 7 | 8 | # @return [Hash] Key value mappings in the interface file 9 | attr_reader :interface_config 10 | 11 | # @param interface [String] Name of the network interface to manage 12 | def initialize(interface) 13 | @interface_file = self.class.path_to_interface_config_file(interface) 14 | super 15 | end 16 | 17 | # Gathers current network information for this interface 18 | # 19 | # @return [Boolean] true if network information was gathered successfully 20 | def reload 21 | super 22 | parse_conf 23 | true 24 | end 25 | 26 | # Parses the interface configuration file into the @interface_config hash 27 | def parse_conf 28 | @interface_config = {} 29 | 30 | if @interface_file.file? 31 | File.foreach(@interface_file) do |line| 32 | next if line =~ /^\s*#/ 33 | 34 | key, value = line.split('=').collect(&:strip) 35 | @interface_config[key] = value 36 | end 37 | end 38 | 39 | @interface_config["NM_CONTROLLED"] = "no" 40 | end 41 | 42 | # Set the IPv4 address for this interface 43 | # 44 | # @param address [String] 45 | # @raise ArgumentError if the address is not formatted properly 46 | def address=(address) 47 | validate_ip(address) 48 | @interface_config["BOOTPROTO"] = "static" 49 | @interface_config["IPADDR"] = address 50 | end 51 | 52 | # Set the IPv6 address for this interface 53 | # 54 | # @param address [String] IPv6 address including the prefix length (i.e. '::1/127') 55 | # @raise ArgumentError if the address is not formatted properly 56 | def address6=(address) 57 | validate_ip(address) 58 | @interface_config['IPV6INIT'] = 'yes' 59 | @interface_config['DHCPV6C'] = 'no' 60 | @interface_config['IPV6ADDR'] = address 61 | end 62 | 63 | # Set the IPv4 gateway address for this interface 64 | # 65 | # @param address [String] 66 | # @raise ArgumentError if the address is not formatted properly 67 | def gateway=(address) 68 | validate_ip(address) 69 | @interface_config["GATEWAY"] = address 70 | end 71 | 72 | # Set the IPv6 gateway address for this interface 73 | # 74 | # @param address [String] IPv6 address optionally including the prefix length 75 | # @raise ArgumentError if the address is not formatted properly 76 | def gateway6=(address) 77 | validate_ip(address) 78 | @interface_config['IPV6_DEFAULTGW'] = address 79 | end 80 | 81 | # Set the IPv4 sub-net mask for this interface 82 | # 83 | # @param mask [String] 84 | # @raise ArgumentError if the mask is not formatted properly 85 | def netmask=(mask) 86 | validate_ip(mask) 87 | @interface_config["NETMASK"] = mask 88 | end 89 | 90 | # Sets one or both DNS servers for this network interface 91 | # 92 | # @param servers [Array] The DNS servers 93 | def dns=(*servers) 94 | server1, server2 = servers.flatten 95 | @interface_config["DNS1"] = server1 96 | @interface_config["DNS2"] = server2 if server2 97 | end 98 | 99 | # Sets the search domain list for this network interface 100 | # 101 | # @param domains [Array] the list of search domains 102 | def search_order=(*domains) 103 | @interface_config["DOMAIN"] = "\"#{domains.flatten.join(' ')}\"" 104 | end 105 | 106 | # Set up the interface to use DHCP 107 | # Removes any previously set static IPv4 networking information 108 | def enable_dhcp 109 | @interface_config["BOOTPROTO"] = "dhcp" 110 | @interface_config.delete("IPADDR") 111 | @interface_config.delete("NETMASK") 112 | @interface_config.delete("GATEWAY") 113 | @interface_config.delete("PREFIX") 114 | @interface_config.delete("DNS1") 115 | @interface_config.delete("DNS2") 116 | @interface_config.delete("DOMAIN") 117 | end 118 | 119 | # Set up the interface to use DHCPv6 120 | # Removes any previously set static IPv6 networking information 121 | def enable_dhcp6 122 | @interface_config['IPV6INIT'] = 'yes' 123 | @interface_config['DHCPV6C'] = 'yes' 124 | @interface_config.delete('IPV6ADDR') 125 | @interface_config.delete('IPV6_DEFAULTGW') 126 | @interface_config.delete("DNS1") 127 | @interface_config.delete("DNS2") 128 | @interface_config.delete("DOMAIN") 129 | end 130 | 131 | # Applies the given static network configuration to the interface 132 | # 133 | # @param ip [String] IPv4 address 134 | # @param mask [String] subnet mask 135 | # @param gw [String] gateway address 136 | # @param dns [Array] list of dns servers 137 | # @param search [Array] list of search domains 138 | # @return [Boolean] true on success, false otherwise 139 | # @raise ArgumentError if an IP is not formatted properly 140 | def apply_static(ip, mask, gw, dns, search = nil) 141 | self.address = ip 142 | self.netmask = mask 143 | self.gateway = gw 144 | self.dns = dns 145 | self.search_order = search if search 146 | save 147 | end 148 | 149 | # Applies the given static IPv6 network configuration to the interface 150 | # 151 | # @param ip [String] IPv6 address 152 | # @param prefix [Number] prefix length for IPv6 address 153 | # @param gw [String] gateway address 154 | # @param dns [Array] list of dns servers 155 | # @param search [Array] list of search domains 156 | # @return [Boolean] true on success, false otherwise 157 | # @raise ArgumentError if an IP is not formatted properly or interface does not start 158 | def apply_static6(ip, prefix, gw, dns, search = nil) 159 | self.address6 = "#{ip}/#{prefix}" 160 | self.gateway6 = gw 161 | self.dns = dns 162 | self.search_order = search if search 163 | save 164 | end 165 | 166 | # Writes the contents of @interface_config to @interface_file as `key`=`value` pairs 167 | # and resets the interface 168 | # 169 | # @return [Boolean] true if the interface was successfully brought up with the 170 | # new configuration, false otherwise 171 | def save 172 | old_contents = @interface_file.file? ? File.read(@interface_file) : "" 173 | 174 | stop_success = stop 175 | # Stop twice because when configure both ipv4 and ipv6 as dhcp, ipv6 dhcp client will 176 | # exit and leave a /var/run/dhclient6-eth0.pid file. Then stop (ifdown eth0) will try 177 | # to kill this exited process so it returns 1. In the second call, this `.pid' file 178 | # has been deleted and ifdown returns 0. 179 | # See: https://bugzilla.redhat.com/show_bug.cgi?id=1472396 180 | stop_success = stop unless stop_success 181 | return false unless stop_success 182 | 183 | File.write(@interface_file, @interface_config.delete_blanks.collect { |k, v| "#{k}=#{v}" }.join("\n")) 184 | 185 | unless start 186 | File.write(@interface_file, old_contents) 187 | start 188 | return false 189 | end 190 | 191 | reload 192 | end 193 | 194 | def self.path_to_interface_config_file(interface) 195 | Pathname.new(IFACE_DIR).join("ifcfg-#{interface}") 196 | end 197 | 198 | private 199 | 200 | # Validate that the given address is formatted correctly 201 | # 202 | # @param ip [String] 203 | # @raise ArgumentError if the address is not correctly formatted 204 | def validate_ip(ip) 205 | IPAddr.new(ip) 206 | rescue ArgumentError 207 | raise ArgumentError, "#{ip} is not a valid IPv4 or IPv6 address" 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /lib/linux_admin/network_interface.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class NetworkInterface 3 | require "ipaddr" 4 | require "json" 5 | 6 | # Cached class instance variable for what distro we are running on 7 | @dist_class = nil 8 | 9 | # Gets the subclass specific to the local Linux distro 10 | # 11 | # @param clear_cache [Boolean] Determines if the cached value will be reevaluated 12 | # @return [Class] The proper class to be used 13 | def self.dist_class(clear_cache = false) 14 | @dist_class = nil if clear_cache 15 | @dist_class ||= begin 16 | case Distros.local 17 | when Distros.rhel, Distros.fedora 18 | NetworkInterfaceRH 19 | when Distros.darwin 20 | NetworkInterfaceDarwin 21 | else 22 | NetworkInterfaceGeneric 23 | end 24 | end 25 | end 26 | 27 | def self.list 28 | ip_link.pluck("ifname").map { |iface| new(iface) } 29 | rescue AwesomeSpawn::CommandResultError => e 30 | raise NetworkInterfaceError.new(e.message, e.result) 31 | end 32 | 33 | private_class_method def self.ip_link 34 | require "json" 35 | 36 | result = Common.run!(Common.cmd("ip"), :params => ["--json", "link"]) 37 | JSON.parse(result.output) 38 | rescue AwesomeSpawn::CommandResultError, JSON::ParserError => e 39 | raise NetworkInterfaceError.new(e.message, e.result) 40 | end 41 | 42 | # Creates an instance of the correct NetworkInterface subclass for the local distro 43 | def self.new(*args) 44 | self == LinuxAdmin::NetworkInterface ? dist_class.new(*args) : super 45 | end 46 | 47 | # @return [String] the interface for networking operations 48 | attr_reader :interface, :link_type 49 | 50 | # @param interface [String] Name of the network interface to manage 51 | def initialize(interface) 52 | @interface = interface 53 | reload 54 | end 55 | 56 | # Gathers current network information for this interface 57 | # 58 | # @return [Boolean] true if network information was gathered successfully 59 | def reload 60 | @network_conf = {} 61 | begin 62 | ip_output = ip_show 63 | rescue NetworkInterfaceError 64 | return false 65 | end 66 | 67 | @link_type = ip_output["link_type"] 68 | addr_info = ip_output["addr_info"] 69 | 70 | parse_ip4(addr_info) 71 | parse_ip6(addr_info, "global") 72 | parse_ip6(addr_info, "link") 73 | 74 | @network_conf[:mac] = ip_output["address"] 75 | 76 | [4, 6].each do |version| 77 | @network_conf["gateway#{version}".to_sym] = ip_route(version, "default")&.dig("gateway") 78 | end 79 | true 80 | end 81 | 82 | def loopback? 83 | @link_type == "loopback" 84 | end 85 | 86 | # Retrieve the IPv4 address assigned to the interface 87 | # 88 | # @return [String] IPv4 address for the managed interface 89 | def address 90 | @network_conf[:address] 91 | end 92 | 93 | # Retrieve the IPv6 address assigned to the interface 94 | # 95 | # @return [String] IPv6 address for the managed interface 96 | # @raise [ArgumentError] if the given scope is not `:global` or `:link` 97 | def address6(scope = :global) 98 | case scope 99 | when :global 100 | @network_conf[:address6_global] 101 | when :link 102 | @network_conf[:address6_link] 103 | else 104 | raise ArgumentError, "Unrecognized address scope #{scope}" 105 | end 106 | end 107 | 108 | # Retrieve the MAC address associated with the interface 109 | # 110 | # @return [String] the MAC address 111 | def mac_address 112 | @network_conf[:mac] 113 | end 114 | 115 | # Retrieve the IPv4 sub-net mask assigned to the interface 116 | # 117 | # @return [String] IPv4 netmask 118 | def netmask 119 | @network_conf[:mask] ||= IPAddr.new('255.255.255.255').mask(prefix).to_s if prefix 120 | end 121 | 122 | # Retrieve the IPv6 sub-net mask assigned to the interface 123 | # 124 | # @return [String] IPv6 netmask 125 | # @raise [ArgumentError] if the given scope is not `:global` or `:link` 126 | def netmask6(scope = :global) 127 | if [:global, :link].include?(scope) 128 | @network_conf["mask6_#{scope}".to_sym] ||= IPAddr.new('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff').mask(prefix6(scope)).to_s if prefix6(scope) 129 | else 130 | raise ArgumentError, "Unrecognized address scope #{scope}" 131 | end 132 | end 133 | 134 | # Retrieve the IPv4 sub-net prefix length assigned to the interface 135 | # 136 | # @return [Numeric] IPv4 prefix length 137 | def prefix 138 | @network_conf[:prefix] 139 | end 140 | 141 | # Retrieve the IPv6 sub-net prefix length assigned to the interface 142 | # 143 | # @return [Numeric] IPv6 prefix length 144 | def prefix6(scope = :global) 145 | if [:global, :link].include?(scope) 146 | @network_conf["prefix6_#{scope}".to_sym] 147 | else 148 | raise ArgumentError, "Unrecognized address scope #{scope}" 149 | end 150 | end 151 | 152 | # Retrieve the IPv4 default gateway associated with the interface 153 | # 154 | # @return [String] IPv4 gateway address 155 | def gateway 156 | @network_conf[:gateway4] 157 | end 158 | 159 | # Retrieve the IPv6 default gateway associated with the interface 160 | # 161 | # @return [String] IPv6 gateway address 162 | def gateway6 163 | @network_conf[:gateway6] 164 | end 165 | 166 | # Brings up the network interface 167 | # 168 | # @return [Boolean] whether the command succeeded or not 169 | def start 170 | Common.run(Common.cmd("ifup"), :params => [@interface]).success? 171 | end 172 | 173 | # Brings down the network interface 174 | # 175 | # @return [Boolean] whether the command succeeded or not 176 | def stop 177 | Common.run(Common.cmd("ifdown"), :params => [@interface]).success? 178 | end 179 | 180 | private 181 | 182 | # Runs the command `ip addr show ` 183 | # 184 | # @return [String] The command output 185 | # @raise [NetworkInterfaceError] if the command fails 186 | def ip_show 187 | output = Common.run!(Common.cmd("ip"), :params => ["--json", "addr", "show", @interface]).output 188 | return {} if output.blank? 189 | 190 | JSON.parse(output).first 191 | rescue AwesomeSpawn::CommandResultError => e 192 | raise NetworkInterfaceError.new(e.message, e.result) 193 | end 194 | 195 | # Runs the command `ip -[4/6] route` and returns the output 196 | # 197 | # @param version [Fixnum] Version of IP protocol (4 or 6) 198 | # @return [String] The command output 199 | # @raise [NetworkInterfaceError] if the command fails 200 | def ip_route(version, route = "default") 201 | output = Common.run!(Common.cmd("ip"), :params => ["--json", "-#{version}", "route", "show", route]).output 202 | return {} if output.blank? 203 | 204 | JSON.parse(output).first 205 | rescue AwesomeSpawn::CommandResultError => e 206 | raise NetworkInterfaceError.new(e.message, e.result) 207 | end 208 | 209 | # Parses the IPv4 information from the output of `ip addr show ` 210 | # 211 | # @param ip_output [String] The command output 212 | def parse_ip4(addr_info) 213 | inet = addr_info&.detect { |addr| addr["family"] == "inet" } 214 | return if inet.nil? 215 | 216 | @network_conf[:address] = inet["local"] 217 | @network_conf[:prefix] = inet["prefixlen"] 218 | end 219 | 220 | # Parses the IPv6 information from the output of `ip addr show ` 221 | # 222 | # @param ip_output [String] The command output 223 | # @param scope [Symbol] The IPv6 scope (either `:global` or `:local`) 224 | def parse_ip6(addr_info, scope) 225 | inet6 = addr_info&.detect { |addr| addr["family"] == "inet6" && addr["scope"] == scope } 226 | return if inet6.nil? 227 | 228 | @network_conf["address6_#{scope}".to_sym] = inet6["local"] 229 | @network_conf["prefix6_#{scope}".to_sym] = inet6["prefixlen"] 230 | end 231 | end 232 | end 233 | 234 | Dir.glob(File.join(File.dirname(__FILE__), "network_interface", "*.rb")).each { |f| require f } 235 | -------------------------------------------------------------------------------- /spec/network_interface/network_interface_rh_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::NetworkInterfaceRH do 2 | let(:device_name) { "eth0" } 3 | let(:ifcfg_file_dhcp) do 4 | <<-EOF 5 | #A comment is here 6 | DEVICE=eth0 7 | BOOTPROTO=dhcp 8 | UUID=3a48a5b5-b80b-4712-82f7-e517e4088999 9 | ONBOOT=yes 10 | TYPE=Ethernet 11 | NAME="System eth0" 12 | EOF 13 | end 14 | 15 | let(:ifcfg_file_static) do 16 | <<-EOF 17 | #A comment is here 18 | DEVICE=eth0 19 | BOOTPROTO=static 20 | UUID=3a48a5b5-b80b-4712-82f7-e517e4088999 21 | ONBOOT=yes 22 | TYPE=Ethernet 23 | NAME="System eth0" 24 | IPADDR=192.168.1.100 25 | NETMASK=255.255.255.0 26 | GATEWAY=192.168.1.1 27 | EOF 28 | end 29 | 30 | def stub_foreach_to_string(string) 31 | allow(File).to receive(:foreach) do |&block| 32 | string.each_line { |l| block.call(l) } 33 | end 34 | end 35 | 36 | def result(output, exit_status) 37 | AwesomeSpawn::CommandResult.new("", output, "", 55, exit_status) 38 | end 39 | 40 | subject(:dhcp_interface) do 41 | allow(File).to receive(:exist?).and_return(true) 42 | stub_path = described_class.path_to_interface_config_file(device_name) 43 | allow(Pathname).to receive(:new).and_return(stub_path) 44 | allow(stub_path).to receive(:file?).and_return(true) 45 | stub_foreach_to_string(ifcfg_file_dhcp) 46 | allow(AwesomeSpawn).to receive(:run!).exactly(6).times.and_return(result("", 0)) 47 | described_class.new(device_name) 48 | end 49 | 50 | subject(:static_interface) do 51 | allow(File).to receive(:exist?).and_return(true) 52 | stub_foreach_to_string(ifcfg_file_static) 53 | allow(AwesomeSpawn).to receive(:run!).exactly(4).times.and_return(result("", 0)) 54 | described_class.new(device_name) 55 | end 56 | 57 | describe ".new" do 58 | it "loads the configuration" do 59 | conf = dhcp_interface.interface_config 60 | expect(conf["NM_CONTROLLED"]).to eq("no") 61 | expect(conf["DEVICE"]).to eq("eth0") 62 | expect(conf["BOOTPROTO"]).to eq("dhcp") 63 | expect(conf["UUID"]).to eq("3a48a5b5-b80b-4712-82f7-e517e4088999") 64 | expect(conf["ONBOOT"]).to eq("yes") 65 | expect(conf["TYPE"]).to eq("Ethernet") 66 | expect(conf["NAME"]).to eq('"System eth0"') 67 | end 68 | end 69 | 70 | describe "#parse_conf" do 71 | it "reloads the interface configuration" do 72 | interface = dhcp_interface 73 | stub_foreach_to_string(ifcfg_file_static) 74 | interface.parse_conf 75 | 76 | conf = interface.interface_config 77 | expect(conf["NM_CONTROLLED"]).to eq("no") 78 | expect(conf["DEVICE"]).to eq("eth0") 79 | expect(conf["BOOTPROTO"]).to eq("static") 80 | expect(conf["UUID"]).to eq("3a48a5b5-b80b-4712-82f7-e517e4088999") 81 | expect(conf["ONBOOT"]).to eq("yes") 82 | expect(conf["TYPE"]).to eq("Ethernet") 83 | expect(conf["NAME"]).to eq('"System eth0"') 84 | expect(conf["IPADDR"]).to eq("192.168.1.100") 85 | expect(conf["NETMASK"]).to eq("255.255.255.0") 86 | expect(conf["GATEWAY"]).to eq("192.168.1.1") 87 | end 88 | end 89 | 90 | describe "#address=" do 91 | it "sets the address" do 92 | address = "192.168.1.100" 93 | 94 | dhcp_interface.address = address 95 | 96 | conf = dhcp_interface.interface_config 97 | expect(conf["IPADDR"]).to eq(address) 98 | expect(conf["BOOTPROTO"]).to eq("static") 99 | end 100 | 101 | it "raises argument error when given a bad address" do 102 | expect { dhcp_interface.address = "garbage" }.to raise_error(ArgumentError) 103 | end 104 | end 105 | 106 | describe '#address6=' do 107 | it 'sets the ipv6 address' do 108 | address = 'fe80::1/64' 109 | dhcp_interface.address6 = address 110 | conf = dhcp_interface.interface_config 111 | expect(conf['IPV6ADDR']).to eq(address) 112 | expect(conf['IPV6INIT']).to eq('yes') 113 | expect(conf['DHCPV6C']).to eq('no') 114 | end 115 | 116 | it 'raises error when given a bad address' do 117 | expect { dhcp_interface.address6 = '1::1::1' }.to raise_error(ArgumentError) 118 | end 119 | end 120 | 121 | describe "#gateway=" do 122 | it "sets the gateway address" do 123 | address = "192.168.1.1" 124 | dhcp_interface.gateway = address 125 | expect(dhcp_interface.interface_config["GATEWAY"]).to eq(address) 126 | end 127 | 128 | it "raises argument error when given a bad address" do 129 | expect { dhcp_interface.gateway = "garbage" }.to raise_error(ArgumentError) 130 | end 131 | end 132 | 133 | describe '#gateway6=' do 134 | it 'sets the default gateway for IPv6' do 135 | address = 'fe80::1/64' 136 | dhcp_interface.gateway6 = address 137 | expect(dhcp_interface.interface_config['IPV6_DEFAULTGW']).to eq(address) 138 | end 139 | end 140 | 141 | describe "#netmask=" do 142 | it "sets the sub-net mask" do 143 | mask = "255.255.255.0" 144 | dhcp_interface.netmask = mask 145 | expect(dhcp_interface.interface_config["NETMASK"]).to eq(mask) 146 | end 147 | 148 | it "raises argument error when given a bad address" do 149 | expect { dhcp_interface.netmask = "garbage" }.to raise_error(ArgumentError) 150 | end 151 | end 152 | 153 | describe "#dns=" do 154 | it "sets the correct configuration" do 155 | dns1 = "192.168.1.1" 156 | dns2 = "192.168.1.10" 157 | 158 | static_interface.dns = dns1, dns2 159 | 160 | conf = static_interface.interface_config 161 | expect(conf["DNS1"]).to eq(dns1) 162 | expect(conf["DNS2"]).to eq(dns2) 163 | end 164 | 165 | it "sets the correct configuration when given an array" do 166 | dns = %w(192.168.1.1 192.168.1.10) 167 | 168 | static_interface.dns = dns 169 | 170 | conf = static_interface.interface_config 171 | expect(conf["DNS1"]).to eq(dns[0]) 172 | expect(conf["DNS2"]).to eq(dns[1]) 173 | end 174 | 175 | it "sets only DNS1 if given one value" do 176 | dns = "192.168.1.1" 177 | 178 | static_interface.dns = dns 179 | 180 | conf = static_interface.interface_config 181 | expect(conf["DNS1"]).to eq(dns) 182 | expect(conf["DNS2"]).to be_nil 183 | end 184 | end 185 | 186 | describe "#search_order=" do 187 | it "sets the search domain list" do 188 | search1 = "localhost" 189 | search2 = "test.example.com" 190 | search3 = "example.com" 191 | static_interface.search_order = search1, search2, search3 192 | expect(static_interface.interface_config["DOMAIN"]).to eq("\"#{search1} #{search2} #{search3}\"") 193 | end 194 | 195 | it "sets the search domain list when given an array" do 196 | search_list = %w(localhost test.example.com example.com) 197 | static_interface.search_order = search_list 198 | expect(static_interface.interface_config["DOMAIN"]).to eq("\"#{search_list.join(' ')}\"") 199 | end 200 | end 201 | 202 | describe "#enable_dhcp" do 203 | it "sets the correct configuration" do 204 | static_interface.enable_dhcp 205 | conf = static_interface.interface_config 206 | expect(conf["BOOTPROTO"]).to eq("dhcp") 207 | expect(conf["IPADDR"]).to be_nil 208 | expect(conf["NETMASK"]).to be_nil 209 | expect(conf["GATEWAY"]).to be_nil 210 | expect(conf["PREFIX"]).to be_nil 211 | end 212 | end 213 | 214 | describe '#enable_dhcp6' do 215 | it 'sets the correct configuration' do 216 | [static_interface, dhcp_interface].each do |interface| 217 | interface.enable_dhcp6 218 | conf = interface.interface_config 219 | expect(conf).to include('IPV6INIT' => 'yes', 'DHCPV6C' => 'yes') 220 | expect(conf.keys).not_to include('IPV6ADDR', 'IPV6_DEFAULTGW') 221 | end 222 | end 223 | end 224 | 225 | describe "#apply_static" do 226 | it "sets the correct configuration" do 227 | expect(dhcp_interface).to receive(:save) 228 | dhcp_interface.apply_static("192.168.1.12", "255.255.255.0", "192.168.1.1", ["192.168.1.1", nil], ["localhost"]) 229 | 230 | conf = dhcp_interface.interface_config 231 | expect(conf["BOOTPROTO"]).to eq("static") 232 | expect(conf["IPADDR"]).to eq("192.168.1.12") 233 | expect(conf["NETMASK"]).to eq("255.255.255.0") 234 | expect(conf["GATEWAY"]).to eq("192.168.1.1") 235 | expect(conf["DNS1"]).to eq("192.168.1.1") 236 | expect(conf["DNS2"]).to be_nil 237 | expect(conf["DOMAIN"]).to eq("\"localhost\"") 238 | end 239 | end 240 | 241 | describe '#apply_static6' do 242 | it 'sets the static IPv6 configuration' do 243 | expect(dhcp_interface).to receive(:save) 244 | dhcp_interface.apply_static6('d:e:a:d:b:e:e:f', 127, 'd:e:a:d::/64', ['d:e:a:d::']) 245 | conf = dhcp_interface.interface_config 246 | expect(conf).to include('IPV6INIT' => 'yes', 'DHCPV6C' => 'no', 'IPV6ADDR' => 'd:e:a:d:b:e:e:f/127', 'IPV6_DEFAULTGW' => 'd:e:a:d::/64') 247 | end 248 | end 249 | 250 | describe "#save" do 251 | let(:iface_file) { Pathname.new("/etc/sysconfig/network-scripts/ifcfg-#{device_name}") } 252 | 253 | def expect_old_contents 254 | expect(File).to receive(:write) do |file, contents| 255 | expect(file).to eq(iface_file) 256 | expect(contents).to include("DEVICE=eth0") 257 | expect(contents).to include("BOOTPROTO=dhcp") 258 | expect(contents).to include("UUID=3a48a5b5-b80b-4712-82f7-e517e4088999") 259 | expect(contents).to include("ONBOOT=yes") 260 | expect(contents).to include("TYPE=Ethernet") 261 | expect(contents).to include('NAME="System eth0"') 262 | end 263 | end 264 | 265 | it "writes the configuration" do 266 | expect(File).to receive(:read).with(iface_file) 267 | expect(dhcp_interface).to receive(:stop).and_return(true) 268 | expect(dhcp_interface).to receive(:start).and_return(true) 269 | expect_old_contents 270 | expect(dhcp_interface.save).to be true 271 | end 272 | 273 | it "returns false when the interface cannot be brought down" do 274 | expect(File).to receive(:read).with(iface_file) 275 | expect(dhcp_interface).to receive(:stop).twice.and_return(false) 276 | expect(File).not_to receive(:write) 277 | expect(dhcp_interface.save).to be false 278 | end 279 | 280 | it "returns false and writes the old contents when the interface fails to come back up" do 281 | dhcp_interface # evaluate the subject first so the expectations stub the right calls 282 | expect(File).to receive(:read).with(iface_file).and_return("old stuff") 283 | expect(dhcp_interface).to receive(:stop).and_return(true) 284 | expect_old_contents 285 | expect(dhcp_interface).to receive(:start).and_return(false) 286 | expect(File).to receive(:write).with(iface_file, "old stuff") 287 | expect(dhcp_interface).to receive(:start) 288 | expect(dhcp_interface.save).to be false 289 | end 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /spec/subscription_manager_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::SubscriptionManager do 2 | context "#run!" do 3 | it "raises a CredentialError if the error message contained a credential error" do 4 | result = AwesomeSpawn::CommandResult.new("stuff", "things", "invalid username or password", 55, 1) 5 | err = AwesomeSpawn::CommandResultError.new("things", result) 6 | expect(LinuxAdmin::Common).to receive(:run!).and_raise(err) 7 | 8 | expect { subject.run!("stuff") }.to raise_error(LinuxAdmin::CredentialError) 9 | end 10 | 11 | it "raises a SubscriptionManagerError if the error message does not contain a credential error" do 12 | result = AwesomeSpawn::CommandResult.new("stuff", "things", "not a credential error", 55, 1) 13 | err = AwesomeSpawn::CommandResultError.new("things", result) 14 | expect(LinuxAdmin::Common).to receive(:run!).and_raise(err) 15 | 16 | expect { subject.run!("stuff") }.to raise_error(LinuxAdmin::SubscriptionManagerError) 17 | end 18 | end 19 | 20 | context "#registered?" do 21 | it "system with subscription-manager commands" do 22 | expect(LinuxAdmin::Common).to( 23 | receive(:run).once.with("subscription-manager identity") 24 | .and_return(double(:exit_status => 0)) 25 | ) 26 | expect(described_class.new.registered?).to be_truthy 27 | end 28 | 29 | it "system without subscription-manager commands" do 30 | expect(LinuxAdmin::Common).to( 31 | receive(:run).once.with("subscription-manager identity").and_return(double(:exit_status => 255)) 32 | ) 33 | expect(described_class.new.registered?).to be_falsey 34 | end 35 | end 36 | 37 | it "#refresh" do 38 | expect(LinuxAdmin::Common).to receive(:run!).once.with("subscription-manager refresh", {}) 39 | described_class.new.refresh 40 | end 41 | 42 | context "#register" do 43 | it "no username" do 44 | expect { described_class.new.register }.to raise_error(ArgumentError) 45 | end 46 | 47 | context "with username and password" do 48 | let(:base_options) do 49 | { 50 | :username => "SomeUser@SomeDomain.org", 51 | :password => "SomePass", 52 | :environment => "Library", 53 | :org => "IT", 54 | :proxy_address => "1.2.3.4", 55 | :proxy_username => "ProxyUser", 56 | :proxy_password => "ProxyPass", 57 | } 58 | end 59 | let(:run_params) do 60 | { 61 | :params => { 62 | "--username=" => "SomeUser@SomeDomain.org", 63 | "--password=" => "SomePass", 64 | "--proxy=" => "1.2.3.4", 65 | "--proxyuser=" => "ProxyUser", 66 | "--proxypassword=" => "ProxyPass", 67 | "--environment=" => "Library" 68 | } 69 | } 70 | end 71 | 72 | it "with server_url" do 73 | run_params.store_path(:params, "--org=", "IT") 74 | base_options.store_path(:server_url, "https://server.url") 75 | 76 | expect(LinuxAdmin::Common).to receive(:run!).once.with("subscription-manager register", run_params) 77 | expect(LinuxAdmin::Rpm).to receive(:upgrade).with("http://server.url/pub/katello-ca-consumer-latest.noarch.rpm") 78 | 79 | described_class.new.register(base_options) 80 | end 81 | 82 | it "without server_url" do 83 | expect(LinuxAdmin::Common).to receive(:run!).once.with("subscription-manager register", run_params) 84 | expect_any_instance_of(described_class).not_to receive(:install_server_certificate) 85 | 86 | described_class.new.register(base_options) 87 | end 88 | end 89 | end 90 | 91 | context "#subscribe" do 92 | it "with pools" do 93 | expect(LinuxAdmin::Common).to( 94 | receive(:run!).once.with("subscription-manager attach", {:params => [["--pool", 123], ["--pool", 456]]}) 95 | ) 96 | described_class.new.subscribe({:pools => [123, 456]}) 97 | end 98 | 99 | it "without pools" do 100 | expect(LinuxAdmin::Common).to receive(:run!).once.with("subscription-manager attach", {:params => {"--auto" => nil}}) 101 | described_class.new.subscribe({}) 102 | end 103 | end 104 | 105 | context "#subscribed_products" do 106 | it "subscribed" do 107 | expect(LinuxAdmin::Common).to( 108 | receive(:run!).once.with("subscription-manager list --installed", {}) 109 | .and_return(double(:output => sample_output("subscription_manager/output_list_installed_subscribed"))) 110 | ) 111 | expect(described_class.new.subscribed_products).to eq(["69", "167"]) 112 | end 113 | 114 | it "not subscribed" do 115 | expect(LinuxAdmin::Common).to( 116 | receive(:run!).once.with("subscription-manager list --installed", {}) 117 | .and_return(double(:output => sample_output("subscription_manager/output_list_installed_not_subscribed"))) 118 | ) 119 | expect(described_class.new.subscribed_products).to eq(["167"]) 120 | end 121 | end 122 | 123 | it "#available_subscriptions" do 124 | expect(LinuxAdmin::Common).to( 125 | receive(:run!).once.with("subscription-manager list --all --available", {}) 126 | .and_return(double(:output => sample_output("subscription_manager/output_list_all_available"))) 127 | ) 128 | expect(described_class.new.available_subscriptions).to eq( 129 | { 130 | "82c042fca983889b10178893f29b06e3" => { 131 | :subscription_name => "Example Subscription", 132 | :sku => "SER0123", 133 | :pool_id => "82c042fca983889b10178893f29b06e3", 134 | :quantity => "1690", 135 | :service_level => "None", 136 | :service_type => "None", 137 | :multi_entitlement => "No", 138 | :ends => Date.parse("2022-01-01"), 139 | :system_type => "Physical", 140 | }, 141 | "4f738052ec866192c775c62f408ab868" => { 142 | :subscription_name => "My Private Subscription", 143 | :sku => "SER9876", 144 | :pool_id => "4f738052ec866192c775c62f408ab868", 145 | :quantity => "Unlimited", 146 | :service_level => "None", 147 | :service_type => "None", 148 | :multi_entitlement => "No", 149 | :ends => Date.parse("2013-06-04"), 150 | :system_type => "Virtual", 151 | }, 152 | "3d81297f352305b9a3521981029d7d83" => { 153 | :subscription_name => "Shared Subscription - With other characters, (2 sockets) (Up to 1 guest)", 154 | :sku => "RH0123456", 155 | :pool_id => "3d81297f352305b9a3521981029d7d83", 156 | :quantity => "1", 157 | :service_level => "Self-support", 158 | :service_type => "L1-L3", 159 | :multi_entitlement => "No", 160 | :ends => Date.parse("2013-05-15"), 161 | :system_type => "Virtual", 162 | }, 163 | "87cefe63b67984d5c7e401d833d2f87f" => { 164 | :subscription_name => "Example Subscription, Premium (up to 2 sockets) 3 year", 165 | :sku => "MCT0123A9", 166 | :pool_id => "87cefe63b67984d5c7e401d833d2f87f", 167 | :quantity => "1", 168 | :service_level => "Premium", 169 | :service_type => "L1-L3", 170 | :multi_entitlement => "No", 171 | :ends => Date.parse("2013-07-05"), 172 | :system_type => "Virtual", 173 | }, 174 | } 175 | ) 176 | end 177 | 178 | context "#organizations" do 179 | it "with valid credentials" do 180 | run_options = ["subscription-manager orgs", { 181 | :params => { 182 | "--username=" => "SomeUser", 183 | "--password=" => "SomePass", 184 | "--proxy=" => "1.2.3.4", 185 | "--proxyuser=" => "ProxyUser", 186 | "--proxypassword=" => "ProxyPass" 187 | } 188 | }] 189 | 190 | expect(LinuxAdmin::Rpm).to receive(:upgrade).with("http://192.168.1.1/pub/katello-ca-consumer-latest.noarch.rpm") 191 | expect(LinuxAdmin::Common).to( 192 | receive(:run!).once.with(*run_options).and_return(double(:output => sample_output("subscription_manager/output_orgs"))) 193 | ) 194 | 195 | manager = described_class.new.organizations( 196 | :username => "SomeUser", 197 | :password => "SomePass", 198 | :proxy_address => "1.2.3.4", 199 | :proxy_username => "ProxyUser", 200 | :proxy_password => "ProxyPass", 201 | :server_url => "192.168.1.1" 202 | ) 203 | expect(manager).to eq({"SomeOrg" => {:name => "SomeOrg", :key => "1234567"}}) 204 | end 205 | 206 | it "with invalid credentials" do 207 | run_options = ["subscription-manager orgs", {:params => {"--username=" => "BadUser", "--password=" => "BadPass"}}] 208 | result = AwesomeSpawn::CommandResult.new("", "", "Invalid username or password. To create a login, please visit https://www.redhat.com/wapps/ugc/register.html", 55, 255) 209 | error = AwesomeSpawn::CommandResultError.new( 210 | "", 211 | result 212 | ) 213 | expect(AwesomeSpawn).to receive(:run!).once.with(*run_options).and_raise(error) 214 | expect { described_class.new.organizations({:username => "BadUser", :password => "BadPass"}) }.to raise_error(LinuxAdmin::CredentialError) 215 | end 216 | end 217 | 218 | it "#enable_repo" do 219 | expect(LinuxAdmin::Common).to( 220 | receive(:run!).once.with("subscription-manager repos", {:params => {"--enable=" => "abc"}}) 221 | ) 222 | 223 | described_class.new.enable_repo("abc") 224 | end 225 | 226 | it "#disable_repo" do 227 | expect(LinuxAdmin::Common).to( 228 | receive(:run!).once.with("subscription-manager repos", {:params => {"--disable=" => "abc"}}) 229 | ) 230 | 231 | described_class.new.disable_repo("abc") 232 | end 233 | 234 | it "#all_repos" do 235 | expected = [ 236 | { 237 | :repo_id => "some-repo-source-rpms", 238 | :repo_name => "Some Repo (Source RPMs)", 239 | :repo_url => "https://my.host.example.com/repos/some-repo/source/rpms", 240 | :enabled => true 241 | }, 242 | { 243 | :repo_id => "some-repo-rpms", 244 | :repo_name => "Some Repo", 245 | :repo_url => "https://my.host.example.com/repos/some-repo/rpms", 246 | :enabled => true 247 | }, 248 | { 249 | :repo_id => "some-repo-2-beta-rpms", 250 | :repo_name => "Some Repo (Beta RPMs)", 251 | :repo_url => "https://my.host.example.com/repos/some-repo-2/beta/rpms", 252 | :enabled => false 253 | } 254 | ] 255 | 256 | expect(LinuxAdmin::Common).to( 257 | receive(:run!).once.with("subscription-manager repos", {}) 258 | .and_return(double(:output => sample_output("subscription_manager/output_repos"))) 259 | ) 260 | 261 | expect(described_class.new.all_repos).to eq(expected) 262 | end 263 | 264 | it "#enabled_repos" do 265 | expected = ["some-repo-source-rpms", "some-repo-rpms"] 266 | 267 | expect(LinuxAdmin::Common).to( 268 | receive(:run!).once.with("subscription-manager repos", {}) 269 | .and_return(double(:output => sample_output("subscription_manager/output_repos"))) 270 | ) 271 | 272 | expect(described_class.new.enabled_repos).to eq(expected) 273 | end 274 | end 275 | --------------------------------------------------------------------------------