racf 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.travis.yml +18 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +40 -0
- data/README.rdoc +56 -0
- data/Rakefile +9 -0
- data/TODO.txt +5 -0
- data/config/racf.yml.sample +27 -0
- data/lib/racf.rb +78 -0
- data/lib/racf/client.rb +138 -0
- data/lib/racf/commands/abstract_command.rb +13 -0
- data/lib/racf/commands/listgrp.rb +189 -0
- data/lib/racf/commands/listgrp/group_members_parser.rb +104 -0
- data/lib/racf/commands/listuser.rb +112 -0
- data/lib/racf/commands/rlist.rb +208 -0
- data/lib/racf/commands/search.rb +31 -0
- data/lib/racf/pagers/cached_ispf_pager.rb +91 -0
- data/lib/racf/pagers/ispf_pager.rb +57 -0
- data/lib/racf/s3270.rb +136 -0
- data/lib/racf/session.rb +149 -0
- data/lib/racf/version.rb +3 -0
- data/racf.gemspec +16 -0
- data/script/ci +3 -0
- data/spec/fixtures/config/racf.yml +9 -0
- data/spec/fixtures/listgrp/multiple_groups_multiple_members.txt +26 -0
- data/spec/fixtures/listgrp/multiple_members.txt +10 -0
- data/spec/fixtures/listgrp/no_members.txt +4 -0
- data/spec/fixtures/listgrp/not_found_groups.txt +21 -0
- data/spec/fixtures/listgrp/one_group.txt +7 -0
- data/spec/fixtures/listgrp/one_group_with_members.txt +13 -0
- data/spec/fixtures/listgrp/one_member.txt +7 -0
- data/spec/fixtures/listuser/all_users.txt +45 -0
- data/spec/fixtures/listuser/just_users_not_found.txt +3 -0
- data/spec/fixtures/listuser/one_user.txt +47 -0
- data/spec/fixtures/listuser/some_not_found_users.txt +88 -0
- data/spec/fixtures/racf_cache_dump.yml +9 -0
- data/spec/fixtures/rlist/gims.txt +135 -0
- data/spec/fixtures/rlist/gims_with_no_tims.txt +135 -0
- data/spec/fixtures/rlist/gims_with_not_found.txt +89 -0
- data/spec/fixtures/rlist/just_one_not_found.txt +1 -0
- data/spec/fixtures/rlist/multiple_not_found.txt +3 -0
- data/spec/fixtures/rlist/rlist_success.txt +50 -0
- data/spec/fixtures/rlist/tims_without_users.txt +119 -0
- data/spec/fixtures/search/gims.txt +30 -0
- data/spec/fixtures/search/tims.txt +30 -0
- data/spec/fixtures/session/screen_with_bottom_menu.txt +31 -0
- data/spec/fixtures/session/screen_with_top_and_bottom_menu.txt +47 -0
- data/spec/fixtures/session/screen_with_top_menu.txt +29 -0
- data/spec/fixtures/session/screen_without_menu.txt +13 -0
- data/spec/racf/client_spec.rb +155 -0
- data/spec/racf/commands/listgrp/group_members_parser_spec.rb +82 -0
- data/spec/racf/commands/listgrp_spec.rb +303 -0
- data/spec/racf/commands/listuser_spec.rb +123 -0
- data/spec/racf/commands/rlist_spec.rb +257 -0
- data/spec/racf/commands/search_spec.rb +66 -0
- data/spec/racf/pagers/cached_ispf_pager_spec.rb +212 -0
- data/spec/racf/pagers/ispf_pager_spec.rb +59 -0
- data/spec/racf/session_spec.rb +114 -0
- data/spec/racf_spec.rb +106 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/helpers.rb +5 -0
- metadata +162 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'racf/pagers/ispf_pager'
|
2
|
+
|
3
|
+
module Racf
|
4
|
+
module Pagers
|
5
|
+
class CachedIspfPager
|
6
|
+
attr_accessor :cache
|
7
|
+
|
8
|
+
def initialize(command, uncached_pager=IspfPager)
|
9
|
+
@command = command
|
10
|
+
@pager = uncached_pager.new @command
|
11
|
+
@cache = Hash.new { |hash, key| hash[key] = {} }
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(*arguments)
|
15
|
+
resources = @command.extract_resources(arguments)
|
16
|
+
|
17
|
+
if resources.empty?
|
18
|
+
@pager.run(*arguments)
|
19
|
+
else
|
20
|
+
run_cached(arguments, resources)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def run_cached(arguments, resources)
|
27
|
+
merged_results = {}
|
28
|
+
|
29
|
+
@other_arguments = arguments.flatten - resources
|
30
|
+
|
31
|
+
merged_results.merge! cached_results(resources)
|
32
|
+
merged_results.merge! uncached_results(resources)
|
33
|
+
|
34
|
+
merged_results
|
35
|
+
end
|
36
|
+
|
37
|
+
def cached_results(resources)
|
38
|
+
cached_resources = cached_resources(resources)
|
39
|
+
|
40
|
+
unless cached_resources.empty?
|
41
|
+
Racf.logger.info "Loading data from the cache: #{cached_resources.join(', ')}"
|
42
|
+
end
|
43
|
+
|
44
|
+
items = cached_resources.map do |resource|
|
45
|
+
[resource.to_sym, current_cache[resource]]
|
46
|
+
end
|
47
|
+
|
48
|
+
Hash[items]
|
49
|
+
end
|
50
|
+
|
51
|
+
def cached_resources(resources)
|
52
|
+
current_cache.keys & resources
|
53
|
+
end
|
54
|
+
|
55
|
+
def uncached_results(resources)
|
56
|
+
uncached_resources = uncached_resources(resources)
|
57
|
+
|
58
|
+
if !uncached_resources.empty?
|
59
|
+
Racf.logger.info("Data not on cache, try to retrieve it from origin: #{uncached_resources.join(', ')}")
|
60
|
+
|
61
|
+
run_pager(uncached_resources).tap do |results|
|
62
|
+
add_to_cache(results)
|
63
|
+
end
|
64
|
+
else
|
65
|
+
{}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def uncached_resources(resources)
|
70
|
+
resources - current_cache.keys
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_to_cache(results)
|
74
|
+
string_results = results.map { |key, value| [key.to_s, value] }
|
75
|
+
results_hash = Hash[string_results]
|
76
|
+
|
77
|
+
@cache[@other_arguments].merge! results_hash
|
78
|
+
end
|
79
|
+
|
80
|
+
def current_cache
|
81
|
+
@cache[@other_arguments]
|
82
|
+
end
|
83
|
+
|
84
|
+
def run_pager uncached_resources
|
85
|
+
uncached_parameters = @other_arguments + [uncached_resources]
|
86
|
+
@pager.run(*uncached_parameters)
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Racf
|
2
|
+
module Pagers
|
3
|
+
class IspfPager
|
4
|
+
# From the ISPF command shell help, restrictions part:
|
5
|
+
# "(234 characters is the maximum allowed, including command name)"
|
6
|
+
# Setting that constant limit should deal with that restriction
|
7
|
+
ISPF_INPUT_LIMIT = 20
|
8
|
+
|
9
|
+
def initialize(command)
|
10
|
+
@command = command
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(*arguments)
|
14
|
+
resources = @command.extract_resources(arguments)
|
15
|
+
|
16
|
+
if resources.empty?
|
17
|
+
@command.run(*arguments)
|
18
|
+
else
|
19
|
+
paginated(arguments, resources)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def paginated(arguments, resources)
|
26
|
+
results = {}
|
27
|
+
|
28
|
+
total = resources.size
|
29
|
+
slice_counter = 0
|
30
|
+
|
31
|
+
other_arguments = arguments.flatten - resources
|
32
|
+
|
33
|
+
resources.each_slice(ISPF_INPUT_LIMIT) do |slice|
|
34
|
+
log_slice(slice_counter, total)
|
35
|
+
|
36
|
+
command_arguments = other_arguments + [slice]
|
37
|
+
results.merge! @command.run(*command_arguments)
|
38
|
+
|
39
|
+
slice_counter += 1
|
40
|
+
end
|
41
|
+
|
42
|
+
results
|
43
|
+
end
|
44
|
+
|
45
|
+
def log_slice(slice_index, total)
|
46
|
+
interval_start = (ISPF_INPUT_LIMIT * slice_index) + 1
|
47
|
+
|
48
|
+
interval_end = (slice_index + 1) * ISPF_INPUT_LIMIT
|
49
|
+
if interval_end > total
|
50
|
+
interval_end = total
|
51
|
+
end
|
52
|
+
|
53
|
+
Racf.logger.info("\tRetrieving #{interval_start} - #{interval_end} of #{total}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/racf/s3270.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'net/telnet'
|
2
|
+
|
3
|
+
# Wrapper around s3270 daemon (http://x3270.bgp.nu/).
|
4
|
+
# S3270 is a scriptable emulator of a mainframe terminal
|
5
|
+
#
|
6
|
+
class S3270
|
7
|
+
DEFAULT_LOG_DIR = File.expand_path("../../../log", __FILE__)
|
8
|
+
|
9
|
+
def initialize(host="127.0.0.1", host_port="3270", verbose=false, script_port="3003", telnet_timeout=30)
|
10
|
+
@host = host
|
11
|
+
@host_port = host_port
|
12
|
+
@script_port = script_port
|
13
|
+
@verbose = verbose
|
14
|
+
@telnet_timeout = telnet_timeout
|
15
|
+
|
16
|
+
open_connection
|
17
|
+
register_exit_hook
|
18
|
+
end
|
19
|
+
|
20
|
+
# Runs a s2370 action
|
21
|
+
#
|
22
|
+
def action(command)
|
23
|
+
# TODO check standarderror
|
24
|
+
# From the x3270-script manual page (http://x3270.bgp.nu/x3270-script.html):
|
25
|
+
# " If an error occurs in processing an action, the usual pop-up
|
26
|
+
# window does not appear. Instead, the text is written to standard output."
|
27
|
+
@connection.cmd(command)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Puts a string command in the screen and press enter
|
31
|
+
#
|
32
|
+
def string_action(command)
|
33
|
+
self.action("string(\"#{command}\")")
|
34
|
+
self.action("enter")
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Closes the s3270 daemon and the telnet connection on its script port
|
39
|
+
#
|
40
|
+
def close
|
41
|
+
self.action("quit")
|
42
|
+
@connection.close
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the current screen of the terminal
|
46
|
+
#
|
47
|
+
def screen
|
48
|
+
self.action("ascii")
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the content of a set of paginated screens
|
52
|
+
#
|
53
|
+
def complete_screen
|
54
|
+
screen, has_next_page = clean_screen_metadata
|
55
|
+
|
56
|
+
while has_next_page
|
57
|
+
self.action("enter")
|
58
|
+
next_screen, has_next_page = clean_screen_metadata
|
59
|
+
screen << "\n#{next_screen}"
|
60
|
+
end
|
61
|
+
|
62
|
+
screen.strip!
|
63
|
+
screen.sub!(/\*\*\*$/, '')
|
64
|
+
screen.strip! || screen
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
# Opens a telnet connection with the daemon
|
69
|
+
#
|
70
|
+
def open_connection
|
71
|
+
@connection ||= begin
|
72
|
+
raise "It was not possible to run s3270" unless run_s3270_daemon
|
73
|
+
sleep(3)
|
74
|
+
|
75
|
+
Net::Telnet.new(telnet_options)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def run_s3270_daemon
|
80
|
+
if @verbose
|
81
|
+
s3270_command = "s3270 #{@host}:#{@host_port} -scriptport #{@script_port} -trace -tracefile #{File.join(DEFAULT_LOG_DIR, "s3270-trace.log")} &"
|
82
|
+
else
|
83
|
+
s3270_command = "s3270 #{@host}:#{@host_port} -scriptport #{@script_port} &"
|
84
|
+
end
|
85
|
+
|
86
|
+
system(s3270_command)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Return paginated terminal screens without the emulator metadata
|
90
|
+
#
|
91
|
+
def clean_screen_metadata
|
92
|
+
raw_output = self.action("ascii")
|
93
|
+
raw_output.gsub!(/data: /, '')
|
94
|
+
raw_output.gsub!(/\s+\n/, "\n")
|
95
|
+
content = raw_output.split("\n")
|
96
|
+
|
97
|
+
success_message = content.delete_at(-1)
|
98
|
+
status_message = content.delete_at(-1)
|
99
|
+
|
100
|
+
has_next_page = nil
|
101
|
+
if content[-1] =~ /\*\*\*/
|
102
|
+
has_next_page = content.delete_at(-1)
|
103
|
+
end
|
104
|
+
|
105
|
+
[content.join("\n"), has_next_page]
|
106
|
+
end
|
107
|
+
|
108
|
+
def register_exit_hook
|
109
|
+
trap("INT") do
|
110
|
+
unless @connection.closed?
|
111
|
+
close
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def telnet_options
|
117
|
+
if @verbose
|
118
|
+
default_telnet_options.merge(verbose_telnet_options)
|
119
|
+
else
|
120
|
+
default_telnet_options
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def default_telnet_options
|
125
|
+
{
|
126
|
+
"Host" => "127.0.0.1",
|
127
|
+
"Port" => @script_port,
|
128
|
+
"Prompt" => /^ok|^error/,
|
129
|
+
"Timeout" => @telnet_timeout
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
def verbose_telnet_options
|
134
|
+
{ "Dump_log" => File.join(DEFAULT_LOG_DIR, "telnet-output.log") }
|
135
|
+
end
|
136
|
+
end
|
data/lib/racf/session.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'racf/s3270'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Racf
|
5
|
+
LoginError = Class.new(StandardError)
|
6
|
+
|
7
|
+
class Session
|
8
|
+
DEFAULT_SERVER_PORT = 23
|
9
|
+
DEFAULT_SCRIPT_PORT = '7060'
|
10
|
+
|
11
|
+
attr_reader :last_error_message
|
12
|
+
|
13
|
+
def initialize(options)
|
14
|
+
@user_id = options[:user_id]
|
15
|
+
@password = options[:password]
|
16
|
+
@server_address = options[:server_address]
|
17
|
+
@verbose = options[:verbose]
|
18
|
+
@telnet_timeout = options[:telnet_timeout]
|
19
|
+
@server_port = options[:server_port] || DEFAULT_SERVER_PORT
|
20
|
+
@script_port = options[:script_port] || DEFAULT_SCRIPT_PORT
|
21
|
+
|
22
|
+
@scraper = options[:scraper] || default_scraper
|
23
|
+
end
|
24
|
+
|
25
|
+
# Start a ISPF shell session
|
26
|
+
#
|
27
|
+
def start
|
28
|
+
Racf.logger.info("Starting RACF session")
|
29
|
+
login
|
30
|
+
load_ispf_shell
|
31
|
+
end
|
32
|
+
|
33
|
+
# Go out of ISPF shell and logoff from mainframe
|
34
|
+
#
|
35
|
+
def finish
|
36
|
+
Racf.logger.info("Closing RACF session")
|
37
|
+
@scraper.action("pf 3")
|
38
|
+
@scraper.string_action("X")
|
39
|
+
@scraper.string_action("logoff")
|
40
|
+
@scraper.close # a action quit faz o emulador dar exit
|
41
|
+
Racf.logger.info("RACF session closed")
|
42
|
+
end
|
43
|
+
|
44
|
+
# Runs a RACF command and returns its raw output
|
45
|
+
#
|
46
|
+
def run_command(command)
|
47
|
+
Racf.logger.info("Executing RACF '#{command}'")
|
48
|
+
|
49
|
+
@scraper.string_action(command)
|
50
|
+
raw_response = @scraper.complete_screen
|
51
|
+
@scraper.action("enter")
|
52
|
+
|
53
|
+
Racf.logger.info("Finished executing RACF '#{command}'")
|
54
|
+
|
55
|
+
screen_without_menu = remove_ispf_menu(raw_response)
|
56
|
+
screen_without_menu
|
57
|
+
end
|
58
|
+
|
59
|
+
# TODO
|
60
|
+
# Remove from the public API. In order to do it, it's neeeded to
|
61
|
+
# improve the design. #theClientIsPushingUs
|
62
|
+
def remove_ispf_menu(screen)
|
63
|
+
if screen.lines.first =~ /Menu\s+List\s+Mode\s+Functions\s+Utilities\s+Help/
|
64
|
+
screen = screen.drop(8)
|
65
|
+
screen = screen.drop_while { |line| line.match(/.*?=>.*?$/) }
|
66
|
+
end
|
67
|
+
|
68
|
+
screen = screen.to_s
|
69
|
+
|
70
|
+
if screen =~ /Menu\s+List\s+Mode\s+Functions\s+Utilities\s+Help/
|
71
|
+
screen.gsub!(/Menu\s+List\s+Mode\s+Functions\s+Utilities\s+Help.*/m, '')
|
72
|
+
end
|
73
|
+
|
74
|
+
screen.strip!
|
75
|
+
screen
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
def default_scraper
|
80
|
+
S3270.new(@server_address, @server_port, @verbose, @script_port, @telnet_timeout)
|
81
|
+
end
|
82
|
+
|
83
|
+
def login
|
84
|
+
@scraper.string_action("TA")
|
85
|
+
|
86
|
+
Racf.logger.info("Trying to login...")
|
87
|
+
|
88
|
+
@scraper.string_action(@user_id)
|
89
|
+
wait_for_text_on_screen("Enter LOGON parameters below")
|
90
|
+
@scraper.string_action(@password)
|
91
|
+
@scraper.action("enter")
|
92
|
+
|
93
|
+
sleep(2) # make sure there's time for login to complete
|
94
|
+
|
95
|
+
if has_login_error?
|
96
|
+
@scraper.string_action("logoff")
|
97
|
+
@scraper.close
|
98
|
+
Racf.logger.info("Tried to login, but #{last_error_message}")
|
99
|
+
raise LoginError, last_error_message
|
100
|
+
end
|
101
|
+
|
102
|
+
return true
|
103
|
+
end
|
104
|
+
|
105
|
+
def has_login_error?
|
106
|
+
user_already_logged_in? || user_has_expired_password? || user_has_not_authorized_password?
|
107
|
+
end
|
108
|
+
|
109
|
+
def user_already_logged_in?
|
110
|
+
if @scraper.screen.match(/LOGON\s+rejected,\s+UserId\s+(.*?)\s+already\s+logged\s+on/i)
|
111
|
+
@last_error_message = "User #{@user_id} already logged in"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def user_has_expired_password?
|
116
|
+
if @scraper.screen.match(/CURRENT\s+PASSWORD\s+HAS\s+EXPIRED/)
|
117
|
+
@last_error_message = "Password for user #{@user_id} has expired"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def user_has_not_authorized_password?
|
122
|
+
if @scraper.screen.match(/PASSWORD\s+NOT\s+AUTHORIZED\s+FOR\s+USERID/)
|
123
|
+
@last_error_message = "Password for user #{@user_id} is not authorized"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def load_ispf_shell
|
128
|
+
Racf.logger.info("Loading ISPF shell")
|
129
|
+
wait_for_text_on_screen('ISPF/PDF')
|
130
|
+
@scraper.string_action("6")
|
131
|
+
Racf.logger.info("ISPF shell loaded")
|
132
|
+
end
|
133
|
+
|
134
|
+
def wait_for_text_on_screen(text)
|
135
|
+
screen = @scraper.complete_screen
|
136
|
+
|
137
|
+
cicles_index = 0
|
138
|
+
until screen.match(text)
|
139
|
+
cicles_index += 1
|
140
|
+
sleep(0.25)
|
141
|
+
Racf.logger.info("\twaiting for text '#{text}' appear in the screen")
|
142
|
+
screen = @scraper.screen
|
143
|
+
end
|
144
|
+
|
145
|
+
Racf.logger.info("Text '#{text}' appeared in the screen")
|
146
|
+
Racf.logger.info("Waited #{cicles_index * 0.25} seconds for the '#{text}' appear in the screen")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
data/lib/racf/version.rb
ADDED
data/racf.gemspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require File.expand_path('../lib/racf/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.authors = ["Hugo Barauna"]
|
5
|
+
gem.email = ["hugo.barauna@autoseg.com"]
|
6
|
+
gem.summary = %q{A wrapper around IBM's Resource Access Control Facility}
|
7
|
+
gem.homepage = ""
|
8
|
+
|
9
|
+
gem.files = `git ls-files`.split("\n")
|
10
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
11
|
+
gem.name = "racf"
|
12
|
+
gem.require_paths = ["lib"]
|
13
|
+
gem.version = Racf::VERSION
|
14
|
+
|
15
|
+
gem.add_dependency "state_machine", "~> 1.1.0"
|
16
|
+
end
|