puppet-debugger 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +54 -0
  4. data/.gitlab-ci.yml +129 -0
  5. data/.rspec +3 -0
  6. data/CHANGELOG.md +61 -0
  7. data/Gemfile +18 -0
  8. data/Gemfile.lock +67 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.md +276 -0
  11. data/Rakefile +32 -0
  12. data/bin/pdb +4 -0
  13. data/lib/awesome_print/ext/awesome_puppet.rb +40 -0
  14. data/lib/puppet-debugger/cli.rb +247 -0
  15. data/lib/puppet-debugger/code/code_file.rb +98 -0
  16. data/lib/puppet-debugger/code/code_range.rb +69 -0
  17. data/lib/puppet-debugger/code/loc.rb +80 -0
  18. data/lib/puppet-debugger/debugger_code.rb +318 -0
  19. data/lib/puppet-debugger/support/compiler.rb +20 -0
  20. data/lib/puppet-debugger/support/environment.rb +38 -0
  21. data/lib/puppet-debugger/support/errors.rb +75 -0
  22. data/lib/puppet-debugger/support/facts.rb +78 -0
  23. data/lib/puppet-debugger/support/functions.rb +72 -0
  24. data/lib/puppet-debugger/support/input_responders.rb +136 -0
  25. data/lib/puppet-debugger/support/node.rb +90 -0
  26. data/lib/puppet-debugger/support/play.rb +91 -0
  27. data/lib/puppet-debugger/support/scope.rb +42 -0
  28. data/lib/puppet-debugger/support.rb +176 -0
  29. data/lib/puppet-debugger.rb +55 -0
  30. data/lib/trollop.rb +861 -0
  31. data/lib/version.rb +3 -0
  32. data/puppet-debugger.gemspec +36 -0
  33. data/run_container_test.sh +12 -0
  34. data/spec/facts_spec.rb +86 -0
  35. data/spec/fixtures/environments/production/manifests/site.pp +1 -0
  36. data/spec/fixtures/invalid_node_obj.yaml +8 -0
  37. data/spec/fixtures/node_obj.yaml +298 -0
  38. data/spec/fixtures/sample_manifest.pp +2 -0
  39. data/spec/fixtures/sample_start_debugger.pp +13 -0
  40. data/spec/pdb_spec.rb +50 -0
  41. data/spec/puppet-debugger_spec.rb +492 -0
  42. data/spec/remote_node_spec.rb +170 -0
  43. data/spec/spec_helper.rb +57 -0
  44. data/spec/support_spec.rb +190 -0
  45. data/test_matrix.rb +42 -0
  46. metadata +148 -0
