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