├── .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 ├── fstab_spec.rb ├── ssh_spec.rb ├── dns_spec.rb ├── registration_system_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 ├── mountable_spec.rb ├── rhn_spec.rb ├── logical_volume_spec.rb ├── hosts_spec.rb ├── yum_spec.rb ├── subscription_manager_spec.rb └── disk_spec.rb ├── lib ├── linux_admin │ ├── package.rb │ ├── version.rb │ ├── logging.rb │ ├── network_interface │ │ ├── network_interface_generic.rb │ │ └── network_interface_rh.rb │ ├── hardware.rb │ ├── null_logger.rb │ ├── yum │ │ └── repo_file.rb │ ├── system.rb │ ├── exceptions.rb │ ├── etc_issue.rb │ ├── volume.rb │ ├── common.rb │ ├── partition.rb │ ├── chrony.rb │ ├── dns.rb │ ├── deb.rb │ ├── service.rb │ ├── time_date.rb │ ├── service │ │ ├── sys_v_init_service.rb │ │ └── systemd_service.rb │ ├── mountable.rb │ ├── ip_address.rb │ ├── distro.rb │ ├── registration_system.rb │ ├── ssh_agent.rb │ ├── rpm.rb │ ├── physical_volume.rb │ ├── volume_group.rb │ ├── scap.rb │ ├── ssh.rb │ ├── fstab.rb │ ├── logical_volume.rb │ ├── hosts.rb │ ├── registration_system │ │ ├── rhn.rb │ │ └── subscription_manager.rb │ ├── yum.rb │ ├── disk.rb │ └── network_interface.rb └── linux_admin.rb ├── Rakefile ├── .rubocop.yml ├── .gitignore ├── .travis.yml ├── Gemfile ├── examples └── subscription_manager_hosted.rb ├── LICENSE.txt ├── README.md └── linux_admin.gemspec /.rubocop_local.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --warnings 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /spec/data/yum/output_repoquery_single: -------------------------------------------------------------------------------- 1 | subscription-manager 1.1.23.1 2 | -------------------------------------------------------------------------------- /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 = "1.2.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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - https://raw.githubusercontent.com/ManageIQ/guides/master/.rubocop_base.yml 3 | # put all local rubocop config into .rubocop_local.yml as it will be loaded by .rubocop_cc.yml as well 4 | - .rubocop_local.yml 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - "2.1" 4 | - "2.2.6" 5 | - "2.3.3" 6 | - "2.4.0" 7 | - ruby-head 8 | - jruby-head 9 | sudo: false 10 | cache: bundler 11 | matrix: 12 | allow_failures: 13 | - rvm: ruby-head 14 | - rvm: jruby-head 15 | fast_finish: true 16 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in linux_admin.gemspec 4 | gemspec 5 | 6 | # HACK: Rails 5 dropped support for Ruby < 2.2.2 7 | active_support_version = "< 5" if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.2.2") 8 | gem 'activesupport', active_support_version 9 | -------------------------------------------------------------------------------- /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/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 MissingConfigurationFileError < StandardError; end 11 | end 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /lib/linux_admin/common.rb: -------------------------------------------------------------------------------- 1 | require 'awesome_spawn' 2 | 3 | module LinuxAdmin 4 | module Common 5 | include Logging 6 | 7 | BIN_DIRS = %w(/bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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 | File.write(@conf, data) 14 | end 15 | 16 | def add_servers(*servers) 17 | data = File.read(@conf) 18 | data << "\n" unless data.end_with?("\n") 19 | servers.each { |s| data << "server #{s} iburst\n" } 20 | File.write(@conf, data) 21 | restart_service_if_running 22 | end 23 | 24 | private 25 | 26 | def restart_service_if_running 27 | service = Service.new(SERVICE_NAME) 28 | service.restart if service.running? 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | [described_class, LinuxAdmin].each do |klass| 23 | describe "#{klass}.run" do 24 | it "runs a command with AwesomeSpawn.run" do 25 | expect(AwesomeSpawn).to receive(:run).with("echo", nil => "test") 26 | klass.run("echo", nil => "test") 27 | end 28 | end 29 | 30 | describe "#{klass}.run!" do 31 | it "runs a command with AwesomeSpawn.run!" do 32 | expect(AwesomeSpawn).to receive(:run!).with("echo", nil => "test") 33 | klass.run!("echo", nil => "test") 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LinuxAdmin 2 | 3 | [![Gem Version](https://badge.fury.io/rb/linux_admin.svg)](http://badge.fury.io/rb/linux_admin) 4 | [![Build Status](https://travis-ci.org/ManageIQ/linux_admin.svg)](https://travis-ci.org/ManageIQ/linux_admin) 5 | [![Code Climate](http://img.shields.io/codeclimate/github/ManageIQ/linux_admin.svg)](https://codeclimate.com/github/ManageIQ/linux_admin) 6 | [![Coverage Status](http://img.shields.io/coveralls/ManageIQ/linux_admin.svg)](https://coveralls.io/r/ManageIQ/linux_admin) 7 | [![Dependency Status](https://gemnasium.com/ManageIQ/linux_admin.svg)](https://gemnasium.com/ManageIQ/linux_admin) 8 | [![Security](https://hakiri.io/github/ManageIQ/linux_admin/master.svg)](https://hakiri.io/github/ManageIQ/linux_admin/master) 9 | 10 | 11 | LinuxAdmin is a module to simplify management of linux systems. 12 | It should be a single place to manage various system level configurations, 13 | registration, updates, etc. 14 | 15 | ## Installation 16 | 17 | Add this line to your application's Gemfile: 18 | 19 | gem 'linux_admin' 20 | 21 | And then execute: 22 | 23 | $ bundle 24 | 25 | Or install it yourself as: 26 | 27 | $ gem install linux_admin 28 | 29 | ## Usage 30 | 31 | TODO: Write usage instructions here 32 | 33 | ## Contributing 34 | 35 | 1. Fork it 36 | 2. Create your feature branch (`git checkout -b my-new-feature`) 37 | 3. Commit your changes (`git commit -am 'Add some feature'`) 38 | 4. Push to the branch (`git push origin my-new-feature`) 39 | 5. Create new Pull Request 40 | -------------------------------------------------------------------------------- /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 21 | Common.run!(Common.cmd(:service), 22 | :params => {nil => [name, "start"]}) 23 | self 24 | end 25 | 26 | def stop 27 | Common.run!(Common.cmd(:service), 28 | :params => {nil => [name, "stop"]}) 29 | self 30 | end 31 | 32 | def restart 33 | status = 34 | Common.run(Common.cmd(:service), 35 | :params => {nil => [name, "restart"]}).exit_status 36 | 37 | # attempt to manually stop/start if restart fails 38 | if status != 0 39 | self.stop 40 | self.start 41 | end 42 | 43 | self 44 | end 45 | 46 | def reload 47 | Common.run!(Common.cmd(:service), :params => [name, "reload"]) 48 | self 49 | end 50 | 51 | def status 52 | Common.run(Common.cmd(:service), :params => [name, "status"]).output 53 | end 54 | 55 | def show 56 | raise NotImplementedError 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /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.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/version' 13 | require 'linux_admin/yum' 14 | require 'linux_admin/ssh' 15 | require 'linux_admin/ssh_agent' 16 | require 'linux_admin/service' 17 | require 'linux_admin/mountable' 18 | require 'linux_admin/disk' 19 | require 'linux_admin/hardware' 20 | require 'linux_admin/hosts' 21 | require 'linux_admin/partition' 22 | require 'linux_admin/etc_issue' 23 | require 'linux_admin/distro' 24 | require 'linux_admin/system' 25 | require 'linux_admin/fstab' 26 | require 'linux_admin/volume' 27 | require 'linux_admin/logical_volume' 28 | require 'linux_admin/physical_volume' 29 | require 'linux_admin/volume_group' 30 | require 'linux_admin/scap' 31 | require 'linux_admin/time_date' 32 | require 'linux_admin/ip_address' 33 | require 'linux_admin/dns' 34 | require 'linux_admin/network_interface' 35 | require 'linux_admin/chrony' 36 | require 'forwardable' 37 | 38 | module LinuxAdmin 39 | class << self 40 | extend Forwardable 41 | extend Gem::Deprecate 42 | attr_writer :logger 43 | 44 | def_delegators Common, :run 45 | def_delegators Common, :run! 46 | deprecate :run, "AwesomeSpawn.run", 2017, 6 47 | deprecate :run!, "AwesomeSpawn.run!", 2017, 6 48 | end 49 | 50 | def self.logger 51 | @logger ||= NullLogger.new 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /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/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.all 22 | @distros ||= [rhel, fedora, ubuntu, generic] 23 | end 24 | 25 | def self.local 26 | @local ||= begin 27 | Distros.all.detect(&:detected?) || Distros.generic 28 | end 29 | end 30 | 31 | class Distro 32 | attr_accessor :release_file, :etc_issue_keywords, :info_class 33 | 34 | def initialize(id, release_file = nil, etc_issue_keywords = [], info_class = nil) 35 | @id = id 36 | @release_file = release_file 37 | @etc_issue_keywords = etc_issue_keywords 38 | @info_class = info_class 39 | end 40 | 41 | def detected? 42 | detected_by_etc_issue? || detected_by_etc_release? 43 | end 44 | 45 | def detected_by_etc_issue? 46 | etc_issue_keywords && etc_issue_keywords.any? { |k| EtcIssue.instance.include?(k) } 47 | end 48 | 49 | def detected_by_etc_release? 50 | release_file && File.exist?(release_file) 51 | end 52 | 53 | def info(pkg) 54 | info_class ? info_class.info(pkg) : nil 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/linux_admin/registration_system.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class RegistrationSystem 3 | include Logging 4 | 5 | def self.registration_type(reload = false) 6 | return @registration_type if @registration_type && !reload 7 | @registration_type = registration_type_uncached 8 | end 9 | 10 | def self.method_missing(meth, *args, &block) 11 | if white_list_methods.include?(meth) 12 | r = self.registration_type.new 13 | raise NotImplementedError, "#{meth} not implemented for #{self.name}" unless r.respond_to?(meth) 14 | r.send(meth, *args, &block) 15 | else 16 | super 17 | end 18 | end 19 | 20 | def registered?(_options = nil) 21 | false 22 | end 23 | 24 | private 25 | 26 | def self.registration_type_uncached 27 | if Rhn.new.registered? 28 | Rhn 29 | elsif SubscriptionManager.new.registered? 30 | SubscriptionManager 31 | else 32 | self 33 | end 34 | end 35 | private_class_method :registration_type_uncached 36 | 37 | def self.white_list_methods 38 | @white_list_methods ||= begin 39 | all_methods = RegistrationSystem.instance_methods(false) + Rhn.instance_methods(false) + SubscriptionManager.instance_methods(false) 40 | all_methods.uniq 41 | end 42 | end 43 | private_class_method :white_list_methods 44 | 45 | def install_server_certificate(server, cert_path) 46 | host = server.start_with?("http") ? URI.parse(server).host : server 47 | 48 | LinuxAdmin::Rpm.upgrade("http://#{host}/#{cert_path}") 49 | end 50 | end 51 | end 52 | 53 | Dir.glob(File.join(File.dirname(__FILE__), "registration_system", "*.rb")).each { |f| require f } 54 | -------------------------------------------------------------------------------- /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') 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 | -------------------------------------------------------------------------------- /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.test_files = `git ls-files -- spec/*`.split("\n") 28 | spec.require_paths = ["lib"] 29 | 30 | spec.required_ruby_version = ">= 2.0.0" 31 | 32 | spec.add_development_dependency "bundler", "~> 1.3" 33 | spec.add_development_dependency "rake" 34 | spec.add_development_dependency "rspec", "~> 3.0" 35 | spec.add_development_dependency "coveralls" 36 | spec.add_development_dependency "rubocop" 37 | 38 | spec.add_dependency "awesome_spawn", "~> 1.3" 39 | spec.add_dependency "inifile" 40 | spec.add_dependency "more_core_extensions", "~> 3.0" 41 | spec.add_dependency "nokogiri", ">= 1.8.1", "~> 1.8" 42 | spec.add_dependency "openscap" 43 | spec.add_dependency "net-ssh", "~> 4.2.0" 44 | end 45 | -------------------------------------------------------------------------------- /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/service/systemd_service.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class SystemdService < Service 3 | def running? 4 | Common.run(command_path, :params => ["status", name]).success? 5 | end 6 | 7 | def enable 8 | Common.run!(command_path, :params => ["enable", name]) 9 | self 10 | end 11 | 12 | def disable 13 | Common.run!(command_path, :params => ["disable", name]) 14 | self 15 | end 16 | 17 | def start 18 | Common.run!(command_path, :params => ["start", name]) 19 | self 20 | end 21 | 22 | def stop 23 | Common.run!(command_path, :params => ["stop", name]) 24 | self 25 | end 26 | 27 | def restart 28 | status = Common.run(command_path, :params => ["restart", name]).exit_status 29 | 30 | # attempt to manually stop/start if restart fails 31 | if status != 0 32 | stop 33 | start 34 | end 35 | 36 | self 37 | end 38 | 39 | def reload 40 | Common.run!(command_path, :params => ["reload", name]) 41 | self 42 | end 43 | 44 | def status 45 | Common.run(command_path, :params => ["status", name]).output 46 | end 47 | 48 | def show 49 | output = Common.run!(command_path, :params => ["show", name]).output 50 | output.split("\n").each_with_object({}) do |line, h| 51 | k, v = line.split("=", 2) 52 | h[k] = cast_show_value(k, v) 53 | end 54 | end 55 | 56 | private 57 | 58 | def command_path 59 | Common.cmd(:systemctl) 60 | end 61 | 62 | def cast_show_value(key, value) 63 | return value.to_i if value =~ /^\d+$/ 64 | 65 | case key 66 | when /^.*Timestamp$/ 67 | Time.parse(value) 68 | when /Exec(Start|Stop)/ 69 | parse_exec_value(value) 70 | else 71 | value 72 | end 73 | end 74 | 75 | def parse_exec_value(value) 76 | value[1..-2].strip.split(" ; ").map { |s| s.split("=", 2) }.to_h 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/linux_admin/volume_group.rb: -------------------------------------------------------------------------------- 1 | module LinuxAdmin 2 | class VolumeGroup 3 | # volume group name 4 | attr_accessor :name 5 | 6 | # other fields available 7 | # volume group access 8 | # volume group status 9 | # internal volume group number 10 | # maximum number of logical volumes 11 | # current number of logical volumes 12 | # open count of all logical volumes in this volume group 13 | # maximum logical volume size 14 | # maximum number of physical volumes 15 | # current number of physical volumes 16 | # actual number of physical volumes 17 | # size of volume group in kilobytes 18 | # physical extent size 19 | # total number of physical extents for this volume group 20 | # allocated number of physical extents for this volume group 21 | # free number of physical extents for this volume group 22 | # uuid of volume group 23 | 24 | def initialize(args = {}) 25 | @name = args[:name] 26 | end 27 | 28 | def attach_to(lv) 29 | Common.run!(Common.cmd(:lvextend), 30 | :params => [lv.name, name]) 31 | self 32 | end 33 | 34 | def extend_with(pv) 35 | Common.run!(Common.cmd(:vgextend), 36 | :params => [@name, pv.device_name]) 37 | pv.volume_group = self 38 | self 39 | end 40 | 41 | def self.create(name, pv) 42 | self.scan # initialize local volume groups 43 | Common.run!(Common.cmd(:vgcreate), 44 | :params => [name, pv.device_name]) 45 | vg = VolumeGroup.new :name => name 46 | pv.volume_group = vg 47 | @vgs << vg 48 | vg 49 | end 50 | 51 | def self.scan 52 | @vgs ||= begin 53 | vgs = [] 54 | 55 | out = Common.run!(Common.cmd(:vgdisplay), :params => {'-c' => nil}).output 56 | 57 | out.each_line do |line| 58 | fields = line.lstrip.split(':') 59 | vgs << VolumeGroup.new(:name => fields[0]) 60 | end 61 | 62 | vgs 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/scap_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Scap do 2 | subject { described_class.new("rhel7") } 3 | 4 | describe "#lockdown" do 5 | it "raises if given no rules" do 6 | allow(described_class).to receive(:openscap_available?).and_return(true) 7 | allow(described_class).to receive(:ssg_available?).and_return(true) 8 | 9 | expect { subject.lockdown("value1" => true) }.to raise_error(RuntimeError) 10 | end 11 | end 12 | 13 | describe "#profile_xml (private)" do 14 | it "creates a Profile tag" do 15 | profile_xml = subject.send(:profile_xml, "test-profile", [], {}) 16 | expect(profile_xml).to match(%r{.*}m) 17 | end 18 | 19 | it "creates a title tag" do 20 | profile_xml = subject.send(:profile_xml, "test-profile", [], {}) 21 | expect(profile_xml).to match(%r{test-profile}m) 22 | end 23 | 24 | it "creates a description tag" do 25 | profile_xml = subject.send(:profile_xml, "test-profile", [], {}) 26 | expect(profile_xml).to match(%r{test-profile}m) 27 | end 28 | 29 | it "creates a select tag for each rule" do 30 | profile_xml = subject.send(:profile_xml, "test-profile", %w(rule1 rule2), {}) 31 | expect(profile_xml).to match(%r{}m) 33 | end 34 | 35 | it "creates a refine-value tag for each value" do 36 | profile_xml = subject.send(:profile_xml, "test-profile", [], "key1" => "val1", "key2" => "val2") 37 | expect(profile_xml).to match(%r{}m) 38 | expect(profile_xml).to match(%r{}m) 39 | end 40 | end 41 | 42 | describe ".ds_file" do 43 | it "returns the platform ds file path" do 44 | file = described_class.ds_file("rhel7") 45 | expect(file.to_s).to eq("/usr/share/xml/scap/ssg/content/ssg-rhel7-ds.xml") 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /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 | server foo.example.net 5 | server bar.example.net iburst 6 | driftfile /var/lib/chrony/drift 7 | makestep 10 3 8 | rtcsync 9 | EOF 10 | 11 | subject do 12 | allow(File).to receive(:exist?).and_return(true) 13 | described_class.new 14 | end 15 | 16 | describe ".new" do 17 | it "raises when the given config file doesn't exist" do 18 | expect { described_class.new("nonsense/file") }.to raise_error(LinuxAdmin::MissingConfigurationFileError) 19 | end 20 | end 21 | 22 | describe "#clear_servers" do 23 | it "removes all the server lines from the conf file" do 24 | allow(File).to receive(:read).and_return(CHRONY_CONF.dup) 25 | expect(File).to receive(:write) do |_file, contents| 26 | expect(contents).to eq "# commented server baz.example.net\ndriftfile /var/lib/chrony/drift\nmakestep 10 3\nrtcsync\n" 27 | end 28 | subject.clear_servers 29 | end 30 | end 31 | 32 | describe "#add_servers" do 33 | it "adds server lines to the conf file" do 34 | allow(File).to receive(:read).and_return(CHRONY_CONF.dup) 35 | expect(File).to receive(:write) do |_file, contents| 36 | expect(contents).to eq(CHRONY_CONF + "server baz.example.net iburst\nserver foo.bar.example.com iburst\n") 37 | end 38 | allow(subject).to receive(:restart_service_if_running) 39 | subject.add_servers("baz.example.net", "foo.bar.example.com") 40 | end 41 | 42 | it "restarts the service if it is running" do 43 | allow(File).to receive(:read).and_return(CHRONY_CONF.dup) 44 | allow(File).to receive(:write) 45 | 46 | chronyd_service = double 47 | expect(LinuxAdmin::Service).to receive(:new).with("chronyd").and_return(chronyd_service) 48 | expect(chronyd_service).to receive(:running?).and_return true 49 | expect(chronyd_service).to receive(:restart) 50 | subject.add_servers("baz.example.net", "foo.bar.example.com") 51 | end 52 | 53 | it "doesn't restart the service if it is not running" do 54 | allow(File).to receive(:read).and_return(CHRONY_CONF.dup) 55 | allow(File).to receive(:write) 56 | 57 | chronyd_service = double 58 | expect(LinuxAdmin::Service).to receive(:new).with("chronyd").and_return(chronyd_service) 59 | expect(chronyd_service).to receive(:running?).and_return false 60 | expect(chronyd_service).not_to receive(:restart) 61 | subject.add_servers("baz.example.net", "foo.bar.example.com") 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /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) 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 Distros.generic" do 47 | etc_issue_contains('') 48 | exists("/etc/fedora-release" => false, "/etc/redhat-release" => false) 49 | expect(subject).to eq(LinuxAdmin::Distros.generic) 50 | end 51 | end 52 | 53 | describe "#info" do 54 | it "dispatches to redhat lookup mechanism" do 55 | stub_distro(LinuxAdmin::Distros.rhel) 56 | expect(LinuxAdmin::Rpm).to receive(:info).with('ruby') 57 | LinuxAdmin::Distros.local.info 'ruby' 58 | end 59 | 60 | it "dispatches to ubuntu lookup mechanism" do 61 | stub_distro(LinuxAdmin::Distros.ubuntu) 62 | expect(LinuxAdmin::Deb).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.generic) 68 | expect { LinuxAdmin::Distros.local.info 'ruby' }.not_to raise_error 69 | end 70 | end 71 | 72 | private 73 | 74 | def exists(files) 75 | files.each_pair { |file, value| allow(File).to receive(:exist?).with(file).and_return(value) } 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /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 = < 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.css("//nist_list|model", "nist_list" => "http://checklists.nist.gov/xccdf/1.2").detect { |model| model.namespace.prefix.nil? } 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/ssh_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::SSH do 2 | before(:each) do 3 | require 'net/ssh' 4 | @example_ssh_key = <<-eos 5 | -----BEGIN RSA PRIVATE KEY----- 6 | MIIEowIBAAKCAQEAasdvkMNteq7hjnqNE61ESnEHnaOtHxZffdsQ33R7BXcu9eCH 7 | ncZadHmmfRZgMPQnHGX0NzboVKfdpdF40o+iGQKyy3wKqdgGnTAWqx/hxrsdsdgh 8 | f/g7AABNjoWp1OiTX2Dn99SH9xPQtpdnwGlBmtPplV2wNcKouQwGbwb/u1EHHxnO 9 | aSQk2tvKHRMgroLsyuM5ay7TrK5cip2QTMn9fieHIepH0qd8ETG+1Uf+XmZxhdMN 10 | QsuiSbAAmuU9qwlXh6QOg1spJ97B/ZY6Ci4d5cdBCCtfjGDBm8CzbrfhYVNhGcNx 11 | c3Y43ld6MJoE+hMuBrBSuZyBmf63AAMqJ8KeQwIDAQABAoIBAFMFXBoSWOy1V9z3 12 | IkoY1ilBp5WKRvaPEMCgeCfZIjIG0Nmt5Knbkp2b3A8YWnGQlDHqdYz53t9RHaaU 13 | KdCnJ9vUSYeeeNElMsCwMYVoZvA5Xpv9cw2YOeTrOzssX1VZv9WW1zHd0Srz9N3r 14 | 719MgpyK4dZACw6ODeD/gh8+OH5OAN9sVbIGniApHZENxJrZzL22qE2asf8vVfgu 15 | 9XNx3WhsdL6ktoeJWLKxTLwVP6xRK/ixvsayUFeMBC4E1+sBuz1z7p27kd8cEC5o 16 | AEHSwF6nr+ix0JctbAYqPVFZi5zFH10WgxyF3asAiQPYrCrY9gT8OQSRDhhKyIXE 17 | rHlLIfkCgYEA+RuY4vsqeU69APFXRCy9s/dcy8tS0Pslq/fq4KsdC4qRu39RjotN 18 | /OXOtVMPRfjTiRHXi/0fvSfyMVgstCtfYpOcoz98tw2AX34AgAH6h57I29rVxk5q 19 | OomgcauRYlmO9ge3429pvECL5EzBbRKwfwuMKHMNetLHdVMc7d165f0CgYEAzdGf 20 | 6VMnZkK66XP/RzMvwUwvdhy+vIUjXFMjotQXYcYcLZadiZET7riOSRyVRvlNg0va 21 | CexWtX+0yOZOrXCvLzSpO8Z91VnkHbRhnpy4bvW/qcFpwIvYeaMgr5C1lBHCIvZS 22 | wSojNrdjAbdQoqab9X7Mxj2ubYGxUSSg715wqT8CgYBaY20iT0imI6/o+6lSj3l2 23 | J7eAKxKtybNtptOPF6RVq74dbqFFO77cmPZcTPspxJPdFKBFp18w36G9zeTKq0I9 24 | HpqjkZHLShbej3XW/ODO/Qqc29bd0e4xt2aEWGC0cxKwqzRKTk7rg/A+sqsszK9G 25 | KgZ9VuH5QyokpDfHB6pkcQKBgChsq8PgGTT0llGT/ue1HgQROqEwNCZC4BcaHT21 26 | +oGxr4cktfx3Cjsw9IFXo9o0zQyksUaRrNYpJxDuazWVlFLpPPQIoF5vMWbELwhA 27 | L9lbWzG0U1kGHpaFe73/5ioW8tJ7HvXhmNj+W+vSXXwUzT0CkqW9J61Kc9FEKHfb 28 | TLVxAoGBAM8WKsTfEMxuv5pabGTo8BC1ruz4c5qLJGLGnH4kWJm5o6ippugFsOlK 29 | rDUWive4uLSKi3Fsb2kPw6gHuRGerFN1CBpCENLib3xG5Bd4XhmKZMDxqU2bO0gd 30 | sV1Tr/acrE0aWBkD9RYrR2/UwG1zfXuIJeufdWf8c0SY3X6J7jJN 31 | -----END RSA PRIVATE KEY----- 32 | eos 33 | end 34 | 35 | it "should create a call using agent" do 36 | expect(Net::SSH).to receive(:start).with("127.0.0.1", "root", :verify_host_key => false, :forward_agent => true, :number_of_password_prompts => 0, :agent_socket_factory => Proc).and_return(true) 37 | ssh_agent = LinuxAdmin::SSHAgent.new(@example_ssh_key) 38 | ssh_agent.with_service do |socket| 39 | ssh = LinuxAdmin::SSH.new("127.0.0.1", "root") 40 | ssh.perform_commands(%w("ls", "pwd"), socket) 41 | end 42 | end 43 | 44 | it "should preform command using private key" do 45 | 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) 46 | LinuxAdmin::SSH.new("127.0.0.1", "root", @example_ssh_key).perform_commands(%w("ls", "pwd")) 47 | end 48 | 49 | it "should preform command using password" do 50 | 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) 51 | LinuxAdmin::SSH.new("127.0.0.1", "root", nil, "password").perform_commands(%w("ls", "pwd")) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /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 | self.columns.collect { |c| c ? c.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(" ") 53 | end 54 | end 55 | 56 | class FSTab 57 | include Singleton 58 | 59 | attr_accessor :entries 60 | attr_accessor :maximum_column_lengths 61 | 62 | def initialize 63 | refresh 64 | end 65 | 66 | def write! 67 | content = '' 68 | @entries.each do |entry| 69 | if entry.has_content? 70 | content << entry.formatted_columns(@maximum_column_lengths) << "\n" 71 | else 72 | content << "#{entry.comment}" 73 | end 74 | end 75 | 76 | File.write('/etc/fstab', content) 77 | self 78 | end 79 | 80 | private 81 | 82 | def read 83 | File.read('/etc/fstab').lines 84 | end 85 | 86 | def refresh 87 | @entries = [] 88 | @maximum_column_lengths = Array.new(7, 0) # # of columns 89 | read.each do |line| 90 | entry = FSTabEntry.from_line(line) 91 | @entries << entry 92 | 93 | lengths = entry.column_lengths 94 | lengths.each_index do |i| 95 | @maximum_column_lengths[i] = 96 | lengths[i] if lengths[i] > @maximum_column_lengths[i] 97 | end 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/dns_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Dns do 2 | RESOLV_CONF = < 0)) 40 | 41 | LinuxAdmin::SubscriptionManager.new.registered? 42 | end 43 | 44 | it "with a proxy" do 45 | expect(LinuxAdmin::Common).to receive(:run).with( 46 | "subscription-manager identity", 47 | :params => { 48 | "--proxy=" => "https://example.com", 49 | "--proxyuser=" => "user", 50 | "--proxypassword=" => "pass" 51 | } 52 | ).and_return(double(:exit_status => 0)) 53 | 54 | LinuxAdmin::SubscriptionManager.new.registered?( 55 | :proxy_address => "https://example.com", 56 | :proxy_username => "user", 57 | :proxy_password => "pass" 58 | ) 59 | end 60 | end 61 | end 62 | 63 | context ".method_missing" do 64 | before do 65 | stub_registered_to_system(:rhn) 66 | end 67 | 68 | it "exists on the subclass" do 69 | expect(LinuxAdmin::RegistrationSystem.registered?).to be_truthy 70 | end 71 | 72 | it "does not exist on the subclass" do 73 | expect { LinuxAdmin::RegistrationSystem.organizations }.to raise_error(NotImplementedError) 74 | end 75 | 76 | it "is an unknown method" do 77 | expect { LinuxAdmin::RegistrationSystem.method_does_not_exist }.to raise_error(NoMethodError) 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 | allow_any_instance_of(LinuxAdmin::Rhn).to receive_messages(:registered? => (system.include?(:rhn))) 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /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/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, "", 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, "", 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, "", 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 | -------------------------------------------------------------------------------- /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 "returns self" do 68 | expect(LinuxAdmin::Common).to receive(:run!) 69 | expect(@service.start).to eq(@service) 70 | end 71 | end 72 | 73 | describe "#stop" do 74 | it "stops service" do 75 | expect(LinuxAdmin::Common).to receive(:run!) 76 | .with(LinuxAdmin::Common.cmd(:service), 77 | :params => {nil => %w(foo stop)}) 78 | @service.stop 79 | end 80 | 81 | it "returns self" do 82 | expect(LinuxAdmin::Common).to receive(:run!) 83 | expect(@service.stop).to eq(@service) 84 | end 85 | end 86 | 87 | describe "#restart" do 88 | it "stops service" do 89 | expect(LinuxAdmin::Common).to receive(:run) 90 | .with(LinuxAdmin::Common.cmd(:service), 91 | :params => {nil => %w(foo restart)}).and_return(double(:exit_status => 0)) 92 | @service.restart 93 | end 94 | 95 | context "service restart fails" do 96 | it "manually stops/starts service" do 97 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:exit_status => 1)) 98 | expect(@service).to receive(:stop) 99 | expect(@service).to receive(:start) 100 | @service.restart 101 | end 102 | end 103 | 104 | it "returns self" do 105 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:exit_status => 0)) 106 | expect(@service.restart).to eq(@service) 107 | end 108 | end 109 | 110 | describe "#reload" do 111 | it "reloads service" do 112 | expect(LinuxAdmin::Common).to receive(:run!) 113 | .with(LinuxAdmin::Common.cmd(:service), :params => %w(foo reload)) 114 | expect(@service.reload).to eq(@service) 115 | end 116 | end 117 | 118 | describe "#status" do 119 | it "returns the service status" do 120 | status = "service status here" 121 | expect(LinuxAdmin::Common).to receive(:run) 122 | .with(LinuxAdmin::Common.cmd(:service), 123 | :params => %w(foo status)).and_return(double(:output => status)) 124 | expect(@service.status).to eq(status) 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/linux_admin/registration_system/rhn.rb: -------------------------------------------------------------------------------- 1 | require 'nokogiri' 2 | 3 | module LinuxAdmin 4 | class Rhn < RegistrationSystem 5 | SATELLITE5_SERVER_CERT_PATH = "pub/rhn-org-trusted-ssl-cert-1.0-1.noarch.rpm" 6 | INSTALLED_SERVER_CERT_PATH = "/usr/share/rhn/RHN-ORG-TRUSTED-SSL-CERT" 7 | 8 | def initialize 9 | warn("[DEPRECATION] 'LinuxAdmin::Rhn' is deprecated. Please use 'LinuxAdmin::SubscriptionManager' instead.") 10 | end 11 | 12 | def registered?(_options = nil) 13 | id = "" 14 | if File.exist?(systemid_file) 15 | xml = Nokogiri.XML(File.read(systemid_file)) 16 | id = xml.xpath('/params/param/value/struct/member[name="system_id"]/value/string').text 17 | end 18 | id.length > 0 19 | end 20 | 21 | def register(options) 22 | cmd = "rhnreg_ks" 23 | params = {} 24 | 25 | if options[:activationkey] 26 | params["--activationkey="] = options[:activationkey] 27 | elsif options[:username] && options[:password] 28 | params["--username="] = options[:username] 29 | params["--password="] = options[:password] 30 | else 31 | raise ArgumentError, "activation key or username and password are required" 32 | end 33 | 34 | install_server_certificate(options[:server_url], SATELLITE5_SERVER_CERT_PATH) if options[:server_url] 35 | certificate_installed = LinuxAdmin::Rpm.list_installed["rhn-org-trusted-ssl-cert"] 36 | 37 | params["--proxy="] = options[:proxy_address] if options[:proxy_address] 38 | params["--proxyUser="] = options[:proxy_username] if options[:proxy_username] 39 | params["--proxyPassword="] = options[:proxy_password] if options[:proxy_password] 40 | params["--serverUrl="] = options[:server_url] if options[:server_url] 41 | params["--systemorgid="] = options[:org] if options[:server_url] && options[:org] 42 | params["--sslCACert="] = INSTALLED_SERVER_CERT_PATH if certificate_installed 43 | 44 | Common.run!(cmd, :params => params) 45 | end 46 | 47 | def enable_channel(repo, options) 48 | cmd = "rhn-channel -a" 49 | params = user_pwd(options).merge("--channel=" => repo) 50 | 51 | logger.info("#{self.class.name}##{__method__} Enabling channel: #{repo}") 52 | Common.run!(cmd, :params => params) 53 | end 54 | alias_method :subscribe, :enable_channel 55 | alias_method :enable_repo, :enable_channel 56 | 57 | def disable_channel(repo, options) 58 | cmd = "rhn-channel -r" 59 | params = user_pwd(options).merge("--channel=" => repo) 60 | 61 | Common.run!(cmd, :params => params) 62 | end 63 | alias_method :disable_repo, :disable_channel 64 | 65 | def enabled_channels 66 | cmd = "rhn-channel -l" 67 | 68 | Common.run!(cmd).output.split("\n").compact 69 | end 70 | alias_method :enabled_repos, :enabled_channels 71 | alias_method :subscribed_products, :enabled_channels 72 | 73 | def available_channels(options) 74 | cmd = "rhn-channel -L" 75 | params = user_pwd(options) 76 | 77 | Common.run!(cmd, :params => params).output.chomp.split("\n").compact 78 | end 79 | 80 | def all_repos(options) 81 | available = available_channels_with_status(options) 82 | merge_enabled_channels_with_status(available) 83 | end 84 | 85 | private 86 | 87 | def available_channels_with_status(options) 88 | available_channels(options).collect { |ac| {:repo_id => ac, :enabled => false} } 89 | end 90 | 91 | def merge_enabled_channels_with_status(available) 92 | enabled_channels.each_with_object(available) do |enabled, all| 93 | if repo = all.detect { |i| i[:repo_id] == enabled } 94 | repo[:enabled] = true 95 | else 96 | all.push({:repo_id => enabled, :enabled => true}) 97 | end 98 | end 99 | end 100 | 101 | def user_pwd(options) 102 | raise ArgumentError, "username and password are required" if options[:username].blank? || options[:password].blank? 103 | 104 | {"--user=" => options[:username], "--password=" => options[:password]} 105 | end 106 | 107 | def systemid_file 108 | "/etc/sysconfig/rhn/systemid" 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /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/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 "returns self" do 57 | expect(LinuxAdmin::Common).to receive(:run!) 58 | expect(@service.start).to eq(@service) 59 | end 60 | end 61 | 62 | describe "#stop" do 63 | it "stops service" do 64 | expect(LinuxAdmin::Common).to receive(:run!).with(command, :params => %w(stop foo)) 65 | @service.stop 66 | end 67 | 68 | it "returns self" do 69 | expect(LinuxAdmin::Common).to receive(:run!) 70 | expect(@service.stop).to eq(@service) 71 | end 72 | end 73 | 74 | describe "#restart" do 75 | it "restarts service" do 76 | expect(LinuxAdmin::Common).to receive(:run).with(command, :params => %w(restart foo)).and_return(double(:exit_status => 0)) 77 | @service.restart 78 | end 79 | 80 | it "manually stops then starts service when restart fails" do 81 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:exit_status => 1)) 82 | expect(@service).to receive(:stop) 83 | expect(@service).to receive(:start) 84 | @service.restart 85 | end 86 | 87 | it "returns self" do 88 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:exit_status => 0)) 89 | expect(@service.restart).to eq(@service) 90 | end 91 | end 92 | 93 | describe "#reload" do 94 | it "reloads service" do 95 | expect(LinuxAdmin::Common).to receive(:run!).with(command, :params => %w(reload foo)) 96 | expect(@service.reload).to eq(@service) 97 | end 98 | end 99 | 100 | describe "#status" do 101 | it "returns the service status" do 102 | status = "service status here" 103 | expect(LinuxAdmin::Common).to receive(:run) 104 | .with(command, :params => %w(status foo)).and_return(double(:output => status)) 105 | expect(@service.status).to eq(status) 106 | end 107 | end 108 | 109 | describe "#show" do 110 | it "returns a hash of runtime information" do 111 | output = <<-EOS 112 | MainPID=29189 113 | ExecMainStartTimestamp=Wed 2017-02-08 13:49:57 EST 114 | ExecStart={ path=/bin/sh ; argv[]=/bin/sh -c /bin/evmserver.sh start ; status=0/0 } 115 | ExecStop={ path=/bin/sh ; argv[]=/bin/sh -c /bin/evmserver.sh stop ; status=0/0 } 116 | ControlGroup=/system.slice/evmserverd.service 117 | MemoryCurrent=2865373184 118 | EOS 119 | 120 | hash = { 121 | "MainPID" => 29_189, 122 | "ExecMainStartTimestamp" => Time.new(2017, 2, 8, 13, 49, 57, "-05:00"), 123 | "ExecStart" => {"path" => "/bin/sh", "argv[]" => "/bin/sh -c /bin/evmserver.sh start", "status" => "0/0"}, 124 | "ExecStop" => {"path" => "/bin/sh", "argv[]" => "/bin/sh -c /bin/evmserver.sh stop", "status" => "0/0"}, 125 | "ControlGroup" => "/system.slice/evmserverd.service", 126 | "MemoryCurrent" => 2_865_373_184 127 | } 128 | expect(LinuxAdmin::Common).to receive(:run!) 129 | .with(command, :params => %w(show foo)).and_return(double(:output => output)) 130 | expect(@service.show).to eq(hash) 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause this 4 | # file to always be loaded, without a need to explicitly require it in any files. 5 | # 6 | # Given that it is always loaded, you are encouraged to keep this file as 7 | # light-weight as possible. Requiring heavyweight dependencies from this file 8 | # will add to the boot time of your test suite on EVERY test run, even for an 9 | # individual file that may not need all of that loaded. Instead, make a 10 | # separate helper file that requires this one and then use it only in the specs 11 | # that actually need it. 12 | # 13 | # The `.rspec` file also contains a few flags that are not defaults but that 14 | # users commonly want. 15 | # 16 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 17 | RSpec.configure do |config| 18 | # The settings below are suggested to provide a good initial experience 19 | # with RSpec, but feel free to customize to your heart's content. 20 | 21 | # These two settings work together to allow you to limit a spec run 22 | # to individual examples or groups you care about by tagging them with 23 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 24 | # get run. 25 | config.filter_run :focus 26 | config.run_all_when_everything_filtered = true 27 | 28 | # Many RSpec users commonly either run the entire suite or an individual 29 | # file, and it's useful to allow more verbose output when running an 30 | # individual spec file. 31 | if config.files_to_run.one? 32 | # Use the documentation formatter for detailed output, 33 | # unless a formatter has already been configured 34 | # (e.g. via a command-line flag). 35 | config.default_formatter = 'doc' 36 | end 37 | 38 | # Print the 10 slowest examples and example groups at the 39 | # end of the spec run, to help surface which specs are running 40 | # particularly slow. 41 | # config.profile_examples = 10 42 | 43 | # Run specs in random order to surface order dependencies. If you find an 44 | # order dependency and want to debug it, you can fix the order by providing 45 | # the seed, which is printed after each run. 46 | # --seed 1234 47 | config.order = :random 48 | 49 | # Seed global randomization in this process using the `--seed` CLI option. 50 | # Setting this allows you to use `--seed` to deterministically reproduce 51 | # test failures related to randomization by passing the same `--seed` value 52 | # as the one that triggered the failure. 53 | Kernel.srand config.seed 54 | 55 | # rspec-expectations config goes here. You can use an alternate 56 | # assertion/expectation library such as wrong or the stdlib/minitest 57 | # assertions if you prefer. 58 | config.expect_with :rspec do |expectations| 59 | # Enable only the newer, non-monkey-patching expect syntax. 60 | # For more details, see: 61 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 62 | expectations.syntax = :expect 63 | end 64 | 65 | # rspec-mocks config goes here. You can use an alternate test double 66 | # library (such as bogus or mocha) by changing the `mock_with` option here. 67 | config.mock_with :rspec do |mocks| 68 | # Enable only the newer, non-monkey-patching expect syntax. 69 | # For more details, see: 70 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 71 | mocks.syntax = :expect 72 | 73 | # Prevents you from mocking or stubbing a method that does not exist on 74 | # a real object. This is generally recommended. 75 | mocks.verify_partial_doubles = true 76 | end 77 | 78 | config.after do 79 | clear_caches 80 | end 81 | end 82 | 83 | begin 84 | require 'coveralls' 85 | Coveralls.wear! 86 | rescue LoadError 87 | end 88 | 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 | -------------------------------------------------------------------------------- /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 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, "", 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 | -------------------------------------------------------------------------------- /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 10 | 11 | def self.local 12 | Dir.glob(['/dev/[vhs]d[a-z]', '/dev/xvd[a-z]']).collect do |d| 13 | Disk.new :path => d 14 | end 15 | end 16 | 17 | def initialize(args = {}) 18 | @path = args[:path] 19 | end 20 | 21 | def size 22 | @size ||= begin 23 | size = nil 24 | out = Common.run!(Common.cmd(:fdisk), :params => {"-l" => nil}).output 25 | out.each_line do |l| 26 | /Disk #{path}: .*B, (\d+) bytes/.match(l) do |m| 27 | size = m[1].to_i 28 | break 29 | end 30 | end 31 | size 32 | end 33 | end 34 | 35 | def partitions 36 | @partitions ||= 37 | parted_output.collect { |disk| 38 | partition_from_parted(disk) 39 | } 40 | end 41 | 42 | def create_partition_table(type = "msdos") 43 | Common.run!(Common.cmd(:parted), :params => {nil => parted_options_array("mklabel", type)}) 44 | end 45 | 46 | def has_partition_table? 47 | result = Common.run(Common.cmd(:parted), :params => {nil => parted_options_array("print")}) 48 | 49 | result_indicates_partition_table?(result) 50 | end 51 | 52 | def create_partition(partition_type, *args) 53 | create_partition_table unless has_partition_table? 54 | 55 | start = finish = size = nil 56 | case args.length 57 | when 1 then 58 | start = partitions.empty? ? 0 : partitions.last.end_sector 59 | size = args.first 60 | finish = start + size 61 | 62 | when 2 then 63 | start = args[0] 64 | finish = args[1] 65 | 66 | else 67 | raise ArgumentError, "must specify start/finish or size" 68 | end 69 | 70 | id = partitions.empty? ? 1 : (partitions.last.id + 1) 71 | options = parted_options_array('mkpart', '-a', 'opt', partition_type, start, finish) 72 | Common.run!(Common.cmd(:parted), :params => {nil => options}) 73 | 74 | partition = Partition.new(:disk => self, 75 | :id => id, 76 | :start_sector => start, 77 | :end_sector => finish, 78 | :size => size, 79 | :partition_type => partition_type) 80 | partitions << partition 81 | partition 82 | end 83 | 84 | def create_partitions(partition_type, *args) 85 | check_if_partitions_overlap(args) 86 | 87 | args.each { |arg| 88 | self.create_partition(partition_type, arg[:start], arg[:end]) 89 | } 90 | end 91 | 92 | def clear! 93 | @partitions = [] 94 | 95 | # clear partition table 96 | Common.run!(Common.cmd(:dd), 97 | :params => { 'if=' => '/dev/zero', 'of=' => @path, 98 | 'bs=' => 512, 'count=' => 1}) 99 | 100 | self 101 | end 102 | 103 | private 104 | 105 | def str_to_bytes(val, unit) 106 | case unit 107 | when 'K' then 108 | val.to_f * 1_024 # 1.kilobytes 109 | when 'M' then 110 | val.to_f * 1_048_576 # 1.megabyte 111 | when 'G' then 112 | val.to_f * 1_073_741_824 # 1.gigabytes 113 | end 114 | end 115 | 116 | def overlapping_ranges?(ranges) 117 | ranges.find do |range1| 118 | ranges.any? do |range2| 119 | range1 != range2 && 120 | ranges_overlap?(range1, range2) 121 | end 122 | end 123 | end 124 | 125 | def ranges_overlap?(range1, range2) # copied from activesupport Range#overlaps? 126 | range1.cover?(range2.first) || range2.cover?(range1.first) 127 | end 128 | 129 | def check_if_partitions_overlap(partitions) 130 | ranges = 131 | partitions.collect do |partition| 132 | start = partition[:start] 133 | finish = partition[:end] 134 | start.delete('%') 135 | finish.delete('%') 136 | start.to_f..finish.to_f 137 | end 138 | 139 | if overlapping_ranges?(ranges) 140 | raise ArgumentError, "overlapping partitions" 141 | end 142 | end 143 | 144 | def parted_output 145 | # TODO: Should this really catch non-zero RC, set output to the default "" and silently return [] ? 146 | # If so, should other calls to parted also do the same? 147 | # requires sudo 148 | out = Common.run(Common.cmd(:parted), 149 | :params => { nil => parted_options_array('print') }).output 150 | split = [] 151 | out.each_line do |l| 152 | if l =~ /^ [0-9].*/ 153 | split << l.split 154 | end 155 | end 156 | split 157 | end 158 | 159 | 160 | def partition_from_parted(output_disk) 161 | args = {:disk => self} 162 | PARTED_FIELDS.each_index do |i| 163 | val = output_disk[i] 164 | case PARTED_FIELDS[i] 165 | when :start_sector, :end_sector, :size 166 | if val =~ /([0-9\.]*)([KMG])B/ 167 | val = str_to_bytes($1, $2) 168 | end 169 | 170 | when :id 171 | val = val.to_i 172 | 173 | end 174 | args[PARTED_FIELDS[i]] = val 175 | end 176 | 177 | Partition.new(args) 178 | end 179 | 180 | def parted_options_array(*args) 181 | args = args.first if args.first.kind_of?(Array) 182 | parted_default_options + args 183 | end 184 | 185 | def parted_default_options 186 | @parted_default_options ||= ['--script', path].freeze 187 | end 188 | 189 | def result_indicates_partition_table?(result) 190 | # parted exits with 1 but writes this oddly spelled error to stdout. 191 | missing = (result.exit_status == 1 && result.output.include?("unrecognised disk label")) 192 | !missing 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/rhn_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Rhn do 2 | context "#registered?" do 3 | it "with registered system_id" do 4 | allow_any_instance_of(described_class).to receive_messages(:systemid_file => data_file_path("rhn/systemid")) 5 | expect(described_class.new).to be_registered 6 | end 7 | 8 | it "with unregistered system_id" do 9 | allow_any_instance_of(described_class).to receive_messages(:systemid_file => data_file_path("rhn/systemid.missing_system_id")) 10 | expect(described_class.new).to_not be_registered 11 | end 12 | 13 | it "with missing systemid file" do 14 | allow_any_instance_of(described_class).to receive_messages(:systemid_file => data_file_path("rhn/systemid.missing_file")) 15 | expect(described_class.new).to_not be_registered 16 | end 17 | end 18 | 19 | context "#register" do 20 | it "no username or activation key" do 21 | expect { described_class.new.register({}) }.to raise_error(ArgumentError) 22 | end 23 | 24 | context "with username and password" do 25 | let(:base_options) { {:username => "SomeUser@SomeDomain.org", 26 | :password => "SomePass", 27 | :org => "2", 28 | :proxy_address => "1.2.3.4", 29 | :proxy_username => "ProxyUser", 30 | :proxy_password => "ProxyPass", 31 | :server_cert => "/path/to/cert", 32 | } 33 | } 34 | let(:run_params) { {:params=>{"--username="=>"SomeUser@SomeDomain.org", "--password="=>"SomePass", "--proxy="=>"1.2.3.4", "--proxyUser="=>"ProxyUser", "--proxyPassword="=>"ProxyPass"}} } 35 | 36 | it "with server_url" do 37 | run_params.store_path(:params, "--systemorgid=", "2") 38 | run_params.store_path(:params, "--serverUrl=", "https://server.url") 39 | run_params.store_path(:params, "--sslCACert=", "/usr/share/rhn/RHN-ORG-TRUSTED-SSL-CERT") 40 | base_options.store_path(:server_url, "https://server.url") 41 | 42 | expect(LinuxAdmin::Common).to receive(:run!).once.with("rhnreg_ks", run_params) 43 | expect(LinuxAdmin::Rpm).to receive(:upgrade).with("http://server.url/pub/rhn-org-trusted-ssl-cert-1.0-1.noarch.rpm") 44 | expect(LinuxAdmin::Rpm).to receive(:list_installed).and_return({"rhn-org-trusted-ssl-cert" => "1.0"}) 45 | 46 | described_class.new.register(base_options) 47 | end 48 | 49 | it "without server_url" do 50 | expect(LinuxAdmin::Common).to receive(:run!).once.with("rhnreg_ks", run_params) 51 | expect_any_instance_of(described_class).not_to receive(:install_server_certificate) 52 | expect(LinuxAdmin::Rpm).to receive(:list_installed).and_return({"rhn-org-trusted-ssl-cert" => nil}) 53 | 54 | described_class.new.register(base_options) 55 | end 56 | end 57 | 58 | it "with activation key" do 59 | expect(LinuxAdmin::Common).to receive(:run!).once.with("rhnreg_ks", 60 | :params => {"--activationkey=" => "123abc", 61 | "--proxy=" => "1.2.3.4", 62 | "--proxyUser=" => "ProxyUser", 63 | "--proxyPassword=" => "ProxyPass"}) 64 | expect(LinuxAdmin::Rpm).to receive(:list_installed).and_return({"rhn-org-trusted-ssl-cert" => nil}) 65 | 66 | described_class.new.register( 67 | :activationkey => "123abc", 68 | :proxy_address => "1.2.3.4", 69 | :proxy_username => "ProxyUser", 70 | :proxy_password => "ProxyPass", 71 | ) 72 | end 73 | end 74 | 75 | it "#enable_channel" do 76 | expect(LinuxAdmin::Common).to receive(:run!).once.with("rhn-channel -a", 77 | :params => {"--user=" => "SomeUser", 78 | "--password=" => "SomePass", 79 | "--channel=" => 123}) 80 | 81 | described_class.new.enable_channel(123, :username => "SomeUser", :password => "SomePass") 82 | end 83 | 84 | it "#enabled_channels" do 85 | expect(LinuxAdmin::Common).to receive(:run!).once.with("rhn-channel -l") 86 | .and_return(double(:output => sample_output("rhn/output_rhn-channel_list"))) 87 | 88 | expect(described_class.new.enabled_channels).to eq(["rhel-x86_64-server-6", "rhel-x86_64-server-6-cf-me-2"]) 89 | end 90 | 91 | it "#disable_channel" do 92 | expect(LinuxAdmin::Common).to receive(:run!).once.with("rhn-channel -r", :params => {"--user=" => "SomeUser", 93 | "--password=" => "SomePass", 94 | "--channel=" => 123}) 95 | 96 | described_class.new.disable_channel(123, :username => "SomeUser", :password => "SomePass") 97 | end 98 | 99 | it "#available_channels" do 100 | credentials = { 101 | :username => "some_user", 102 | :password => "password" 103 | } 104 | expected = [ 105 | "rhel-x86_64-server-6-cf-me-2", 106 | "rhel-x86_64-server-6-cf-me-2-beta", 107 | "rhel-x86_64-server-6-cf-me-3", 108 | "rhel-x86_64-server-6-cf-me-3-beta" 109 | ] 110 | cmd = "rhn-channel -L" 111 | params = { 112 | :params => { 113 | "--user=" => "some_user", 114 | "--password=" => "password" 115 | } 116 | } 117 | 118 | expect(LinuxAdmin::Common).to receive(:run!).once.with(cmd, params) 119 | .and_return(double(:output => sample_output("rhn/output_rhn-channel_list_available"))) 120 | 121 | expect(described_class.new.available_channels(credentials)).to eq(expected) 122 | end 123 | 124 | it "#all_repos" do 125 | credentials = { 126 | :username => "some_user", 127 | :password => "password" 128 | } 129 | expected = [ 130 | {:repo_id => "rhel-x86_64-server-6-cf-me-2", :enabled => true}, 131 | {:repo_id => "rhel-x86_64-server-6-cf-me-2-beta", :enabled => false}, 132 | {:repo_id => "rhel-x86_64-server-6-cf-me-3", :enabled => false}, 133 | {:repo_id => "rhel-x86_64-server-6-cf-me-3-beta", :enabled => false}, 134 | {:repo_id => "rhel-x86_64-server-6", :enabled => true} 135 | ] 136 | 137 | expect(LinuxAdmin::Common).to receive(:run!).once 138 | .and_return(double(:output => sample_output("rhn/output_rhn-channel_list_available"))) 139 | expect(LinuxAdmin::Common).to receive(:run!).once.with("rhn-channel -l") 140 | .and_return(double(:output => sample_output("rhn/output_rhn-channel_list"))) 141 | 142 | expect(described_class.new.all_repos(credentials)).to eq(expected) 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /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, nil, 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", 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("", "", "", 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("", "", "", 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", 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 | parse_conf 16 | end 17 | 18 | # Parses the interface configuration file into the @interface_config hash 19 | def parse_conf 20 | @interface_config = {} 21 | 22 | if @interface_file.file? 23 | File.foreach(@interface_file) do |line| 24 | next if line =~ /^\s*#/ 25 | 26 | key, value = line.split('=').collect(&:strip) 27 | @interface_config[key] = value 28 | end 29 | end 30 | 31 | @interface_config["NM_CONTROLLED"] = "no" 32 | end 33 | 34 | # Set the IPv4 address for this interface 35 | # 36 | # @param address [String] 37 | # @raise ArgumentError if the address is not formatted properly 38 | def address=(address) 39 | validate_ip(address) 40 | @interface_config["BOOTPROTO"] = "static" 41 | @interface_config["IPADDR"] = address 42 | end 43 | 44 | # Set the IPv6 address for this interface 45 | # 46 | # @param address [String] IPv6 address including the prefix length (i.e. '::1/127') 47 | # @raise ArgumentError if the address is not formatted properly 48 | def address6=(address) 49 | validate_ip(address) 50 | @interface_config['IPV6INIT'] = 'yes' 51 | @interface_config['DHCPV6C'] = 'no' 52 | @interface_config['IPV6ADDR'] = address 53 | end 54 | 55 | # Set the IPv4 gateway address for this interface 56 | # 57 | # @param address [String] 58 | # @raise ArgumentError if the address is not formatted properly 59 | def gateway=(address) 60 | validate_ip(address) 61 | @interface_config["GATEWAY"] = address 62 | end 63 | 64 | # Set the IPv6 gateway address for this interface 65 | # 66 | # @param address [String] IPv6 address optionally including the prefix length 67 | # @raise ArgumentError if the address is not formatted properly 68 | def gateway6=(address) 69 | validate_ip(address) 70 | @interface_config['IPV6_DEFAULTGW'] = address 71 | end 72 | 73 | # Set the IPv4 sub-net mask for this interface 74 | # 75 | # @param mask [String] 76 | # @raise ArgumentError if the mask is not formatted properly 77 | def netmask=(mask) 78 | validate_ip(mask) 79 | @interface_config["NETMASK"] = mask 80 | end 81 | 82 | # Sets one or both DNS servers for this network interface 83 | # 84 | # @param servers [Array] The DNS servers 85 | def dns=(*servers) 86 | server1, server2 = servers.flatten 87 | @interface_config["DNS1"] = server1 88 | @interface_config["DNS2"] = server2 if server2 89 | end 90 | 91 | # Sets the search domain list for this network interface 92 | # 93 | # @param domains [Array] the list of search domains 94 | def search_order=(*domains) 95 | @interface_config["DOMAIN"] = "\"#{domains.flatten.join(' ')}\"" 96 | end 97 | 98 | # Set up the interface to use DHCP 99 | # Removes any previously set static IPv4 networking information 100 | def enable_dhcp 101 | @interface_config["BOOTPROTO"] = "dhcp" 102 | @interface_config.delete("IPADDR") 103 | @interface_config.delete("NETMASK") 104 | @interface_config.delete("GATEWAY") 105 | @interface_config.delete("PREFIX") 106 | @interface_config.delete("DNS1") 107 | @interface_config.delete("DNS2") 108 | @interface_config.delete("DOMAIN") 109 | end 110 | 111 | # Set up the interface to use DHCPv6 112 | # Removes any previously set static IPv6 networking information 113 | def enable_dhcp6 114 | @interface_config['IPV6INIT'] = 'yes' 115 | @interface_config['DHCPV6C'] = 'yes' 116 | @interface_config.delete('IPV6ADDR') 117 | @interface_config.delete('IPV6_DEFAULTGW') 118 | @interface_config.delete("DNS1") 119 | @interface_config.delete("DNS2") 120 | @interface_config.delete("DOMAIN") 121 | end 122 | 123 | # Applies the given static network configuration to the interface 124 | # 125 | # @param ip [String] IPv4 address 126 | # @param mask [String] subnet mask 127 | # @param gw [String] gateway address 128 | # @param dns [Array] list of dns servers 129 | # @param search [Array] list of search domains 130 | # @return [Boolean] true on success, false otherwise 131 | # @raise ArgumentError if an IP is not formatted properly 132 | def apply_static(ip, mask, gw, dns, search = nil) 133 | self.address = ip 134 | self.netmask = mask 135 | self.gateway = gw 136 | self.dns = dns 137 | self.search_order = search if search 138 | save 139 | end 140 | 141 | # Applies the given static IPv6 network configuration to the interface 142 | # 143 | # @param ip [String] IPv6 address 144 | # @param prefix [Number] prefix length for IPv6 address 145 | # @param gw [String] gateway address 146 | # @param dns [Array] list of dns servers 147 | # @param search [Array] list of search domains 148 | # @return [Boolean] true on success, false otherwise 149 | # @raise ArgumentError if an IP is not formatted properly or interface does not start 150 | def apply_static6(ip, prefix, gw, dns, search = nil) 151 | self.address6 = "#{ip}/#{prefix}" 152 | self.gateway6 = gw 153 | self.dns = dns 154 | self.search_order = search if search 155 | save 156 | end 157 | 158 | # Writes the contents of @interface_config to @interface_file as `key`=`value` pairs 159 | # and resets the interface 160 | # 161 | # @return [Boolean] true if the interface was successfully brought up with the 162 | # new configuration, false otherwise 163 | def save 164 | old_contents = @interface_file.file? ? File.read(@interface_file) : "" 165 | 166 | stop_success = stop 167 | # Stop twice because when configure both ipv4 and ipv6 as dhcp, ipv6 dhcp client will 168 | # exit and leave a /var/run/dhclient6-eth0.pid file. Then stop (ifdown eth0) will try 169 | # to kill this exited process so it returns 1. In the second call, this `.pid' file 170 | # has been deleted and ifdown returns 0. 171 | # See: https://bugzilla.redhat.com/show_bug.cgi?id=1472396 172 | stop_success = stop unless stop_success 173 | return false unless stop_success 174 | 175 | File.write(@interface_file, @interface_config.delete_blanks.collect { |k, v| "#{k}=#{v}" }.join("\n")) 176 | 177 | unless start 178 | File.write(@interface_file, old_contents) 179 | start 180 | return false 181 | end 182 | 183 | reload 184 | end 185 | 186 | def self.path_to_interface_config_file(interface) 187 | Pathname.new(IFACE_DIR).join("ifcfg-#{interface}") 188 | end 189 | 190 | private 191 | 192 | # Validate that the given address is formatted correctly 193 | # 194 | # @param ip [String] 195 | # @raise ArgumentError if the address is not correctly formatted 196 | def validate_ip(ip) 197 | IPAddr.new(ip) 198 | rescue ArgumentError 199 | raise ArgumentError, "#{ip} is not a valid IPv4 or IPv6 address" 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /lib/linux_admin/network_interface.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddr' 2 | 3 | module LinuxAdmin 4 | class NetworkInterface 5 | # Cached class instance variable for what distro we are running on 6 | @dist_class = nil 7 | 8 | # Gets the subclass specific to the local Linux distro 9 | # 10 | # @param clear_cache [Boolean] Determines if the cached value will be reevaluated 11 | # @return [Class] The proper class to be used 12 | def self.dist_class(clear_cache = false) 13 | @dist_class = nil if clear_cache 14 | @dist_class ||= begin 15 | if [Distros.rhel, Distros.fedora].include?(Distros.local) 16 | NetworkInterfaceRH 17 | else 18 | NetworkInterfaceGeneric 19 | end 20 | end 21 | end 22 | 23 | # Creates an instance of the correct NetworkInterface subclass for the local distro 24 | def self.new(*args) 25 | self == LinuxAdmin::NetworkInterface ? dist_class.new(*args) : super 26 | end 27 | 28 | # @return [String] the interface for networking operations 29 | attr_reader :interface 30 | 31 | # @param interface [String] Name of the network interface to manage 32 | def initialize(interface) 33 | @interface = interface 34 | reload 35 | end 36 | 37 | # Gathers current network information for this interface 38 | # 39 | # @return [Boolean] true if network information was gathered successfully 40 | def reload 41 | @network_conf = {} 42 | begin 43 | ip_output = ip_show 44 | rescue NetworkInterfaceError 45 | return false 46 | end 47 | 48 | parse_ip4(ip_output) 49 | parse_ip6(ip_output, :global) 50 | parse_ip6(ip_output, :link) 51 | 52 | @network_conf[:mac] = parse_ip_output(ip_output, %r{link/ether}, 1) 53 | 54 | [4, 6].each do |version| 55 | @network_conf["gateway#{version}".to_sym] = parse_ip_output(ip_route(version), /^default/, 2) 56 | end 57 | true 58 | end 59 | 60 | # Retrieve the IPv4 address assigned to the interface 61 | # 62 | # @return [String] IPv4 address for the managed interface 63 | def address 64 | @network_conf[:address] 65 | end 66 | 67 | # Retrieve the IPv6 address assigned to the interface 68 | # 69 | # @return [String] IPv6 address for the managed interface 70 | # @raise [ArgumentError] if the given scope is not `:global` or `:link` 71 | def address6(scope = :global) 72 | case scope 73 | when :global 74 | @network_conf[:address6_global] 75 | when :link 76 | @network_conf[:address6_link] 77 | else 78 | raise ArgumentError, "Unrecognized address scope #{scope}" 79 | end 80 | end 81 | 82 | # Retrieve the MAC address associated with the interface 83 | # 84 | # @return [String] the MAC address 85 | def mac_address 86 | @network_conf[:mac] 87 | end 88 | 89 | # Retrieve the IPv4 sub-net mask assigned to the interface 90 | # 91 | # @return [String] IPv4 netmask 92 | def netmask 93 | @network_conf[:mask] ||= IPAddr.new('255.255.255.255').mask(prefix).to_s if prefix 94 | end 95 | 96 | # Retrieve the IPv6 sub-net mask assigned to the interface 97 | # 98 | # @return [String] IPv6 netmask 99 | # @raise [ArgumentError] if the given scope is not `:global` or `:link` 100 | def netmask6(scope = :global) 101 | if [:global, :link].include?(scope) 102 | @network_conf["mask6_#{scope}".to_sym] ||= IPAddr.new('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff').mask(prefix6(scope)).to_s if prefix6(scope) 103 | else 104 | raise ArgumentError, "Unrecognized address scope #{scope}" 105 | end 106 | end 107 | 108 | # Retrieve the IPv4 sub-net prefix length assigned to the interface 109 | # 110 | # @return [Numeric] IPv4 prefix length 111 | def prefix 112 | @network_conf[:prefix] 113 | end 114 | 115 | # Retrieve the IPv6 sub-net prefix length assigned to the interface 116 | # 117 | # @return [Numeric] IPv6 prefix length 118 | def prefix6(scope = :global) 119 | if [:global, :link].include?(scope) 120 | @network_conf["prefix6_#{scope}".to_sym] 121 | else 122 | raise ArgumentError, "Unrecognized address scope #{scope}" 123 | end 124 | end 125 | 126 | # Retrieve the IPv4 default gateway associated with the interface 127 | # 128 | # @return [String] IPv4 gateway address 129 | def gateway 130 | @network_conf[:gateway4] 131 | end 132 | 133 | # Retrieve the IPv6 default gateway associated with the interface 134 | # 135 | # @return [String] IPv6 gateway address 136 | def gateway6 137 | @network_conf[:gateway6] 138 | end 139 | 140 | # Brings up the network interface 141 | # 142 | # @return [Boolean] whether the command succeeded or not 143 | def start 144 | Common.run(Common.cmd("ifup"), :params => [@interface]).success? 145 | end 146 | 147 | # Brings down the network interface 148 | # 149 | # @return [Boolean] whether the command succeeded or not 150 | def stop 151 | Common.run(Common.cmd("ifdown"), :params => [@interface]).success? 152 | end 153 | 154 | private 155 | 156 | # Parses the output of `ip addr show` 157 | # 158 | # @param output [String] The command output 159 | # @param regex [Regexp] Regular expression to match the desired output line 160 | # @param col [Fixnum] The whitespace delimited column to be returned 161 | # @return [String] The parsed data 162 | def parse_ip_output(output, regex, col) 163 | the_line = output.split("\n").detect { |l| l =~ regex } 164 | the_line.nil? ? nil : the_line.strip.split(' ')[col] 165 | end 166 | 167 | # Runs the command `ip addr show ` 168 | # 169 | # @return [String] The command output 170 | # @raise [NetworkInterfaceError] if the command fails 171 | def ip_show 172 | Common.run!(Common.cmd("ip"), :params => ["addr", "show", @interface]).output 173 | rescue AwesomeSpawn::CommandResultError => e 174 | raise NetworkInterfaceError.new(e.message, e.result) 175 | end 176 | 177 | # Runs the command `ip -[4/6] route` and returns the output 178 | # 179 | # @param version [Fixnum] Version of IP protocol (4 or 6) 180 | # @return [String] The command output 181 | # @raise [NetworkInterfaceError] if the command fails 182 | def ip_route(version) 183 | Common.run!(Common.cmd("ip"), :params => ["-#{version}", 'route']).output 184 | rescue AwesomeSpawn::CommandResultError => e 185 | raise NetworkInterfaceError.new(e.message, e.result) 186 | end 187 | 188 | # Parses the IPv4 information from the output of `ip addr show ` 189 | # 190 | # @param ip_output [String] The command output 191 | def parse_ip4(ip_output) 192 | cidr_ip = parse_ip_output(ip_output, /inet /, 1) 193 | return unless cidr_ip 194 | 195 | parts = cidr_ip.split('/') 196 | @network_conf[:address] = parts[0] 197 | @network_conf[:prefix] = parts[1].to_i 198 | end 199 | 200 | # Parses the IPv6 information from the output of `ip addr show ` 201 | # 202 | # @param ip_output [String] The command output 203 | # @param scope [Symbol] The IPv6 scope (either `:global` or `:local`) 204 | def parse_ip6(ip_output, scope) 205 | cidr_ip = parse_ip_output(ip_output, /inet6 .* scope #{scope}/, 1) 206 | return unless cidr_ip 207 | 208 | parts = cidr_ip.split('/') 209 | @network_conf["address6_#{scope}".to_sym] = parts[0] 210 | @network_conf["prefix6_#{scope}".to_sym] = parts[1].to_i 211 | end 212 | end 213 | end 214 | 215 | Dir.glob(File.join(File.dirname(__FILE__), "network_interface", "*.rb")).each { |f| require f } 216 | -------------------------------------------------------------------------------- /spec/subscription_manager_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::SubscriptionManager do 2 | context "#registered?" do 3 | it "system with subscription-manager commands" do 4 | expect(LinuxAdmin::Common).to receive(:run).once.with("subscription-manager identity") 5 | .and_return(double(:exit_status => 0)) 6 | expect(described_class.new.registered?).to be_truthy 7 | end 8 | 9 | it "system without subscription-manager commands" do 10 | expect(LinuxAdmin::Common).to receive(:run).once.with("subscription-manager identity") 11 | .and_return(double(:exit_status => 255)) 12 | expect(described_class.new.registered?).to be_falsey 13 | end 14 | end 15 | 16 | it "#refresh" do 17 | expect(LinuxAdmin::Common).to receive(:run!).once.with("subscription-manager refresh", {}) 18 | described_class.new.refresh 19 | end 20 | 21 | context "#register" do 22 | it "no username" do 23 | expect { described_class.new.register }.to raise_error(ArgumentError) 24 | end 25 | 26 | context "with username and password" do 27 | let(:base_options) { {:username => "SomeUser@SomeDomain.org", 28 | :password => "SomePass", 29 | :environment => "Library", 30 | :org => "IT", 31 | :proxy_address => "1.2.3.4", 32 | :proxy_username => "ProxyUser", 33 | :proxy_password => "ProxyPass", 34 | } 35 | } 36 | let(:run_params) { {:params=>{"--username="=>"SomeUser@SomeDomain.org", "--password="=>"SomePass", "--proxy="=>"1.2.3.4", "--proxyuser="=>"ProxyUser", "--proxypassword="=>"ProxyPass", "--environment="=>"Library"}} } 37 | 38 | it "with server_url" do 39 | run_params.store_path(:params, "--org=", "IT") 40 | base_options.store_path(:server_url, "https://server.url") 41 | 42 | expect(LinuxAdmin::Common).to receive(:run!).once.with("subscription-manager register", run_params) 43 | expect(LinuxAdmin::Rpm).to receive(:upgrade).with("http://server.url/pub/katello-ca-consumer-latest.noarch.rpm") 44 | 45 | described_class.new.register(base_options) 46 | end 47 | 48 | it "without server_url" do 49 | expect(LinuxAdmin::Common).to receive(:run!).once.with("subscription-manager register", run_params) 50 | expect_any_instance_of(described_class).not_to receive(:install_server_certificate) 51 | 52 | described_class.new.register(base_options) 53 | end 54 | end 55 | end 56 | 57 | context "#subscribe" do 58 | it "with pools" do 59 | expect(LinuxAdmin::Common).to receive(:run!).once 60 | .with("subscription-manager attach", :params => [["--pool", 123], ["--pool", 456]]) 61 | described_class.new.subscribe({:pools => [123, 456]}) 62 | end 63 | 64 | it "without pools" do 65 | expect(LinuxAdmin::Common).to receive(:run!).once 66 | .with("subscription-manager attach", :params => {"--auto" => nil}) 67 | described_class.new.subscribe({}) 68 | end 69 | end 70 | 71 | context "#subscribed_products" do 72 | it "subscribed" do 73 | expect(LinuxAdmin::Common).to receive(:run!).once.with("subscription-manager list --installed", {}) 74 | .and_return(double(:output => sample_output("subscription_manager/output_list_installed_subscribed"))) 75 | expect(described_class.new.subscribed_products).to eq(["69", "167"]) 76 | end 77 | 78 | it "not subscribed" do 79 | expect(LinuxAdmin::Common).to receive(:run!).once.with("subscription-manager list --installed", {}) 80 | .and_return(double(:output => sample_output("subscription_manager/output_list_installed_not_subscribed"))) 81 | expect(described_class.new.subscribed_products).to eq(["167"]) 82 | end 83 | end 84 | 85 | it "#available_subscriptions" do 86 | expect(LinuxAdmin::Common).to receive(:run!).once.with("subscription-manager list --all --available", {}) 87 | .and_return(double(:output => sample_output("subscription_manager/output_list_all_available"))) 88 | expect(described_class.new.available_subscriptions).to eq({ 89 | "82c042fca983889b10178893f29b06e3" => { 90 | :subscription_name => "Example Subscription", 91 | :sku => "SER0123", 92 | :pool_id => "82c042fca983889b10178893f29b06e3", 93 | :quantity => "1690", 94 | :service_level => "None", 95 | :service_type => "None", 96 | :multi_entitlement => "No", 97 | :ends => Date.parse("2022-01-01"), 98 | :system_type => "Physical", 99 | }, 100 | "4f738052ec866192c775c62f408ab868" => { 101 | :subscription_name => "My Private Subscription", 102 | :sku => "SER9876", 103 | :pool_id => "4f738052ec866192c775c62f408ab868", 104 | :quantity => "Unlimited", 105 | :service_level => "None", 106 | :service_type => "None", 107 | :multi_entitlement => "No", 108 | :ends => Date.parse("2013-06-04"), 109 | :system_type => "Virtual", 110 | }, 111 | "3d81297f352305b9a3521981029d7d83" => { 112 | :subscription_name => "Shared Subscription - With other characters, (2 sockets) (Up to 1 guest)", 113 | :sku => "RH0123456", 114 | :pool_id => "3d81297f352305b9a3521981029d7d83", 115 | :quantity => "1", 116 | :service_level => "Self-support", 117 | :service_type => "L1-L3", 118 | :multi_entitlement => "No", 119 | :ends => Date.parse("2013-05-15"), 120 | :system_type => "Virtual", 121 | }, 122 | "87cefe63b67984d5c7e401d833d2f87f" => { 123 | :subscription_name => "Example Subscription, Premium (up to 2 sockets) 3 year", 124 | :sku => "MCT0123A9", 125 | :pool_id => "87cefe63b67984d5c7e401d833d2f87f", 126 | :quantity => "1", 127 | :service_level => "Premium", 128 | :service_type => "L1-L3", 129 | :multi_entitlement => "No", 130 | :ends => Date.parse("2013-07-05"), 131 | :system_type => "Virtual", 132 | }, 133 | }) 134 | end 135 | 136 | context "#organizations" do 137 | it "with valid credentials" do 138 | run_options = ["subscription-manager orgs", {:params=>{"--username="=>"SomeUser", "--password="=>"SomePass", "--proxy="=>"1.2.3.4", "--proxyuser="=>"ProxyUser", "--proxypassword="=>"ProxyPass"}}] 139 | 140 | expect(LinuxAdmin::Rpm).to receive(:upgrade).with("http://192.168.1.1/pub/katello-ca-consumer-latest.noarch.rpm") 141 | expect(LinuxAdmin::Common).to receive(:run!).once.with(*run_options) 142 | .and_return(double(:output => sample_output("subscription_manager/output_orgs"))) 143 | 144 | expect(described_class.new.organizations({:username=>"SomeUser", :password=>"SomePass", :proxy_address=>"1.2.3.4", :proxy_username=>"ProxyUser", :proxy_password=>"ProxyPass", :server_url=>"192.168.1.1"})).to eq({"SomeOrg"=>{:name=>"SomeOrg", :key=>"1234567"}}) 145 | end 146 | 147 | it "with invalid credentials" do 148 | run_options = ["subscription-manager orgs", {:params=>{"--username="=>"BadUser", "--password="=>"BadPass"}}] 149 | error = AwesomeSpawn::CommandResultError.new("", 150 | double( 151 | :error => "Invalid username or password. To create a login, please visit https://www.redhat.com/wapps/ugc/register.html", 152 | :exit_status => 255 153 | ) 154 | ) 155 | expect(AwesomeSpawn).to receive(:run!).once.with(*run_options).and_raise(error) 156 | expect { described_class.new.organizations({:username=>"BadUser", :password=>"BadPass"}) }.to raise_error(LinuxAdmin::CredentialError) 157 | end 158 | end 159 | 160 | it "#enable_repo" do 161 | expect(LinuxAdmin::Common).to receive(:run!).once 162 | .with("subscription-manager repos", :params => {"--enable=" => "abc"}) 163 | 164 | described_class.new.enable_repo("abc") 165 | end 166 | 167 | it "#disable_repo" do 168 | expect(LinuxAdmin::Common).to receive(:run!).once 169 | .with("subscription-manager repos", :params => {"--disable=" => "abc"}) 170 | 171 | described_class.new.disable_repo("abc") 172 | end 173 | 174 | it "#all_repos" do 175 | expected = [ 176 | { 177 | :repo_id => "some-repo-source-rpms", 178 | :repo_name => "Some Repo (Source RPMs)", 179 | :repo_url => "https://my.host.example.com/repos/some-repo/source/rpms", 180 | :enabled => true 181 | }, 182 | { 183 | :repo_id => "some-repo-rpms", 184 | :repo_name => "Some Repo", 185 | :repo_url => "https://my.host.example.com/repos/some-repo/rpms", 186 | :enabled => true 187 | }, 188 | { 189 | :repo_id => "some-repo-2-beta-rpms", 190 | :repo_name => "Some Repo (Beta RPMs)", 191 | :repo_url => "https://my.host.example.com/repos/some-repo-2/beta/rpms", 192 | :enabled => false 193 | } 194 | ] 195 | 196 | expect(LinuxAdmin::Common).to receive(:run!).once.with("subscription-manager repos", {}) 197 | .and_return(double(:output => sample_output("subscription_manager/output_repos"))) 198 | 199 | expect(described_class.new.all_repos).to eq(expected) 200 | end 201 | 202 | it "#enabled_repos" do 203 | expected = ["some-repo-source-rpms", "some-repo-rpms"] 204 | 205 | expect(LinuxAdmin::Common).to receive(:run!).once.with("subscription-manager repos", {}) 206 | .and_return(double(:output => sample_output("subscription_manager/output_repos"))) 207 | 208 | expect(described_class.new.enabled_repos).to eq(expected) 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /spec/disk_spec.rb: -------------------------------------------------------------------------------- 1 | describe LinuxAdmin::Disk do 2 | describe "#local" do 3 | it "returns local disks" do 4 | expect(Dir).to receive(:glob).with(['/dev/[vhs]d[a-z]', '/dev/xvd[a-z]']). 5 | and_return(['/dev/hda', '/dev/sda']) 6 | disks = LinuxAdmin::Disk.local 7 | paths = disks.collect { |disk| disk.path } 8 | expect(paths).to include('/dev/hda') 9 | expect(paths).to include('/dev/sda') 10 | end 11 | end 12 | 13 | describe "#size" do 14 | it "uses fdisk" do 15 | disk = LinuxAdmin::Disk.new :path => '/dev/hda' 16 | expect(LinuxAdmin::Common).to receive(:run!) 17 | .with(LinuxAdmin::Common.cmd(:fdisk), 18 | :params => {"-l" => nil}) 19 | .and_return(double(:output => "")) 20 | disk.size 21 | end 22 | 23 | it "returns disk size" do 24 | fdisk = < '/dev/hda' 39 | allow(LinuxAdmin::Common).to receive(:run!).and_return(double(:output => fdisk)) 40 | expect(disk.size).to eq(500_107_862_016) 41 | end 42 | end 43 | 44 | describe "#partitions" do 45 | it "uses parted" do 46 | disk = LinuxAdmin::Disk.new :path => '/dev/hda' 47 | expect(LinuxAdmin::Common).to receive(:run) 48 | .with(LinuxAdmin::Common.cmd(:parted), 49 | :params => {nil => %w(--script /dev/hda print)}).and_return(double(:output => "")) 50 | disk.partitions 51 | end 52 | 53 | it "returns [] on non-zero parted rc" do 54 | disk = LinuxAdmin::Disk.new :path => '/dev/hda' 55 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:output => "", :exit_status => 1)) 56 | expect(disk.partitions).to eq([]) 57 | end 58 | 59 | it "sets partitons" do 60 | partitions = < partitions)) 74 | 75 | expect(disk.partitions[0].id).to eq(1) 76 | expect(disk.partitions[0].disk).to eq(disk) 77 | expect(disk.partitions[0].size).to eq(86_436_216_832.0) # 80.5.gigabytes 78 | expect(disk.partitions[0].start_sector).to eq(1_320_157_184) # 1259.megabytes 79 | expect(disk.partitions[0].end_sector).to eq(87_832_081_203.2) # 81.8.gigabytes 80 | expect(disk.partitions[0].partition_type).to eq('primary') 81 | expect(disk.partitions[0].fs_type).to eq('ntfs') 82 | expect(disk.partitions[1].id).to eq(2) 83 | expect(disk.partitions[1].disk).to eq(disk) 84 | expect(disk.partitions[1].size).to eq(86_436_216_832.0) # 80.5.gigabytes 85 | expect(disk.partitions[1].start_sector).to eq(87_832_081_203.2) # 81.8.gigabytes 86 | expect(disk.partitions[1].end_sector).to eq(173_946_175_488) # 162.gigabytes 87 | expect(disk.partitions[1].partition_type).to eq('primary') 88 | expect(disk.partitions[1].fs_type).to eq('ext4') 89 | expect(disk.partitions[2].id).to eq(3) 90 | expect(disk.partitions[2].disk).to eq(disk) 91 | expect(disk.partitions[2].size).to eq(1_126_170_624) # 1074.megabytes 92 | expect(disk.partitions[2].start_sector).to eq(173_946_175_488) # 162.gigabytes 93 | expect(disk.partitions[2].end_sector).to eq(175_019_917_312) # 163.gigabytes 94 | expect(disk.partitions[2].partition_type).to eq('logical') 95 | expect(disk.partitions[2].fs_type).to eq('linux-swap(v1)') 96 | end 97 | end 98 | 99 | describe "#create_partitions" do 100 | before(:each) do 101 | @disk = LinuxAdmin::Disk.new(:path => '/dev/hda') 102 | end 103 | 104 | it "dispatches to create_partition" do 105 | expect(@disk).to receive(:create_partition).with("primary", "0%", "50%") 106 | @disk.create_partitions "primary", :start => "0%", :end => "50%" 107 | end 108 | 109 | context "multiple partitions specified" do 110 | it "calls create_partition for each partition" do 111 | expect(@disk).to receive(:create_partition).with("primary", "0%", "49%") 112 | expect(@disk).to receive(:create_partition).with("primary", "50%", "100%") 113 | @disk.create_partitions("primary", {:start => "0%", :end => "49%"}, 114 | {:start => "50%", :end => "100%"}) 115 | end 116 | 117 | context "partitions overlap" do 118 | it "raises argument error" do 119 | expect{ 120 | @disk.create_partitions("primary", {:start => "0%", :end => "50%"}, 121 | {:start => "49%", :end => "100%"}) 122 | }.to raise_error(ArgumentError) 123 | end 124 | end 125 | end 126 | end 127 | 128 | describe "#create_partition" do 129 | before(:each) do 130 | # test disk w/ existing partition 131 | @disk = LinuxAdmin::Disk.new :path => '/dev/hda' 132 | @disk.instance_variable_set(:@partitions, 133 | [LinuxAdmin::Partition.new(:id => 1, 134 | :end_sector => 1024)]) 135 | allow(@disk).to receive_messages(:has_partition_table? => true) 136 | end 137 | 138 | it "uses parted" do 139 | params = ['--script', '/dev/hda', 'mkpart', '-a', 'opt', 'primary', 1024, 2048] 140 | expect(LinuxAdmin::Common).to receive(:run!).with(LinuxAdmin::Common.cmd(:parted), :params => {nil => params}) 141 | @disk.create_partition 'primary', 1024 142 | end 143 | 144 | it "accepts start/end params" do 145 | params = ['--script', '/dev/hda', 'mkpart', '-a', 'opt', 'primary', "0%", "50%"] 146 | expect(LinuxAdmin::Common).to receive(:run!).with(LinuxAdmin::Common.cmd(:parted), :params => {nil => params}) 147 | @disk.create_partition 'primary', "0%", "50%" 148 | end 149 | 150 | context "missing params" do 151 | it "raises ArgumentError" do 152 | expect{ 153 | @disk.create_partition 'primary' 154 | }.to raise_error(ArgumentError) 155 | 156 | expect{ 157 | @disk.create_partition 'primary', '0%', '50%', 100 158 | }.to raise_error(ArgumentError) 159 | end 160 | end 161 | 162 | it "returns partition" do 163 | expect(LinuxAdmin::Common).to receive(:run!) # stub out call to parted 164 | partition = @disk.create_partition 'primary', 1024 165 | expect(partition).to be_an_instance_of(LinuxAdmin::Partition) 166 | end 167 | 168 | it "increments partition id" do 169 | expect(LinuxAdmin::Common).to receive(:run!) # stub out call to parted 170 | partition = @disk.create_partition 'primary', 1024 171 | expect(partition.id).to eq(2) 172 | end 173 | 174 | it "sets partition start to first unused sector on disk" do 175 | expect(LinuxAdmin::Common).to receive(:run!) # stub out call to parted 176 | partition = @disk.create_partition 'primary', 1024 177 | expect(partition.start_sector).to eq(1024) 178 | end 179 | 180 | it "stores new partition locally" do 181 | expect(LinuxAdmin::Common).to receive(:run!) # stub out call to parted 182 | expect { 183 | @disk.create_partition 'primary', 1024 184 | }.to change{@disk.partitions.size}.by(1) 185 | end 186 | 187 | it "creates partition table if missing" do 188 | allow(@disk).to receive_messages(:has_partition_table? => false) 189 | expect(@disk).to receive(:create_partition_table) 190 | expect(LinuxAdmin::Common).to receive(:run!) 191 | @disk.create_partition 'primary', 1024 192 | end 193 | end 194 | 195 | describe "#has_partition_table?" do 196 | it "positive case" do 197 | disk = LinuxAdmin::Disk.new :path => '/dev/hda' 198 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:output => "", :exit_status => 0)) 199 | expect(disk).to have_partition_table 200 | end 201 | 202 | it "negative case" do 203 | disk = LinuxAdmin::Disk.new :path => '/dev/hda' 204 | output = "\e[?1034h\r\rError: /dev/sdb: unrecognised disk label\n" 205 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:output => output, :exit_status => 1)) 206 | expect(disk).not_to have_partition_table 207 | end 208 | end 209 | 210 | it "#create_partition_table" do 211 | disk = LinuxAdmin::Disk.new :path => '/dev/hda' 212 | options = {:params => {nil => %w(--script /dev/hda mklabel msdos)}} 213 | expect(LinuxAdmin::Common).to receive(:run!).with(LinuxAdmin::Common.cmd(:parted), options) 214 | disk.create_partition_table 215 | end 216 | 217 | describe "#clear!" do 218 | it "clears partitions" do 219 | disk = LinuxAdmin::Disk.new :path => '/dev/hda' 220 | expect(LinuxAdmin::Common).to receive(:run).and_return(double(:output => "")) # stub out call to cmds 221 | disk.partitions << LinuxAdmin::Partition.new 222 | 223 | expect(LinuxAdmin::Common).to receive(:run!) 224 | disk.clear! 225 | expect(disk.partitions).to be_empty 226 | end 227 | 228 | it "uses dd to clear partition table" do 229 | disk = LinuxAdmin::Disk.new :path => '/dev/hda' 230 | expect(LinuxAdmin::Common).to receive(:run!) 231 | .with(LinuxAdmin::Common.cmd(:dd), 232 | :params => {'if=' => '/dev/zero', 'of=' => '/dev/hda', 233 | 'bs=' => 512, 'count=' => 1}) 234 | disk.clear! 235 | end 236 | 237 | it "returns self" do 238 | disk = LinuxAdmin::Disk.new :path => '/dev/hda' 239 | allow(LinuxAdmin::Common).to receive(:run!) # stub out call to dd 240 | expect(disk.clear!).to eq(disk) 241 | end 242 | end 243 | 244 | end 245 | --------------------------------------------------------------------------------