doable 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8049e12988f84644e8ad77af4aa81c69586cd584
4
- data.tar.gz: cdde8ea4ff303b1cdd1407ce2a45f4155fad67f4
3
+ metadata.gz: 12cd4bee334b582b69a835051c8cfeb0314aa3e3
4
+ data.tar.gz: 84db114358f2ea2f7511900421206ed8ddcbe9d5
5
5
  SHA512:
6
- metadata.gz: 8e5e57273ae3eb3a16aafa7992a2513bc7eefada445a1feac54d53e55a4633001eeed83b7a7967a3dbced96d7868a7fb041db469c5ed968763c4023a2a2f8af4
7
- data.tar.gz: e13885ee5299ae731b16a9d1c83ec4868994a66cd2ed57e5bd0a8372b7758a3645e12c95e5ad4d0eab946e84d42c71af7da200b6d41caef7ccf20bebc9f56074
6
+ metadata.gz: 380841e88c0f7dcf73a6d9a85bf6d3bdb0941a519f8072e0957515c42a38b224fc3e8be81ed6be0b4c338a7ae071f3c55228f0877fb634bfb8322cf7c69f4745
7
+ data.tar.gz: 56aea29e8c7bfd238d806d8953ab456ef13239b9ec019d2316b22be8e2a747bf9620106a3f686d7d429c4446d79ef41e2b7adfec0e98ba8de829596deabdfccd
@@ -1,21 +1,21 @@
1
1
  module Doable
2
2
  module Exceptions
3
3
  module Framework
4
+ # Exception to be thrown when nothing else applies. Inherited by other exceptions.
4
5
  class GenericFrameworkError < StandardError
5
6
  end
6
7
 
8
+ # When the provided parameters or other input is not valid.
7
9
  class InvalidInput < GenericFrameworkError
8
10
  end
9
11
 
12
+ # Correct number of parameters is passed but it is not of the correct type.
13
+ # i.e., type error in strongly typed languages
10
14
  class NotApplicable < InvalidInput
11
15
  end
12
16
 
13
- class InvalidAction < GenericFrameworkError
14
- end
15
-
16
- class MissingDefinitionsDirectory < GenericFrameworkError
17
- end
18
-
17
+ # This can be called when a helper method is expecting a parameter that wasn't passed.
18
+ # i.e., an options hash doesn't contain the requisite elements.
19
19
  class MissingParameter < InvalidInput
20
20
  end
21
21
 
@@ -24,11 +24,13 @@ module Doable
24
24
  end
25
25
 
26
26
  # !These exceptions should never be caught by anything!
27
+ # This is an internal exception used primarily for logging purposes. Provides a clean way to log
27
28
  class SkipStep < StandardError
28
29
  end
29
30
 
31
+ # Allows the the application to notify the completion of a Rollback and that the job object was not successfully executed.
30
32
  class RolledBack < StandardError
31
33
  end
32
34
  end
33
35
  end
34
- end
36
+ end
@@ -0,0 +1,11 @@
1
+ module Doable
2
+ module Exceptions
3
+ module Linux
4
+ class MissingRPMs < StandardError
5
+ end
6
+
7
+ class InvalidRPMList < StandardError
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,6 +1,8 @@
1
1
  module Doable
2
2
  module Exceptions
3
+ # Exceptions which are agnostic to the underlying OS.
3
4
  module OS
5
+ # Generic exception to be inherited by all other OS-level exceptions.
4
6
  class OSException < GenericFrameworkError
5
7
  end
6
8
 
@@ -16,8 +18,50 @@ module Doable
16
18
  class InvalidHostname < OSException
17
19
  end
18
20
 
21
+ class FailedPasswordChange < OSException
22
+ end
23
+
19
24
  class MissingFile < OSException
20
25
  end
26
+
27
+ class MissingExecutable < OSException
28
+ end
29
+
30
+ class FailedCopy < OSException
31
+ end
32
+
33
+ class NotApplicable < OSException
34
+ end
35
+
36
+ class DuplicateUsers < OSException
37
+ end
38
+
39
+ class DuplicateGroups < OSException
40
+ end
41
+
42
+ class WrongHomedir < OSException
43
+ end
44
+
45
+ class InvalidArchitecture < OSException
46
+ end
47
+
48
+ class MissingUser < OSException
49
+ end
50
+
51
+ class WrongUser < OSException
52
+ end
53
+
54
+ class FailedGroupAdd < OSException
55
+ end
56
+
57
+ class FailedUserModification < OSException
58
+ end
59
+
60
+ class FailedUncompress < OSException
61
+ end
62
+
63
+ class AmbiguousProcess < OSException
64
+ end
21
65
  end
