summaryrefslogtreecommitdiffstats
path: root/lib/puppet/configurer.rb
blob: 634602c1a1d1d805245da31b417f010dbd33f418 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# The client for interacting with the puppetmaster config server.
require 'sync'
require 'timeout'
require 'puppet/network/http_pool'
require 'puppet/util'

class Puppet::Configurer
    class CommandHookError < RuntimeError; end

    require 'puppet/configurer/fact_handler'
    require 'puppet/configurer/plugin_handler'

    include Puppet::Configurer::FactHandler
    include Puppet::Configurer::PluginHandler

    # For benchmarking
    include Puppet::Util

    attr_reader :compile_time

    # Provide more helpful strings to the logging that the Agent does
    def self.to_s
        "Puppet configuration client"
    end

    class << self
        # Puppetd should only have one instance running, and we need a way
        # to retrieve it.
        attr_accessor :instance
        include Puppet::Util
    end

    # How to lock instances of this class.
    def self.lockfile_path
        Puppet[:puppetdlockfile]
    end

    def clear
        @catalog.clear(true) if @catalog
        @catalog = nil
    end

    def execute_postrun_command
        execute_from_setting(:postrun_command)
    end

    def execute_prerun_command
        execute_from_setting(:prerun_command)
    end

    # Initialize and load storage
    def dostorage
        begin
            Puppet::Util::Storage.load
            @compile_time ||= Puppet::Util::Storage.cache(:configuration)[:compile_time]
        rescue => detail
            puts detail.backtrace if Puppet[:trace]
            Puppet.err "Corrupt state file #{Puppet[:statefile]}: #{detail}"
            begin
                ::File.unlink(Puppet[:statefile])
                retry
            rescue => detail
                raise Puppet::Error.new("Cannot remove #{Puppet[:statefile]}: #{detail}")
            end
        end
    end

    # Just so we can specify that we are "the" instance.
    def initialize
        Puppet.settings.use(:main, :ssl, :agent)

        self.class.instance = self
        @running = false
        @splayed = false
    end

    def initialize_report
        Puppet::Transaction::Report.new
    end

    # Prepare for catalog retrieval.  Downloads everything necessary, etc.
    def prepare
        dostorage()

        download_plugins()

        download_fact_plugins()

        execute_prerun_command
    end

    # Get the remote catalog, yo.  Returns nil if no catalog can be found.
    def retrieve_catalog
        if Puppet::Resource::Catalog.indirection.terminus_class == :rest
            # This is a bit complicated.  We need the serialized and escaped facts,
            # and we need to know which format they're encoded in.  Thus, we
            # get a hash with both of these pieces of information.
            fact_options = facts_for_uploading()
        else
            fact_options = {}
        end

        # First try it with no cache, then with the cache.
        unless (Puppet[:use_cached_catalog] and result = retrieve_catalog_from_cache(fact_options)) or result = retrieve_new_catalog(fact_options)
            if ! Puppet[:usecacheonfailure]
                Puppet.warning "Not using cache on failed catalog"
                return nil
            end
            result = retrieve_catalog_from_cache(fact_options)
        end

        return nil unless result

        convert_catalog(result, @duration)
    end

    # Convert a plain resource catalog into our full host catalog.
    def convert_catalog(result, duration)
        catalog = result.to_ral
        catalog.finalize
        catalog.retrieval_duration = duration
        catalog.write_class_file
        catalog
    end

    # The code that actually runs the catalog.
    # This just passes any options on to the catalog,
    # which accepts :tags and :ignoreschedules.
    def run(options = {})
        begin
            prepare()
        rescue SystemExit,NoMemoryError
            raise
        rescue Exception => detail
            puts detail.backtrace if Puppet[:trace]
            Puppet.err "Failed to prepare catalog: #{detail}"
        end

        options[:report] ||= initialize_report()
        report = options[:report]
        Puppet::Util::Log.newdestination(report)

        if catalog = options[:catalog]
            options.delete(:catalog)
        elsif ! catalog = retrieve_catalog
            Puppet.err "Could not retrieve catalog; skipping run"
            return
        end

        transaction = nil

        begin
            benchmark(:notice, "Finished catalog run") do
                transaction = catalog.apply(options)
            end
            report
        rescue => detail
            puts detail.backtrace if Puppet[:trace]
            Puppet.err "Failed to apply catalog: #{detail}"
            return
        end
    ensure
        # Now close all of our existing http connections, since there's no
        # reason to leave them lying open.
        Puppet::Network::HttpPool.clear_http_instances
        execute_postrun_command

        Puppet::Util::Log.close(report)

        send_report(report, transaction)
    end

    def send_report(report, trans = nil)
        trans.generate_report if trans
        puts report.summary if Puppet[:summarize]
        report.save() if Puppet[:report]
    rescue => detail
        puts detail.backtrace if Puppet[:trace]
        Puppet.err "Could not send report: #{detail}"
    end

    private

    def self.timeout
        timeout = Puppet[:configtimeout]
        case timeout
        when String
            if timeout =~ /^\d+$/
                timeout = Integer(timeout)
            else
                raise ArgumentError, "Configuration timeout must be an integer"
            end
        when Integer # nothing
        else
            raise ArgumentError, "Configuration timeout must be an integer"
        end

        timeout
    end

    def execute_from_setting(setting)
        return if (command = Puppet[setting]) == ""

        begin
            Puppet::Util.execute([command])
        rescue => detail
            raise CommandHookError, "Could not run command from #{setting}: #{detail}"
        end
    end

    def retrieve_catalog_from_cache(fact_options)
        result = nil
        @duration = thinmark do
            result = Puppet::Resource::Catalog.find(Puppet[:certname], fact_options.merge(:ignore_terminus => true))
        end
        Puppet.notice "Using cached catalog"
        result
    rescue => detail
        puts detail.backtrace if Puppet[:trace]
        Puppet.err "Could not retrieve catalog from cache: #{detail}"
        return nil
    end

    def retrieve_new_catalog(fact_options)
        result = nil
        @duration = thinmark do
            result = Puppet::Resource::Catalog.find(Puppet[:certname], fact_options.merge(:ignore_cache => true))
        end
        result
    rescue SystemExit,NoMemoryError
        raise
    rescue Exception => detail
        puts detail.backtrace if Puppet[:trace]
        Puppet.err "Could not retrieve catalog from remote server: #{detail}"
        return nil
    end
end