@@ -0,0 +1,136 @@
1
+ module PuppetDebugger
2
+ module Support
3
+ module InputResponders
4
+
5
+ def static_responder_list
6
+ ["exit", "functions", "classification","vars", 'facterdb_filter', "krt", "facts",
7
+ "resources", "classes", "whereami", "play","reset", "help"
8
+ ]
9
+ end
10
+
11
+ # @source_file and @source_line_num instance variables must be set for this
12
+ # method to show the surrounding code
13
+ # @return [String] - string output of the code surrounded by the breakpoint or nil if file or line_num do not exist
14
+ def whereami(command=nil, args=nil)
15
+ file=@source_file
16
+ line_num=@source_line_num
17
+ if file and line_num
18
+ if file == :code
19
+ source_code = Puppet[:code]
20
+ code = DebuggerCode.from_string(source_code, :puppet)
21
+ else
22
+ code = DebuggerCode.from_file(file, :puppet)
23
+ end
24
+ return code.with_marker(line_num).around(line_num, 5).with_line_numbers.with_indentation(5).to_s
25
+ end
26
+ end
27
+
28
+ # displays the facterdb filter
29
+ # @param [Array] - args is not used
30
+ def facterdb_filter(args=[])
31
+ dynamic_facterdb_filter.ai
32
+ end
33
+
34
+ def help(args=[])
35
+ PuppetDebugger::Cli.print_repl_desc
36
+ end
37
+
38
+ def handle_set(input)
39
+ output = ''
40
+ args = input.split(' ')
41
+ args.shift # throw away the set
42
+ case args.shift
43
+ when /node/
44
+ if name = args.shift
45
+ output = "Resetting to use node #{name}"
46
+ reset
47
+ set_remote_node_name(name)
48
+ else
49
+ out_buffer.puts "Must supply a valid node name"
50
+ end
51
+ when /loglevel/
52
+ if level = args.shift
53
+ @log_level = level
54
+ set_log_level(level)
55
+ output = "loglevel #{Puppet::Util::Log.level} is set"
56
+ end
57
+ end
58
+ output
59
+ end
60
+
61
+ def facts(args=[])
62
+ variables = node.facts.values
63
+ variables.ai({:sort_keys => true, :indent => -1})
64
+ end
65
+
66
+ def functions(args=[])
67
+ filter = args.first || ''
68
+ function_map.keys.sort.grep(/^#{Regexp.escape(filter)}/)
69
+ end
70
+
71
+ def vars(args=[])
72
+ # remove duplicate variables that are also in the facts hash
73
+ variables = scope.to_hash.delete_if {| key, value | node.facts.values.key?(key) }
74
+ variables['facts'] = 'removed by the puppet-debugger' if variables.key?('facts')
75
+ output = "Facts were removed for easier viewing".ai + "\n"
76
+ output += variables.ai({:sort_keys => true, :indent => -1})
77
+ end
78
+
79
+ def environment(args=[])
80
+ "Puppet Environment: #{puppet_env_name}"
81
+ end
82
+
83
+ def reset(args=[])
84
+ set_scope(nil)
85
+ set_remote_node_name(nil)
86
+ set_node(nil)
87
+ set_facts(nil)
88
+ set_environment(nil)
89
+ set_log_level(log_level)
90
+ end
91
+
92
+ def set_log_level(level)
93
+ Puppet::Util::Log.level = level.to_sym
94
+ buffer_log = Puppet::Util::Log.newdestination(:buffer)
95
+ if buffer_log
96
+ # if this is already set the buffer_log is nil
97
+ buffer_log.out_buffer = out_buffer
98
+ buffer_log.err_buffer = out_buffer
99
+ end
100
+ nil
101
+ end
102
+
103
+ def krt(args=[])
104
+ known_resource_types.ai({:sort_keys => true, :indent => -1})
105
+ end
106
+
107
+ def play(args=[])
108
+ config = {}
109
+ config[:play] = args.first
110
+ play_back(config)
111
+ return nil # we don't want to return anything
112
+ end
113
+
114
+ def classification(args=[])
115
+ node.classes.ai
116
+ end
117
+
118
+ def resources(args=[])
119
+ res = scope.compiler.catalog.resources.map do |res|
120
+ res.to_s.gsub(/\[/, "['").gsub(/\]/, "']") # ensure the title has quotes
121
+ end
122
+ if !args.first.nil?
123
+ res[args.first.to_i].ai
124
+ else
125
+ output = "Resources not shown in any specific order\n".warning
126
+ output += res.ai
127
+ end
128
+ end
129
+
130
+ def classes(args=[])
131
+ scope.compiler.catalog.classes.ai
132
+ end
133
+
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,90 @@
1
+ require 'puppet/indirector/node/rest'
2
+
3
+ module PuppetDebugger
4
+ module Support
5
+ module Node
6
+ # creates a node object using defaults or gets the remote node
7
+ # object if the remote_node_name is defined
8
+ def create_node
9
+ Puppet[:trusted_server_facts] = true if Puppet.version.to_f >= 4.1
10
+
11
+ if remote_node_name
12
+ # refetch
13
+ node_obj = set_node_from_name(remote_node_name)
14
+ end
15
+ unless node_obj
16
+ options = {}
17
+ options[:parameters] = default_facts.values
18
+ options[:facts] = default_facts
19
+ options[:classes] = []
20
+ options[:environment] = puppet_environment
21
+ name = default_facts.values['fqdn']
22
+ node_obj = Puppet::Node.new(name, options)
23
+ node_obj.add_server_facts(server_facts) if node_obj.respond_to?(:add_server_facts)
24
+ node_obj
25
+ end
26
+ node_obj
27
+ end
28
+
29
+ def set_remote_node_name(name)
30
+ @remote_node_name = name
31
+ end
32
+
33
+ def remote_node_name=(name)
34
+ @remote_node_name = name
35
+ end
36
+
37
+ def remote_node_name
38
+ @remote_node_name
39
+ end
40
+
41
+ # @return [node] puppet node object
42
+ def node
43
+ @node ||= create_node
44
+ end
45
+
46
+ def get_remote_node(name)
47
+ indirection = Puppet::Indirector::Indirection.instance(:node)
48
+ indirection.terminus_class = 'rest'
49
+ remote_node = indirection.find(name, :environment => puppet_environment)
50
+ remote_node
51
+ end
52
+
53
+ # this is a hack to get around that the puppet node fact face does not return
54
+ # a proper node object with the facts hash populated
55
+ # returns a node object with a proper facts hash
56
+ def convert_remote_node(remote_node)
57
+ options = {}
58
+ # remove trusted data as it will later get populated during compilation
59
+ parameters = remote_node.parameters.dup
60
+ trusted_data = parameters.delete('trusted')
61
+ options[:parameters] = parameters || {}
62
+ options[:facts] = Puppet::Node::Facts.new(remote_node.name,remote_node.parameters)
63
+ options[:classes] = remote_node.classes
64
+ options[:environment] = puppet_environment
65
+ node_object = Puppet::Node.new(remote_node.name, options)
66
+ node_object.add_server_facts(server_facts) if node_object.respond_to?(:add_server_facts)
67
+ node_object.trusted_data = trusted_data
68
+ node_object
69
+ end
70
+
71
+ # query the remote puppet server and retrieve the node object
72
+ #
73
+ def set_node_from_name(name)
74
+ out_buffer.puts ("Fetching node #{name}")
75
+ remote_node = get_remote_node(name)
76
+ if remote_node and remote_node.parameters.empty?
77
+ remote_node_name = nil # clear out the remote name
78
+ raise PuppetDebugger::Exception::UndefinedNode.new(:name => remote_node.name)
79
+ end
80
+ remote_node_name = remote_node.name
81
+ node_object = convert_remote_node(remote_node)
82
+ set_node(node_object)
83
+ end
84
+
85
+ def set_node(value)
86
+ @node = value
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,91 @@
1
+ module PuppetDebugger
2
+ module Support
3
+ module Play
4
+
5
+ def play_back(config={})
6
+ if config[:play]
7
+ if config[:play] =~ /^http/
8
+ play_back_url(config[:play])
9
+ elsif File.exists? config[:play]
10
+ play_back_string(File.read(config[:play]))
11
+ else config[:play]
12
+ out_buffer.puts "puppet-debugger can't play #{config[:play]}'"
13
+ end
14
+ end
15
+ end
16
+
17
+ def convert_to_text(url)
18
+ require 'uri'
19
+ url_data = URI(url)
20
+ case url_data.host
21
+ when /^gist\.github*/
22
+ unless url_data.path =~ /raw/
23
+ url = url += '.txt'
24
+ end
25
+ url
26
+ when /^github.com/
27
+ if url_data.path =~ /blob/
28
+ url.gsub('blob', 'raw')
29
+ end
30
+ when /^gist.github.com/
31
+ unless url_data.path =~ /raw/
32
+ url = url += '.txt'
33
+ end
34
+ url
35
+ when /^gitlab.com/
36
+ if url_data.path =~ /snippets/
37
+ url += '/raw' unless url_data.path =~ /raw/
38
+ url
39
+ else
40
+ url.gsub('blob', 'raw')
41
+ end
42
+ else
43
+ url
44
+ end
45
+ end
46
+
47
+ # opens the url and reads the data
48
+ def fetch_url_data(url)
49
+ open(url).read
50
+ end
51
+
52
+ def play_back_url(url)
53
+ begin
54
+ require 'open-uri'
55
+ require 'net/http'
56
+ converted_url = convert_to_text(url)
57
+ str = fetch_url_data(converted_url)
58
+ play_back_string(str)
59
+ rescue SocketError
60
+ abort "puppet-debugger can't play `#{converted_url}'"
61
+ end
62
+ end
63
+
64
+ # plays back the string to the output stream
65
+ # puts the input to the output as well as the produced output
66
+ def play_back_string(str)
67
+ full_buffer = ''
68
+ str.split("\n").each do |buf|
69
+ begin
70
+ full_buffer += buf
71
+ # unless this is puppet code, otherwise skip repl keywords
72
+ if keyword_expression.match(buf)
73
+ out_buffer.write(">> ")
74
+ else
75
+ parser.parse_string(full_buffer)
76
+ out_buffer.write(">> ")
77
+ end
78
+ rescue Puppet::ParseErrorWithIssue => e
79
+ if multiline_input?(e)
80
+ full_buffer += "\n"
81
+ next
82
+ end
83
+ end
84
+ out_buffer.puts(full_buffer)
85
+ handle_input(full_buffer)
86
+ full_buffer = ''
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,42 @@
1
+ module PuppetDebugger
2
+ module Support
3
+ module Scope
4
+ def set_scope(value)
5
+ @scope = value
6
+ end
7
+
8
+ # @return [Scope] puppet scope object
9
+ def scope
10
+ unless @scope
11
+ @scope ||= create_scope
12
+ end
13
+ @scope
14
+ end
15
+
16
+ def create_scope
17
+ do_initialize
18
+ begin
19
+ @compiler = create_compiler(node) # creates a new compiler for each scope
20
+ scope = Puppet::Parser::Scope.new(@compiler)
21
+ # creates a node class
22
+ scope.source = Puppet::Resource::Type.new(:node, node.name)
23
+ scope.parent = @compiler.topscope
24
+ load_lib_dirs
25
+ # compiling will load all the facts into the scope
26
+ # without this step facts will not get resolved
27
+ scope.compiler.compile # this will load everything into the scope
28
+ rescue StandardError => e
29
+ err = parse_error(e)
30
+ raise err
31
+ end
32
+ scope
33
+ end
34
+
35
+ # returns a hash of varaibles that are currently in scope
36
+ def scope_vars
37
+ vars = scope.to_hash.delete_if {| key, value | node.facts.values.key?(key.to_sym) }
38
+ vars['facts'] = 'removed by the puppet-debugger'
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,176 @@
1
+ require 'puppet/pops'
2
+ require 'facterdb'
3
+ require 'tempfile'
4
+
5
+ # load all the generators found in the generators directory
6
+ Dir.glob(File.join(File.dirname(__FILE__),'support', '*.rb')).each do |file|
7
+ require_relative File.join('support', File.basename(file, '.rb'))
8
+ end
9
+
10
+ module PuppetDebugger
11
+ module Support
12
+ include PuppetDebugger::Support::Compilier
13
+ include PuppetDebugger::Support::Environment
14
+ include PuppetDebugger::Support::Facts
15
+ include PuppetDebugger::Support::Scope
16
+ include PuppetDebugger::Support::Functions
17
+ include PuppetDebugger::Support::Node
18
+ include PuppetDebugger::Support::InputResponders
19
+ include PuppetDebugger::Support::Play
20
+
21
+ # parses the error type into a more useful error message defined in errors.rb
22
+ # returns new error object or the original if error cannot be parsed
23
+ def parse_error(error)
24
+ case error
25
+ when SocketError
26
+ PuppetDebugger::Exception::ConnectError.new(:message => "Unknown host: #{Puppet[:server]}")
27
+ when Net::HTTPError
28
+ PuppetDebugger::Exception::AuthError.new(:message => error.message)
29
+ when Errno::ECONNREFUSED
30
+ PuppetDebugger::Exception::ConnectError.new(:message => error.message)
31
+ when Puppet::Error
32
+ if error.message =~ /could\ not\ find\ class/i
33
+ PuppetDebugger::Exception::NoClassError.new(:default_modules_paths => default_modules_paths,
34
+ :message => error.message)
35
+ elsif error.message =~ /default\ node/i
36
+ PuppetDebugger::Exception::NodeDefinitionError.new(:default_site_manifest => default_site_manifest,
37
+ :message => error.message)
38
+ else
39
+ error
40
+ end
41
+ else
42
+ error
43
+ end
44
+ end
45
+
46
+ # returns an array of module directories, generally this is the only place
47
+ # to look for puppet code by default. This is read from the puppet configuration
48
+ def default_modules_paths
49
+ dirs = []
50
+ do_initialize if Puppet[:codedir].nil?
51
+ # add the puppet-debugger directory so we can load any defined functions
52
+ dirs << File.join(Puppet[:environmentpath],default_puppet_env_name,'modules') unless Puppet[:environmentpath].empty?
53
+ dirs << Puppet.settings[:basemodulepath].split(':')
54
+ dirs.flatten
55
+ end
56
+
57
+ # this is the lib directory of this gem
58
+ # in order to load any puppet functions from this gem we need to add the lib path
59
+ # of this gem
60
+ def puppet_repl_lib_dir
61
+ File.expand_path(File.join(File.dirname(File.dirname(File.dirname(__FILE__))), 'lib'))
62
+ end
63
+
64
+ # returns all the modules paths defined in the environment
65
+ def modules_paths
66
+ puppet_environment.full_modulepath
67
+ end
68
+
69
+ def initialize_from_scope(value)
70
+ set_scope(value)
71
+ unless value.nil?
72
+ set_environment(value.environment)
73
+ set_node(value.compiler.node)
74
+ set_compiler(value.compiler)
75
+ end
76
+ end
77
+
78
+ def keyword_expression
79
+ @keyword_expression ||= Regexp.new(/^exit|^:set|^play|^classification|^facts|^vars|^functions|^classes|^resources|^krt|^environment|^reset|^help/)
80
+ end
81
+
82
+ def known_resource_types
83
+ res = {
84
+ :hostclasses => scope.environment.known_resource_types.hostclasses.keys,
85
+ :definitions => scope.environment.known_resource_types.definitions.keys,
86
+ :nodes => scope.environment.known_resource_types.nodes.keys,
87
+ }
88
+ if sites = scope.environment.known_resource_types.instance_variable_get(:@sites)
89
+ res.merge!(:sites => scope.environment.known_resource_types.instance_variable_get(:@sites).first)
90
+ end
91
+ if scope.environment.known_resource_types.respond_to?(:applications)
92
+ res.merge!(:applications => scope.environment.known_resource_types.applications.keys)
93
+ end
94
+ # some versions of puppet do not support capabilities
95
+ if scope.environment.known_resource_types.respond_to?(:capability_mappings)
96
+ res.merge!(:capability_mappings => scope.environment.known_resource_types.capability_mappings.keys)
97
+ end
98
+ res
99
+ end
100
+
101
+ # this is required in order to load things only when we need them
102
+ def do_initialize
103
+ begin
104
+ Puppet.initialize_settings
105
+ Puppet[:parser] = 'future' # this is required in order to work with puppet 3.8
106
+ Puppet[:trusted_node_data] = true
107
+ rescue ArgumentError => e
108
+
109
+ rescue Puppet::DevError => e
110
+ # do nothing otherwise calling init twice raises an error
111
+ end
112
+ end
113
+
114
+ # @param String - any valid puppet language code
115
+ # @return Hostclass - a puppet Program object which is considered the main class
116
+ def generate_ast(string = nil)
117
+ parse_result = parser.parse_string(string, '')
118
+ # the parse_result may be
119
+ # * empty / nil (no input)
120
+ # * a Model::Program
121
+ # * a Model::Expression
122
+ #
123
+ model = parse_result.nil? ? nil : parse_result.current
124
+ args = {}
125
+ ::Puppet::Pops::Model::AstTransformer.new('').merge_location(args, model)
126
+
127
+ ast_code =
128
+ if model.is_a? ::Puppet::Pops::Model::Program
129
+ ::Puppet::Parser::AST::PopsBridge::Program.new(model, args)
130
+ else
131
+ args[:value] = model
132
+ ::Puppet::Parser::AST::PopsBridge::Expression.new(args)
133
+ end
134
+ # Create the "main" class for the content - this content will get merged with all other "main" content
135
+ ::Puppet::Parser::AST::Hostclass.new('', :code => ast_code)
136
+ end
137
+
138
+ # @param String - any valid puppet language code
139
+ # @return Object - returns either a string of the result or object from puppet evaulation
140
+ def puppet_eval(input)
141
+ # in order to add functions to the scope the loaders must be created
142
+ # in order to call native functions we need to set the global_scope
143
+ ast = generate_ast(input)
144
+ # record the input for puppet to retrieve and reference later
145
+ file = Tempfile.new(['puppet_repl_input', '.pp'])
146
+ File.open(file, 'w') do |f|
147
+ f.write(input)
148
+ end
149
+ Puppet.override( {:code => input, :global_scope => scope, :loaders => scope.compiler.loaders } , 'For puppet-debugger') do
150
+ # because the repl is not a module we leave the modname blank
151
+ scope.environment.known_resource_types.import_ast(ast, '')
152
+ parser.evaluate_string(scope, input, File.expand_path(file))
153
+ end
154
+ end
155
+
156
+ def puppet_lib_dir
157
+ # returns something like "/Library/Ruby/Gems/2.0.0/gems/puppet-4.2.2/lib/puppet.rb"
158
+ # this is only useful when returning a namespace with the functions
159
+ @puppet_lib_dir ||= File.dirname(Puppet.method(:[]).source_location.first)
160
+ end
161
+
162
+ # returns a future parser for evaluating code
163
+ def parser
164
+ @parser ||= ::Puppet::Pops::Parser::EvaluatingParser.new
165
+ end
166
+
167
+ def default_manifests_dir
168
+ File.join(Puppet[:environmentpath],default_puppet_env_name,'manifests')
169
+ end
170
+
171
+ def default_site_manifest
172
+ File.join(default_manifests_dir, 'site.pp')
173
+ end
174
+
175
+ end
176
+ end
@@ -0,0 +1,55 @@
1
+ require_relative 'puppet-debugger/cli'
2
+ require_relative 'version'
3
+ require 'awesome_print'
4
+ require_relative 'awesome_print/ext/awesome_puppet'
5
+ require_relative 'trollop'
6
+ require 'puppet/util/log'
7
+ require_relative 'puppet-debugger/debugger_code'
8
+
9
+ require_relative 'puppet-debugger/support/errors'
10
+ # monkey patch in some color effects string methods
11
+ class String
12
+ def red; "\033[31m#{self}\033[0m" end
13
+ def green; "\033[32m#{self}\033[0m" end
14
+ def cyan; "\033[36m#{self}\033[0m" end
15
+ def yellow; "\033[33m#{self}\033[0m" end
16
+ def warning; yellow end
17
+ def fatal; red end
18
+ def info; green end
19
+
20
+ def camel_case
21
+ return self if self !~ /_/ && self =~ /[A-Z]+.*/
22
+ split('_').map(&:capitalize).join
23
+ end
24
+ end
25
+
26
+ Puppet::Util::Log.newdesttype :buffer do
27
+ require 'puppet/util/colors'
28
+ include Puppet::Util::Colors
29
+
30
+ attr_accessor :err_buffer, :out_buffer
31
+
32
+ def initialize(err=$stderr, out=$stdout)
33
+ @err_buffer = err
34
+ @out_buffer = out
35
+ end
36
+
37
+ def handle(msg)
38
+ levels = {
39
+ :emerg => { :name => 'Emergency', :color => :hred, :stream => err_buffer },
40
+ :alert => { :name => 'Alert', :color => :hred, :stream => err_buffer },
41
+ :crit => { :name => 'Critical', :color => :hred, :stream => err_buffer },
42
+ :err => { :name => 'Error', :color => :hred, :stream => err_buffer },
43
+ :warning => { :name => 'Warning', :color => :hred, :stream => err_buffer },
44
+ :notice => { :name => 'Notice', :color => :reset, :stream => out_buffer },
45
+ :info => { :name => 'Info', :color => :green, :stream => out_buffer },
46
+ :debug => { :name => 'Debug', :color => :cyan, :stream => out_buffer },
47
+ }
48
+
49
+ str = msg.respond_to?(:multiline) ? msg.multiline : msg.to_s
50
+ str = msg.source == "Puppet" ? str : "#{msg.source}: #{str}"
51
+
52
+ level = levels[msg.level]
53
+ level[:stream].puts colorize(level[:color], "#{level[:name]}: #{str}")
54
+ end
55
+ end