22
66
  end
23
- end
67
+ end
@@ -0,0 +1,296 @@
1
+ require 'doable/helpers/os'
2
+ require 'doable/exceptions/linux'
3
+
4
+ module Doable
5
+ module Helpers
6
+ module Linux
7
+ include OS
8
+ include Exceptions::Linux
9
+ # Used like Unix chown
10
+ # @param user [String] User to set as the owner
11
+ # @param group [String] Group to set as the owner
12
+ # @param file_list [Array<String>,String] List of files to change the owner of
13
+ def chown(user, group, file_list, options = {recursive: false})
14
+ if options[:recursive]
15
+ options.delete :recursive
16
+ FileUtils.chown_R(user, group, file_list, options)
17
+ else
18
+ options.delete(:recursive) if options.has_key?(:recursive)
19
+ FileUtils.chown(user, group, file_list, options)
20
+ end
21
+ end
22
+
23
+ # Used like Unix chmod
24
+ # @param permission [String, Fixnum] Permissions to set
25
+ # @param file_list [Array<String>,String] List of files to change the permissions of
26
+ def chmod(permission, file_list, options = {recursive: false})
27
+ if options[:recursive]
28
+ options.delete :recursive
29
+ FileUtils.chmod_R(permission, file_list, options)
30
+ else
31
+ options.delete(:recursive) if options.has_key?(:recursive)
32
+ FileUtils.chmod(permission, file_list, options)
33
+ end
34
+ end
35
+
36
+ # Apply a patch file
37
+ # @param options [String] Options to be added to the `patch` system command
38
+ # @param patch_file [String] Path to the patch file to apply
39
+ # @param path [String] Optionally cd to this directory before patching
40
+ # @param user [String] Optionally apply patch as this user
41
+ def apply_patch(options, patch_file, path = nil, user = nil)
42
+ cmd = "patch #{options} < #{patch_file}"
43
+
44
+ olddir = `pwd`.chomp
45
+ Dir.chdir(path) if path
46
+ tee 'pwd'
47
+ if user
48
+ run_as_user user, cmd
49
+ else
50
+ tee cmd
51
+ end
52
+ Dir.chdir(olddir) if path
53
+ end
54
+
55
+ # Add an entry to a user's ~/.bashrc
56
+ # @param user [String] User to add bashrc entries to
57
+ # @param entry [String] Entry to add to user's .bashrc
58
+ def add_bashrc_entry(user, entry)
59
+ File.open(File.expand_path("~#{user}/.bashrc"), "a") {|f| f.puts entry }
60
+ end
61
+
62
+ # Get a list of process matching a Regexp
63
+ # @param regex [Regexp] Regular expression to use for finding processes
64
+ # @return [Array<String>] Array of PIDs that match the regex
65
+ def find_processes(regex)
66
+ `ps aux`.chomp.split("\n").map {|l| l.split(nil, 11) }.collect {|p| p if p[10].match(regex)}.compact
67
+ end
68
+
69
+ # Kill a process
70
+ # @param pid [Fixnum,String] PID to kill
71
+ # @param signal [String] Signal to kill PID with
72
+ def kill(pid, signal = "TERM")
73
+ Process.kill(signal, pid)
74
+ end
75
+
76
+ # Does the system have an executable in $PATH? If not, return false.
77
+ # @param name [String] Executable to verify
78
+ # @return [String, false]
79
+ def have_executable?(name)
80
+ app_path = `which #{name}`.chomp
81
+ if app_path.match(name)
82
+ return app_path
83
+ else
84
+ return false
85
+ end
86
+ end
87
+
88
+ # Require an executable to be in $PATH
89
+ # @raise [MissingExecutable]
90
+ # @param name [String] Required executable
91
+ def find_executable(name)
92
+ have_executable?(name) ? true : raise(MissingExecutable, name)
93
+ end
94
+
95
+ # Check for RPMs (kind of RHEL / SuSE specific)
96
+ # @raise [InvalidRPMList]
97
+ # @raise [NotApplicable]
98
+ # @raise [MissingRPMs]
99
+ # @param rpms [Array<String>] RPMs to verify
100
+ # #return [Boolean]
101
+ def check_for_rpms(rpms)
102
+ raise InvalidRPMList unless rpms.kind_of?(Array)
103
+ # Split the list of all installed RPMs into an Array containing only the RPM's base name
104
+ system_rpms = `rpm -qa`.chomp.split("\n").collect {|rpm| rpm.split(/-[0-9]/)[0]}
105
+ i686_rpms = `rpm -qa`.chomp.split("\n").collect {|rpm| rpm.split(/-[0-9]/)[0] if rpm.match(/\.i686$/)}.compact
106
+ raise NotApplicable unless $?.success?
107
+ missing_rpms = rpms.collect {|rpm| rpm unless rpm.match(/\.i686$/) }.compact - system_rpms # calculate what RPMs are not installed
108
+ missing_rpms = (missing_rpms + (rpms.collect {|rpm| rpm.split('.')[0] if rpm.match(/\.i686$/) }.compact - i686_rpms).compact.map {|r| "#{r}.i686" } ).compact
109
+ if missing_rpms.size > 0
110
+ log "Missing RPMs: #{missing_rpms.join(', ')}", :error
111
+ raise MissingRPMs
112
+ else
113
+ return true
114
+ end
115
+ end
116
+
117
+ # Ensure a user exists on the system
118
+ # @raise [DuplicateUsers]
119
+ # @raise [WrongHomedir]
120
+ # @param user [String] User to add or verify
121
+ # @param homedir [String] User home directory
122
+ # @return [Boolean]
123
+ def find_or_create_user(user, homedir, options = {})
124
+ # Create an Array of Arrays storing all known system users and their attributes
125
+ all_users = `getent passwd`.chomp.split("\n").map {|line| line.split(':')}
126
+ # Determine if the user we want exists
127
+ this_user = all_users.collect {|u| u if u[0] == user}.compact
128
+ if this_user.size > 1
129
+ log "Multiple instances of user #{user} found. This is known to cause problems.", :error
130
+ raise DuplicateUsers
131
+ elsif this_user.size == 1
132
+ log "User '#{user}' already exists with uid: #{this_user.flatten[2]}. Skipping creation of user...", :warn
133
+ full_home = File.expand_path(homedir)
134
+ current_home = File.expand_path(this_user.flatten[5])
135
+ if (full_home != current_home) and (homedir != current_home)
136
+ log "User's home directory is set to #{current_home}, not #{homedir}!", :error
137
+ raise WrongHomedir
138
+ end
139
+ else
140
+ log "Creating #{user} user..."
141
+ shell = options[:shell] ? options[:shell] : '/bin/bash'
142
+ `useradd -d "#{homedir}" -s "#{shell}" -mr "#{user}"`
143
+ end
144
+
145
+ return true
146
+ end
147
+
148
+ # Sets a user's password
149
+ # @note This _may_ only work on RHEL (and the like), as chpasswd may not be available on other OS's
150
+ # @raise [FailedPasswordChange]
151
+ # @param user [String] User to set password for
152
+ # @param new_pass [String] New password for user
153
+ # @return [Boolean]
154
+ def set_user_password(user, new_pass)
155
+ log "Setting password for #{user}..."
156
+ `echo "#{user}:#{new_pass}" | chpasswd`
157
+ raise FailedPasswordChange unless $?.success?
158
+ return $?.success?
159
+ end
160
+
161
+ # Die if a user doesn't exist
162
+ # @raise [DuplicateUsers]
163
+ # @raise [MissingUser]
164
+ # @param user [String] User to verify
165
+ # @param noisy [Boolean] Should extra info be logged?
166
+ # @return [Array]
167
+ def find_user(user, noisy = false)
168
+ # Create an Array of Arrays storing all known system users and their attributes
169
+ all_users = `getent passwd`.chomp.split("\n").map {|line| line.split(':')}
170
+ # Determine if the user we want exists
171
+ this_user = all_users.collect {|u| u if u[0] == user}.compact
172
+ if this_user.size > 1
173
+ log "Multiple instances of user #{user} found. This is known to cause problems.", :error
174
+ raise DuplicateUsers
175
+ elsif this_user.size < 1
176
+ log "Missing Critical User '#{user}'!", :error
177
+ raise MissingUser
178
+ else
179
+ log "Found required user '#{user}' with uid #{this_user.flatten[2]}..." if noisy
180
+ end
181
+ return this_user.flatten
182
+ end
183
+
184
+ # Ensure a group exists
185
+ # @raise [DuplicateGroups]
186
+ # @raise [FailedGroupAdd]
187
+ # @param group [String] Group to create or verify
188
+ # @return [Boolean]
189
+ def find_or_create_group(group)
190
+ all_groups = `getent group`.chomp.split("\n").map {|line| line.split(':')}
191
+ this_group = all_groups.collect {|g| g if g[0] == group}.compact
192
+ if this_group.size > 1
193
+ log "Multiple instances of group #{group} found. This is known to cause problems.", :error
194
+ raise DuplicateGroups
195
+ elsif this_group.size == 1
196
+ log "Group '#{group}' already exists with gid: #{this_group.flatten[2]}. Skipping creation of group...", :warn
197
+ else
198
+ log "Creating group '#{group}'..."
199
+ `groupadd -r "#{group}"`
200
+ raise FailedGroupAdd unless $?.success?
201
+ return $?.success?
202
+ end
203
+ end
204
+
205
+ # Set a user's gid (default group)
206
+ # @raise [FailedUserModification]
207
+ # @param user [String] User to update
208
+ # @param gid [String,Fixnum] Group to set as default for user
209
+ def update_user_gid(user, gid)
210
+ user_details = find_user(user)
211
+ if user_details[3] != gid
212
+ log "Setting default group for #{user} to #{gid}..."
213
+ `usermod -g #{gid} "#{user}"`
214
+ raise FailedUserModification unless $?.success?
215
+ log "Fixing permissions..."
216
+ `chown -R #{user}:#{gid} "#{user_details[5]}"`
217
+ end
218
+ return gid
219
+ end
220
+
221
+ # Finds the mount point for a directory, then checks it for a minimum number of kilobytes
222
+ # @param install_dir [String] Directory from which check starts
223
+ # @param min_space [Fixnum] Minumum amount of space required on mount-point discovered from install_dir
224
+ def check_disk_space(install_dir, min_space)
225
+ all_filesystems = `df -PB 1024`.chomp.split("\n").collect {|fs| fs.split if fs.match(/^\/dev/) }.compact
226
+ best_fit = nil
227
+ current_test = install_dir
228
+ until best_fit
229
+ matching_fs = all_filesystems.collect {|fs| fs if File.expand_path(fs[5]) == File.expand_path(current_test) }.compact
230
+ if matching_fs.size == 1
231
+ best_fit = matching_fs.flatten
232
+ else
233
+ current_test = File.dirname(current_test)
234
+ end
235
+ end
236
+
237
+ log "Closest matching filesystem is '#{best_fit[5]}'. Testing for free space..."
238
+ if Integer(best_fit[3]) >= min_space
239
+ log "Found sufficient space #{best_fit[3]} for install on #{best_fit[5]} with an install directory of #{install_dir}."
240
+ else
241
+ log "Insufficient space #{best_fit[3]} for install on #{best_fit[5]} with an install directory of #{install_dir}!", :error
242
+ end
243
+ end
244
+
245
+ # Linux specific way to run something as another user. Command must be passed as a String.
246
+ # @param user [String] User that will run command
247
+ # @param command [String] Command to be run by user
248
+ def run_as_user(user, command)
249
+ log "Running '#{command}' as '#{user}'..."
250
+ tee("su -l #{user} -c \"#{command}\"")
251
+ end
252
+
253
+ # Set the max number of open files for a user (nofiles)
254
+ # (sets both hard and soft, errors out if user is already listed in limits.conf)
255
+ # Currently does NOT verify the existence of the specified user on the system
256
+ # @param user [String] User to set 'nofile' ulimit security setting for
257
+ # @param limit [Fixnum] Limit to set for user
258
+ # @return [Boolean]
259
+ def set_nofile(user, limit)
260
+ # See if the user is already in the limits.conf file
261
+ user_entries = `grep -E '^#{user}(\s|\t)+' /etc/security/limits.conf`.chomp.split("\n")
262
+ if user_entries.empty?
263
+ # Doing this the lazy way for now... this is probably dangerous for very large files
264
+
265
+ # Open and copy the current content of the limits file plus a few extra lines towards the end
266
+ current_file = File.readlines("/etc/security/limits.conf")
267
+ temporary = Tempfile.new("new_limits")
268
+ current_file[0..(current_file.size - 2)].each {|l| temporary.write l }
269
+
270
+ temporary.write "#{user}\t\tsoft\tnofile\t\t#{limit}\n"
271
+ temporary.write "#{user}\t\thard\tnofile\t\t#{limit}\n"
272
+ temporary.write "\n"
273
+ temporary.write current_file[current_file.size - 1]
274
+ temporary.rewind
275
+
276
+ # Create a backup of the current file
277
+ FileUtils.cp "/etc/security/limits.conf", "/etc/security/limits.conf.#{Time.now.to_i}"
278
+ # Clear out the current file
279
+ File.truncate("/etc/security/limits.conf", 0)
280
+ # Copy the temporary (new) file to the existing
281
+ current_file = File.new("/etc/security/limits.conf", "w")
282
+ temporary.each {|l| current_file.puts l }
283
+ # Close everything up
284
+ temporary.close
285
+ temporary.unlink
286
+ current_file.close
287
+ return true
288
+ else
289
+ log "User '#{user}' already has a specific entry in /etc/security/limits.conf. Ensure 'nofile' is at least #{limit}.", :warn
290
+ return false
291
+ end
292
+ end # set_nofile()
293
+
294
+ end
295
+ end
296
+ end
@@ -10,6 +10,19 @@ module Doable
10
10
 
