racf 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/.gitignore +8 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +18 -0
  4. data/Gemfile +14 -0
  5. data/Gemfile.lock +40 -0
  6. data/README.rdoc +56 -0
  7. data/Rakefile +9 -0
  8. data/TODO.txt +5 -0
  9. data/config/racf.yml.sample +27 -0
  10. data/lib/racf.rb +78 -0
  11. data/lib/racf/client.rb +138 -0
  12. data/lib/racf/commands/abstract_command.rb +13 -0
  13. data/lib/racf/commands/listgrp.rb +189 -0
  14. data/lib/racf/commands/listgrp/group_members_parser.rb +104 -0
  15. data/lib/racf/commands/listuser.rb +112 -0
  16. data/lib/racf/commands/rlist.rb +208 -0
  17. data/lib/racf/commands/search.rb +31 -0
  18. data/lib/racf/pagers/cached_ispf_pager.rb +91 -0
  19. data/lib/racf/pagers/ispf_pager.rb +57 -0
  20. data/lib/racf/s3270.rb +136 -0
  21. data/lib/racf/session.rb +149 -0
  22. data/lib/racf/version.rb +3 -0
  23. data/racf.gemspec +16 -0
  24. data/script/ci +3 -0
  25. data/spec/fixtures/config/racf.yml +9 -0
  26. data/spec/fixtures/listgrp/multiple_groups_multiple_members.txt +26 -0
  27. data/spec/fixtures/listgrp/multiple_members.txt +10 -0
  28. data/spec/fixtures/listgrp/no_members.txt +4 -0
  29. data/spec/fixtures/listgrp/not_found_groups.txt +21 -0
  30. data/spec/fixtures/listgrp/one_group.txt +7 -0
  31. data/spec/fixtures/listgrp/one_group_with_members.txt +13 -0
  32. data/spec/fixtures/listgrp/one_member.txt +7 -0
  33. data/spec/fixtures/listuser/all_users.txt +45 -0
  34. data/spec/fixtures/listuser/just_users_not_found.txt +3 -0
  35. data/spec/fixtures/listuser/one_user.txt +47 -0
  36. data/spec/fixtures/listuser/some_not_found_users.txt +88 -0
  37. data/spec/fixtures/racf_cache_dump.yml +9 -0
  38. data/spec/fixtures/rlist/gims.txt +135 -0
  39. data/spec/fixtures/rlist/gims_with_no_tims.txt +135 -0
  40. data/spec/fixtures/rlist/gims_with_not_found.txt +89 -0
  41. data/spec/fixtures/rlist/just_one_not_found.txt +1 -0
  42. data/spec/fixtures/rlist/multiple_not_found.txt +3 -0
  43. data/spec/fixtures/rlist/rlist_success.txt +50 -0
  44. data/spec/fixtures/rlist/tims_without_users.txt +119 -0
  45. data/spec/fixtures/search/gims.txt +30 -0
  46. data/spec/fixtures/search/tims.txt +30 -0
  47. data/spec/fixtures/session/screen_with_bottom_menu.txt +31 -0
  48. data/spec/fixtures/session/screen_with_top_and_bottom_menu.txt +47 -0
  49. data/spec/fixtures/session/screen_with_top_menu.txt +29 -0
  50. data/spec/fixtures/session/screen_without_menu.txt +13 -0
  51. data/spec/racf/client_spec.rb +155 -0
  52. data/spec/racf/commands/listgrp/group_members_parser_spec.rb +82 -0
  53. data/spec/racf/commands/listgrp_spec.rb +303 -0
  54. data/spec/racf/commands/listuser_spec.rb +123 -0
  55. data/spec/racf/commands/rlist_spec.rb +257 -0
  56. data/spec/racf/commands/search_spec.rb +66 -0
  57. data/spec/racf/pagers/cached_ispf_pager_spec.rb +212 -0
  58. data/spec/racf/pagers/ispf_pager_spec.rb +59 -0
  59. data/spec/racf/session_spec.rb +114 -0
  60. data/spec/racf_spec.rb +106 -0
  61. data/spec/spec_helper.rb +18 -0
  62. data/spec/support/helpers.rb +5 -0
  63. 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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Racf
2
+ VERSION = "0.6.0"
3
+ end
@@ -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