summaryrefslogtreecommitdiffstats
path: root/lib/puppet/client.rb
blob: cbb7dbedc8ec4c4c9e6d6991008988f20fcafc70 (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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
#!/usr/local/bin/ruby -w

# the available clients

require 'puppet'
require 'puppet/sslcertificates'
require 'puppet/type'
require 'facter'
require 'openssl'
require 'puppet/transaction'
require 'puppet/transportable'
require 'puppet/metric'
require 'puppet/daemon'
require 'puppet/server'
require 'puppet/base64'

$noclientnetworking = false
begin
    require 'webrick'
    require 'cgi'
    require 'xmlrpc/client'
    require 'xmlrpc/server'
rescue LoadError => detail
    $noclientnetworking = detail
end

module Puppet
    class NetworkClientError < RuntimeError; end
    class ClientError < RuntimeError; end
    #---------------------------------------------------------------
    if $noclientnetworking
        Puppet.err "Could not load client network libs: %s" % $noclientnetworking
    else
        class NetworkClient < XMLRPC::Client
            #include Puppet::Daemon

            # add the methods associated with each namespace
            Puppet::Server::Handler.each { |handler|
                interface = handler.interface
                namespace = interface.prefix

                interface.methods.each { |ary|
                    method = ary[0]
                    Puppet.info "Defining %s.%s" % [namespace, method]
                    self.send(:define_method,method) { |*args|
                        #Puppet.info "peer cert is %s" % @http.peer_cert
                        #Puppet.info "cert is %s" % @http.cert
                        begin
                            call("%s.%s" % [namespace, method.to_s],*args)
                        rescue OpenSSL::SSL::SSLError => detail
                            Puppet.err "Could not call %s.%s: Untrusted certificates" %
                                [namespace, method]
                            raise NetworkClientError,
                                "Certificates were not trusted"
                        rescue XMLRPC::FaultException => detail
                            Puppet.err "Could not call %s.%s: %s" %
                                [namespace, method, detail.faultString]
                            raise NetworkClientError,
                                "XMLRPC Error: %s" % detail.faultString
                        #rescue => detail
                        #    Puppet.err "Could not call %s.%s: %s" %
                        #        [namespace, method, detail.inspect]
                        #    raise NetworkClientError.new(detail.to_s)
                        end
                    }
                }
            }

            def ca_file=(cafile)
                @http.ca_file = cafile
                store = OpenSSL::X509::Store.new
                cacert = OpenSSL::X509::Certificate.new(
                    File.read(cafile)
                )
                store.add_cert(cacert) 
                store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
                @http.cert_store = store
            end

            def cert=(cert)
                Puppet.info "Adding certificate"
                @http.cert = cert
                @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
            end

            def key=(key)
                @http.key = key
            end

            def initialize(hash)
                hash[:Path] ||= "/RPC2"
                hash[:Server] ||= "localhost"
                hash[:Port] ||= Puppet[:masterport]

                super(
                    hash[:Server],
                    hash[:Path],
                    hash[:Port],
                    nil, # proxy_host
                    nil, # proxy_port
                    nil, # user
                    nil, # password
                    true # use_ssl
                )

                if hash[:Certificate]
                    self.cert = hash[:Certificate]
                else
                    Puppet.err "No certificate; running with reduced functionality."
                end

                if hash[:Key]
                    self.key = hash[:Key]
                end

                if hash[:CAFile]
                    self.ca_file = hash[:CAFile]
                end

                # from here, i need to add the key, cert, and ca cert
                # and reorgize how i start the client
            end

            def local
                false
            end
        end
    end

    # FIXME this still isn't a good design, because none of the handlers overlap
    # so i could just as easily include them all in the main module
    # but at least it's better organized for now
    class Client
        include Puppet

        # FIXME the cert stuff should only come up with networking, so it
        # should be in the network client, not the normal client
        # but if i do that, it's hard to tell whether the certs have been initialized
        include Puppet::Daemon
        attr_reader :local, :secureinit

        class << self
            attr_reader :drivername
        end

        def initcerts
            unless self.readcert
                unless self.requestcert
                    return nil
                end
            end

            # unless we have a driver, we're a local client and we can't add
            # certs anyway, so it doesn't matter
            unless @driver
                return true
            end

            self.setcerts
        end

        def initialize(hash)
            # to whom do we connect?
            @server = nil
            @nil = nil
            @secureinit = hash[:NoSecureInit] || true

            if hash.include?(:FQDN)
                @fqdn = hash[:FQDN]
            else
                self.fqdn
            end

            if hash.include?(:Cache)
                @cache = hash[:Cache]
            else
                @cache = true
            end

            driverparam = self.class.drivername
            if hash.include?(:Server)
                if $noclientnetworking
                    raise NetworkClientError.new("Networking not available: %s" %
                        $nonetworking)
                end

                args = {:Server => hash[:Server]}
                args[:Port] = hash[:Port] || Puppet[:masterport]

                if self.readcert
                    args[:Certificate] = @cert
                    args[:Key] = @key
                    args[:CAFile] = @cacertfile
                end

                @driver = Puppet::NetworkClient.new(args)
                @local = false
            elsif hash.include?(driverparam)
                @driver = hash[driverparam]
                @local = true
            else
                raise ClientError, "%s must be passed a Server or %s" %
                    [self.class, driverparam]
            end
        end

        def setcerts
            @driver.cert = @cert
            @driver.key = @key
            @driver.ca_file = @cacertfile
        end

        class MasterClient < Puppet::Client
            @drivername = :Master

            def self.facts
                facts = {}
                Facter.each { |name,fact|
                    facts[name] = fact
                }

                facts
            end

            # this method is how the client receives the tree of Transportable
            # objects
            # for now, just descend into the tree and perform and necessary
            # manipulations
            def apply
                unless defined? @objects
                    raise Puppet::Error, "Cannot apply; objects not defined"
                end

                begin
                    Puppet::Storage.init
                    Puppet::Storage.load
                rescue => detail
                    Puppet.err "Corrupt state file %s" % Puppet[:checksumfile]
                    begin
                        File.unlink(Puppet[:checksumfile])
                        retry
                    rescue => detail
                        raise Puppet::Error.new("Cannot remove %s: %s" %
                            [Puppet[statefile], detail])
                    end
                end

                # FIXME this should be in getconfig, not apply
                container = @objects.to_type
                #if @local
                #    container = @objects.to_type
                #else
                #    container = Marshal::load(@objects).to_type
                #end

                # this is a gross hack... but i don't see a good way around it
                # set all of the variables to empty
                Puppet::Transaction.init

                # for now we just evaluate the top-level container, but eventually
                # there will be schedules and such associated with each object,
                # and probably with the container itself
                transaction = container.evaluate
                #transaction = Puppet::Transaction.new(objects)
                transaction.toplevel = true
                transaction.evaluate
                Puppet::Metric.gather
                Puppet::Metric.tally
                if Puppet[:rrdgraph] == true
                    Metric.store
                    Metric.graph
                end
                Puppet::Storage.store

                return transaction
            end

            def getconfig
                #client.loadproperty('files/sslclient.properties')
                Puppet.debug("getting config")

                facts = self.class.facts

                unless facts.length > 0
                    raise Puppet::ClientError.new(
                        "Could not retrieve any facts"
                    )
                end

                objects = nil
                if @local
                    objects = @driver.getconfig(facts)

                    if objects == ""
                        raise Puppet::Error, "Could not retrieve configuration"
                    end
                else
                    textfacts = CGI.escape(Marshal::dump(facts))

                    # error handling for this is done in the network client
                    textobjects = @driver.getconfig(textfacts)

                    unless textobjects == ""
                        begin
                            textobjects = CGI.unescape(textobjects)
                        rescue => detail
                            raise Puppet::Error, "Could not CGI.unescape configuration"
                        end
                    end

                    if @cache
                        if textobjects == ""
                            if FileTest.exists?(Puppet[:localconfig])
                                textobjects = File.read(Puppet[:localconfig])
                            else
                                raise Puppet::Error.new(
                                    "Cannot connect to server and there is no cached configuration"
                                )
                            end
                        else
                            # we store the config so that if we can't connect next time, we
                            # can just run against the most recently acquired copy
                            confdir = File.dirname(Puppet[:localconfig])
                            unless FileTest.exists?(confdir)
                                Puppet.recmkdir(confdir, 0770)
                            end
                            File.open(Puppet[:localconfig], "w", 0660) { |f|
                                f.print textobjects
                            }
                        end
                    elsif textobjects == ""
                        raise Puppet::Error, "Could not retrieve configuration"
                    end

                    begin
                        objects = Marshal::load(textobjects)
                    rescue => detail
                        raise Puppet::Error.new("Could not understand configuration")
                    end
                end
                if objects.is_a?(Puppet::TransBucket)
                    @objects = objects
                else
                    Puppet.warning objects.inspect
                    raise NetworkClientError.new(objects.class)
                end
            end
        end

        class Dipper < Puppet::Client
            @drivername = :Bucket

            def initialize(hash = {})
                if hash.include?(:Path)
                    bucket = Puppet::Server::FileBucket.new(
                        :Bucket => hash[:Path]
                    )
                    hash.delete(:Path)
                    hash[:Bucket] = bucket
                end

                super(hash)
            end

            def backup(file)
                unless FileTest.exists?(file)
                    raise(BucketError, "File %s does not exist" % file, caller)
                end
                contents = File.open(file) { |of| of.read }

                string = Base64.encode64(contents)
                #puts "string is created"

                sum = @driver.addfile(string,file)
                #puts "file %s is added" % file
                return sum
            end

            def restore(file,sum)
                restore = true
                if FileTest.exists?(file)
                    contents = File.open(file) { |of| of.read }

                    cursum = Digest::MD5.hexdigest(contents)

                    # if the checksum has changed...
                    # this might be extra effort
                    if cursum == sum
                        restore = false
                    end
                end

                if restore
                    #puts "Restoring %s" % file
                    if tmp = @driver.getfile(sum)
                        newcontents = Base64.decode64(tmp)
                        newsum = Digest::MD5.hexdigest(newcontents)
                        File.open(file,File::WRONLY|File::TRUNC) { |of|
                            of.print(newcontents)
                        }
                    else
                        Puppet.err "Could not find file with checksum %s" % sum
                        return nil
                    end
                    #puts "Done"
                    return newsum
                else
                    return nil
                end

            end
        end

        # unlike the other client classes (again, this design sucks) this class
        # is basically just a proxy class -- it calls its methods on the driver
        # and that's about it
        class ProxyClient < Puppet::Client
            def self.mkmethods
                interface = @handler.interface
                namespace = interface.prefix

                interface.methods.each { |ary|
                    method = ary[0]
                    Puppet.debug "%s: defining %s.%s" % [self, namespace, method]
                    self.send(:define_method,method) { |*args|
                        begin
                            @driver.send(method, *args)
                        rescue XMLRPC::FaultException => detail
                            Puppet.err "Could not call %s.%s: %s" %
                                [namespace, method, detail.faultString]
                            raise NetworkClientError,
                                "XMLRPC Error: %s" % detail.faultString
                        end
                    }
                }
            end
        end

        class FileClient < Puppet::Client::ProxyClient
            @drivername = :FileServer

            # set up the appropriate interface methods
            @handler = Puppet::Server::FileServer

            self.mkmethods

            def initialize(hash = {})
                if hash.include?(:FileServer)
                    unless hash[:FileServer].is_a?(Puppet::Server::FileServer)
                        raise Puppet::DevError, "Must pass an actual FS object"
                    end
                end

                super(hash)
            end
        end

        class CAClient < Puppet::Client::ProxyClient
            @drivername = :CA

            # set up the appropriate interface methods
            @handler = Puppet::Server::CA
            self.mkmethods

            def initialize(hash = {})
                if hash.include?(:CA)
                    hash[:CA] = Puppet::Server::CA.new()
                end

                super(hash)
            end
        end

        class StatusClient < Puppet::Client::ProxyClient
            # set up the appropriate interface methods
            @handler = Puppet::Server::ServerStatus
            self.mkmethods
        end

    end
#---------------------------------------------------------------
end

# $Id$