11
11
  (1..length).collect{|a| options[:characters][rand(options[:characters].size)] }.join
12
12
  end # generate_password()
13
+
14
+ def sha1_hash(password, add_salt = true)
15
+ salt = if add_salt?
16
+ if add_salt.kind_of?(String) and add_salt.downcase.match(/^[0-9a-f]+$/)
17
+ add_salt.downcase
18
+ else
19
+ generate_password(16, characters: [*(0..9), *('a'..'f')])
20
+ end
21
+ else
22
+ ''
23
+ end
24
+ Base64.encode64(Digest::SHA1.digest(password + salt) + salt).chomp
25
+ end
13
26
  end
14
27
  end
15
28
  end
data/lib/doable/job.rb CHANGED
@@ -8,10 +8,19 @@ module Doable
8
8
  include Helpers::Logging
9
9
  attr_reader :steps, :hooks, :handlers, :threads
10
10
 
11
+ # Allows sequential definition of job steps.
12
+ # @example usage
13
+ # job = Doable::Job.plan do |j|
14
+ # j.before { log "Starting my awesome job" }
15
+ # j.step { # do some stuff here }
16
+ # j.attempt { # try to do some other stuff here }
17
+ # j.after { log "Looks like we're all set" }
18
+ # end
11
19
  def self.plan(&block)
