abide_dev_utils 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'abide_dev_utils/errors/base'
4
+
5
+ module AbideDevUtils
6
+ module Errors
7
+ # Raised when an xpath search of an xccdf file fails
8
+ class XPathSearchError < GenericError
9
+ @default = 'XPath seach failed to find anything at:'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbideDevUtils
4
+ module Files
5
+ class Writer
6
+ MSG_EXT_APPEND = 'Appending %s extension to file'
7
+
8
+ def write(content, file: nil, add_ext: true, file_ext: nil)
9
+ valid_file = add_ext ? append_ext(file, file_ext) : file
10
+ File.open(valid_file, 'w') { |f| f.write(content) }
11
+ verify_write(valid_file)
12
+ end
13
+
14
+ def method_missing(m, *args, **kwargs, &_block)
15
+ if m.to_s.match?(/^write_/)
16
+ ext = m.to_s.split('_')[-1]
17
+ write(args[0], **kwargs, file_ext: ext)
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ def respond_to_missing?(method_name, include_private = false)
24
+ method_name.to_s.start_with?('write_') || super
25
+ end
26
+
27
+ def append_ext(file_path, ext)
28
+ return file_path if ext.nil?
29
+
30
+ s_ext = ".#{ext}"
31
+ unless File.extname(file_path) == s_ext
32
+ puts MSG_EXT_APPEND % s_ext
33
+ file_path << s_ext
34
+ end
35
+ file_path
36
+ end
37
+
38
+ def verify_write(file_path)
39
+ if File.file?(file_path)
40
+ puts "Successfully wrote to #{file_path}"
41
+ else
42
+ puts "Something went wrong! Failed writing to #{file_path}!"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jira-ruby'
4
+ require 'abide_dev_utils/output'
5
+ require 'abide_dev_utils/prompt'
6
+ require 'abide_dev_utils/config'
7
+ require 'abide_dev_utils/errors/jira'
8
+
9
+ module AbideDevUtils
10
+ module Jira
11
+ ERRORS = AbideDevUtils::Errors::Jira
12
+ COV_PARENT_SUMMARY_PREFIX = '::BENCHMARK:: '
13
+ COV_CHILD_SUMMARY_PREFIX = '::CONTROL:: '
14
+
15
+ def self.project(client, project)
16
+ client.Project.find(project)
17
+ end
18
+
19
+ def self.issue(client, issue)
20
+ client.Issue.find(issue)
21
+ end
22
+
23
+ def self.myself(client)
24
+ client.User.myself
25
+ end
26
+
27
+ def self.issuetype(client, id)
28
+ client.Issuetype.find(id)
29
+ end
30
+
31
+ def self.priority(client, id)
32
+ client.Priority.find(id)
33
+ end
34
+
35
+ def self.all_project_issues_attrs(project)
36
+ raw_issues = project.issues
37
+ raw_issues.collect(&:attrs)
38
+ end
39
+
40
+ def self.new_issue(client, project, summary, dry_run: false)
41
+ if dry_run
42
+ sleep(0.2)
43
+ return Dummy.new
44
+ end
45
+ fields = {}
46
+ fields['summary'] = summary
47
+ fields['project'] = project(client, project)
48
+ fields['reporter'] = myself(client)
49
+ fields['issuetype'] = issuetype(client, '3')
50
+ fields['priority'] = priority(client, '6')
51
+ issue = client.Issue.build
52
+ raise ERRORS::CreateIssueError, issue.attrs unless issue.save({ 'fields' => fields })
53
+
54
+ issue
55
+ end
56
+
57
+ # This should probably be threaded in the future
58
+ def self.bulk_new_issue(client, project, summaries, dry_run: false)
59
+ summaries.each { |s| new_issue(client, project, s, dry_run: dry_run) }
60
+ end
61
+
62
+ def self.new_subtask(client, issue, summary, dry_run: false)
63
+ if dry_run
64
+ sleep(0.2)
65
+ return Dummy.new
66
+ end
67
+ issue_fields = issue.attrs['fields']
68
+ fields = {}
69
+ fields['parent'] = issue
70
+ fields['summary'] = summary
71
+ fields['project'] = issue_fields['project']
72
+ fields['reporter'] = myself(client)
73
+ fields['issuetype'] = issuetype(client, '5')
74
+ fields['priority'] = issue_fields['priority']
75
+ subtask = client.Issue.build
76
+ raise ERRORS::CreateSubtaskError, subtask.attrs unless subtask.save({ 'fields' => fields })
77
+
78
+ subtask
79
+ end
80
+
81
+ def self.bulk_new_subtask(client, issue, summaries, dry_run: false)
82
+ summaries.each do |s|
83
+ new_subtask(client, issue, s, dry_run: dry_run)
84
+ end
85
+ end
86
+
87
+ def self.client(options: {})
88
+ opts = merge_options(options)
89
+ opts[:username] = AbideDevUtils::Prompt.username if opts[:username].nil?
90
+ opts[:password] = AbideDevUtils::Prompt.password if opts[:password].nil?
91
+ opts[:site] = AbideDevUtils::Prompt.single_line('Jira URL') if opts[:site].nil?
92
+ opts[:context_path] = '' if opts[:context_path].nil?
93
+ opts[:auth_type] = :basic if opts[:auth_type].nil?
94
+ JIRA::Client.new(opts)
95
+ end
96
+
97
+ def self.client_from_prompts(http_debug: false)
98
+ options = {}
99
+ options[:username] = AbideDevUtils::Prompt.username
100
+ options[:password] = AbideDevUtils::Prompt.password
101
+ options[:site] = AbideDevUtils::Prompt.single_line('Jira URL')
102
+ options[:context_path] = ''
103
+ options[:auth_type] = :basic
104
+ options[:http_debug] = http_debug
105
+ JIRA::Client.new(options)
106
+ end
107
+
108
+ def self.project_from_prompts(http_debug: false)
109
+ client = client_from_prompts(http_debug)
110
+ project = AbideDevUtils::Prompt.single_line('Project').upcase
111
+ client.Project.find(project)
112
+ end
113
+
114
+ def self.new_issues_from_coverage(client, project, report, dry_run: false)
115
+ dr_prefix = dry_run ? 'DRY RUN: ' : ''
116
+ i_attrs = all_project_issues_attrs(project)
117
+ rep_sums = summaries_from_coverage_report(report)
118
+ rep_sums.each do |k, v|
119
+ next if summary_exist?(k, i_attrs)
120
+
121
+ parent = new_issue(client, project.attrs['key'], k.to_s, dry_run: dry_run)
122
+ AbideDevUtils::Output.simple("#{dr_prefix}Created parent issue #{k}")
123
+ parent_issue = issue(client, parent.attrs['key']) unless parent.respond_to?(:dummy)
124
+ AbideDevUtils::Output.simple("#{dr_prefix}Creating subtasks, this can take a while...")
125
+ progress = AbideDevUtils::Output.progress(title: "#{dr_prefix}Creating Subtasks", total: nil)
126
+ v.each do |s|
127
+ next if summary_exist?(s, i_attrs)
128
+
129
+ progress.title = "#{dr_prefix}#{s}"
130
+ new_subtask(client, parent_issue, s, dry_run: dry_run)
131
+ progress.increment
132
+ end
133
+ end
134
+ end
135
+
136
+ def self.merge_options(options)
137
+ config.merge(options)
138
+ end
139
+
140
+ def self.config
141
+ AbideDevUtils::Config.config_section(:jira)
142
+ end
143
+
144
+ def self.summary_exist?(summary, issue_attrs)
145
+ issue_attrs.each do |i|
146
+ return true if i['fields']['summary'] == summary
147
+ end
148
+ false
149
+ end
150
+
151
+ def self.summaries_from_coverage_report(report)
152
+ summaries = {}
153
+ benchmark = nil
154
+ report.each do |k, v|
155
+ benchmark = v if k == 'benchmark'
156
+ next unless k.match?(/^profile_/)
157
+
158
+ parent_sum = k
159
+ v.each do |sk, sv|
160
+ next unless sk == 'uncovered'
161
+
162
+ summaries[parent_sum] = sv.collect { |s| "#{COV_CHILD_SUMMARY_PREFIX}#{s}" }
163
+ end
164
+ end
165
+ summaries.transform_keys { |k| "#{COV_PARENT_SUMMARY_PREFIX}#{benchmark}-#{k}"}
166
+ end
167
+
168
+ class Dummy
169
+ def attrs
170
+ { 'fields' => {
171
+ 'project' => 'dummy',
172
+ 'priority' => 'dummy'
173
+ } }
174
+ end
175
+
176
+ def dummy
177
+ true
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'pp'
5
+ require 'yaml'
6
+ require 'ruby-progressbar'
7
+ require 'abide_dev_utils/validate'
8
+ require 'abide_dev_utils/files'
9
+
10
+ module AbideDevUtils
11
+ module Output
12
+ FWRITER = AbideDevUtils::Files::Writer.new
13
+ def self.simple(msg, stream: $stdout)
14
+ stream.puts msg
15
+ end
16
+
17
+ def self.json(in_obj, console: false, file: nil, pretty: true)
18
+ AbideDevUtils::Validate.hashable(in_obj)
19
+ json_out = pretty ? JSON.pretty_generate(in_obj) : JSON.generate(in_obj)
20
+ simple(json_out) if console
21
+ FWRITER.write_json(json_out, file: file) unless file.nil?
22
+ end
23
+
24
+ def self.yaml(in_obj, console: false, file: nil)
25
+ AbideDevUtils::Validate.hashable(in_obj)
26
+ # Use object's #to_yaml method if it exists, convert to hash if not
27
+ yaml_out = in_obj.respond_to?(:to_yaml) ? in_obj.to_yaml : in_obj.to_h.to_yaml
28
+ simple(yaml_out) if console
29
+ FWRITER.write_yaml(yaml_out, file: file) unless file.nil?
30
+ end
31
+
32
+ def self.yml(in_obj, console: false, file: nil)
33
+ AbideDevUtils::Validate.hashable(in_obj)
34
+ # Use object's #to_yaml method if it exists, convert to hash if not
35
+ yml_out = in_obj.respond_to?(:to_yaml) ? in_obj.to_yaml : in_obj.to_h.to_yaml
36
+ simple(yml_out) if console
37
+ FWRITER.write_yml(yml_out, file: file) unless file.nil?
38
+ end
39
+
40
+ def self.progress(title: 'Progress', start: 0, total: 100)
41
+ ProgressBar.create(title: title, starting_at: start, total: total)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'pathname'
5
+ require 'yaml'
6
+ require 'puppet_pal'
7
+
8
+ module AbideDevUtils
9
+ module Ppt
10
+ def self.coverage_report(puppet_class_dir, hiera_path, profile = nil)
11
+ coverage = {}
12
+ coverage['classes'] = {}
13
+ all_cap = find_all_classes_and_paths(puppet_class_dir)
14
+ invalid_classes = find_invalid_classes(all_cap)
15
+ valid_classes = all_cap.dup.transpose[0] - invalid_classes
16
+ coverage['classes']['invalid'] = invalid_classes
17
+ coverage['classes']['valid'] = valid_classes
18
+ hiera = YAML.safe_load(File.open(hiera_path))
19
+ matcher = profile.nil? ? /^profile_/ : /^profile_#{profile}/
20
+ hiera.each do |k, v|
21
+ key_base = k.split('::')[-1]
22
+ coverage['benchmark'] = v if key_base == 'title'
23
+ next unless key_base.match?(matcher)
24
+
25
+ coverage[key_base] = generate_uncovered_data(v, valid_classes)
26
+ end
27
+ coverage
28
+ end
29
+
30
+ def self.generate_uncovered_data(ctrl_list, valid_classes)
31
+ out_hash = {}
32
+ out_hash[:num_total] = ctrl_list.length
33
+ out_hash[:uncovered] = []
34
+ out_hash[:covered] = []
35
+ ctrl_list.each do |c|
36
+ if valid_classes.include?(c)
37
+ out_hash[:covered] << c
38
+ else
39
+ out_hash[:uncovered] << c
40
+ end
41
+ end
42
+ out_hash[:num_covered] = out_hash[:covered].length
43
+ out_hash[:num_uncovered] = out_hash[:uncovered].length
44
+ out_hash[:coverage] = Float(
45
+ (Float(out_hash[:num_covered]) / Float(out_hash[:num_total])) * 100.0
46
+ ).floor(3)
47
+ out_hash
48
+ end
49
+
50
+ # Given a directory holding Puppet manifests, returns
51
+ # the full namespace for all classes in that directory.
52
+ # @param puppet_class_dir [String] path to a dir containing Puppet manifests
53
+ # @return [String] The namespace for all classes in manifests in the dir
54
+ def self.find_class_namespace(puppet_class_dir)
55
+ path = Pathname.new(puppet_class_dir)
56
+ mod_root = nil
57
+ ns_parts = []
58
+ found_manifests = false
59
+ path.ascend do |p|
60
+ if found_manifests
61
+ mod_root = find_mod_root(p)
62
+ break
63
+ end
64
+ if File.basename(p) == 'manifests'
65
+ found_manifests = true
66
+ next
67
+ else
68
+ ns_parts << File.basename(p)
69
+ end
70
+ end
71
+ "#{mod_root}::#{ns_parts.reverse.join('::')}::"
72
+ end
73
+
74
+ # Given a Pathname object of the 'manifests' directory in a Puppet module,
75
+ # determines the module namespace root. Does this by consulting
76
+ # metadata.json, if it exists, or by using the parent directory name.
77
+ # @param pathname [Pathname] A Pathname object of the module's manifests dir
78
+ # @return [String] The module's namespace root
79
+ def self.find_mod_root(pathname)
80
+ metadata_file = nil
81
+ pathname.entries.each do |e|
82
+ metadata_file = "#{pathname}/metadata.json" if File.basename(e) == 'metadata.json'
83
+ end
84
+ if metadata_file.nil?
85
+ File.basename(p)
86
+ else
87
+ File.open(metadata_file) do |f|
88
+ file = JSON.parse(f.read)
89
+ File.basename(p) unless file.key?('name')
90
+ file['name'].split('-')[-1]
91
+ end
92
+ end
93
+ end
94
+
95
+ # @return [Array] An array of frozen arrays where each sub-array's
96
+ # index 0 is class_name and index 1 is the full path to the file.
97
+ def self.find_all_classes_and_paths(puppet_class_dir)
98
+ all_cap = []
99
+ Dir.each_child(puppet_class_dir) do |c|
100
+ path = "#{puppet_class_dir}/#{c}"
101
+ next if File.directory?(path) || File.extname(path) != '.pp'
102
+
103
+ all_cap << [File.basename(path, '.pp'), path].freeze
104
+ end
105
+ all_cap
106
+ end
107
+
108
+ def self.find_valid_classes(all_cap)
109
+ all_classes = all_cap.dup.transpose[0]
110
+ all_classes - find_invalid_classes(all_cap)
111
+ end
112
+
113
+ def self.find_invalid_classes(all_cap)
114
+ invalid_classes = []
115
+ all_cap.each do |cap|
116
+ invalid_classes << cap[0] unless class_valid?(cap[1])
117
+ end
118
+ invalid_classes
119
+ end
120
+
121
+ def self.class_valid?(manifest_path)
122
+ compiler = Puppet::Pal::Compiler.new(nil)
123
+ ast = compiler.parse_file(manifest_path)
124
+ ast.body.body.statements.each do |s|
125
+ next unless s.respond_to?(:arguments)
126
+ next unless s.arguments.respond_to?(:each)
127
+
128
+ s.arguments.each do |i|
129
+ return false if i.value == 'Not implemented'
130
+ end
131
+ end
132
+ true
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+
5
+ module AbideDevUtils
6
+ module Prompt
7
+ def self.yes_no(msg)
8
+ print "#{msg} (Y/n): "
9
+ return true if $stdin.cooked(&:gets).match?(/^[Yy].*/)
10
+
11
+ false
12
+ end
13
+
14
+ def self.single_line(msg)
15
+ print "#{msg}: "
16
+ $stdin.cooked(&:gets).chomp
17
+ end
18
+
19
+ def self.username
20
+ print 'Username: '
21
+ $stdin.cooked(&:gets).chomp
22
+ end
23
+
24
+ def self.password
25
+ $stdin.getpass('Password:')
26
+ end
27
+
28
+ def self.secure(msg)
29
+ $stdin.getpass(msg)
30
+ end
31
+ end
32
+ end