├── .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 | [](http://badge.fury.io/rb/linux_admin)
4 | [](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)
32 | 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 |
--------------------------------------------------------------------------------