12
20
  self.new(&block)
13
21
  end
14
-
22
+
23
+ # Yields itself to allow the syntax seen in the plan class method.
15
24
  def initialize
16
25
  @hooks = {}
17
26
  @steps = []
@@ -58,7 +67,7 @@ module Doable
58
67
  # WARNING! Exception handlers are __not__ used with these steps, as they never actually raise exceptions
59
68
  # @param options [Hash]
60
69
  # @param block [Proc]
61
- # @return [Boolean]
70
+ # @return [Boolean] this will __always__ be true by the nature of "attempt"
62
71
  def attempt(options = {}, &block)
63
72
  @steps << Step.new(self, options) do
64
73
  begin
@@ -113,6 +122,7 @@ module Doable
113
122
 
114
123
  # This triggers a block associated with a hook
115
124
  # @param hook [Symbol] Hook to trigger
125
+ # @return [Boolean] returns true no exceptions are encounter during enumeration of hook steps.
116
126
  def trigger(hook)
117
127
  @hooks[hook].each_with_index do |step, index|
118
128
  begin
@@ -151,6 +161,7 @@ module Doable
151
161
  end
152
162
  end # begin()
153
163
  end if @hooks[hook] # each_with_index()
164
+ return true
154
165
  end # trigger()
155
166
 
156
167
  # Here we actually trigger the execution of a Job
@@ -221,4 +232,4 @@ module Doable
221
232
  log "All Job steps completed successfully!", :success # This should only happen if everything goes well
222
233
  end # run()
223
234
  end
224
- end
235
+ end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Gnagy
8
+ - Tim Earle
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2015-01-29 00:00:00.000000000 Z
12
+ date: 2015-02-02 00:00:00.000000000 Z
12
13
  dependencies: []
13
14
  description: A framework for automating tasks with ease
14
15
  email: jonathan.gnagy@gmail.com
@@ -20,8 +21,10 @@ files:
20
21
  - README.md
21
22
  - lib/doable.rb
22
23
  - lib/doable/exceptions/framework.rb
24
+ - lib/doable/exceptions/linux.rb
23
25
  - lib/doable/exceptions/os.rb
24
26
  - lib/doable/helpers/framework.rb
27
+ - lib/doable/helpers/linux.rb
25
28
  - lib/doable/helpers/logging.rb
26
29
  - lib/doable/helpers/os.rb
27
30
  - lib/doable/helpers/password.rb