abide_dev_utils 0.1.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.
@@ -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