racf 0.6.0
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.
- 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
|