diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index f1b1ac74b..360fd5a44 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -1,942 +1,947 @@ # The majority of the system configuration parameters are set in this file. module Puppet setdefaults(:main, :confdir => [Puppet.run_mode.conf_dir, "The main Puppet configuration directory. The default for this parameter is calculated based on the user. If the process is running as root or the user that Puppet is supposed to run as, it defaults to a system directory, but if it's running as any other user, it defaults to being in the user's home directory."], :vardir => [Puppet.run_mode.var_dir, "Where Puppet stores dynamic and growing data. The default for this parameter is calculated specially, like `confdir`_."], :name => [Puppet.application_name.to_s, "The name of the application, if we are running as one. The default is essentially $0 without the path or `.rb`."], :run_mode => [Puppet.run_mode.name.to_s, "The effective 'run mode' of the application: master, agent, or user."] ) setdefaults(:main, :logdir => Puppet.run_mode.logopts) setdefaults(:main, :trace => [false, "Whether to print stack traces on some errors"], :autoflush => { :default => false, :desc => "Whether log files should always flush to disk.", :hook => proc { |value| Log.autoflush = value } }, :syslogfacility => ["daemon", "What syslog facility to use when logging to syslog. Syslog has a fixed list of valid facilities, and you must choose one of those; you cannot just make one up."], :statedir => { :default => "$vardir/state", :mode => 01755, :desc => "The directory where Puppet state is stored. Generally, this directory can be removed without causing harm (although it might result in spurious service restarts)." }, :rundir => { :default => Puppet.run_mode.run_dir, :mode => 01777, :desc => "Where Puppet PID files are kept." }, :genconfig => [false, "Whether to just print a configuration to stdout and exit. Only makes sense when used interactively. Takes into account arguments specified on the CLI."], :genmanifest => [false, "Whether to just print a manifest to stdout and exit. Only makes sense when used interactively. Takes into account arguments specified on the CLI."], :configprint => ["", "Print the value of a specific configuration parameter. If a parameter is provided for this, then the value is printed and puppet exits. Comma-separate multiple values. For a list of all values, specify 'all'. This feature is only available in Puppet versions higher than 0.18.4."], :color => { :default => (Puppet.features.microsoft_windows? ? "false" : "ansi"), :type => :setting, :desc => "Whether to use colors when logging to the console. Valid values are `ansi` (equivalent to `true`), `html` (mostly used during testing with TextMate), and `false`, which produces no color.", }, :mkusers => [false, "Whether to create the necessary user and group that puppet agent will run as."], :manage_internal_file_permissions => [true, "Whether Puppet should manage the owner, group, and mode of files it uses internally" ], :onetime => {:default => false, :desc => "Run the configuration once, rather than as a long-running daemon. This is useful for interactively running puppetd.", :short => 'o' }, :path => {:default => "none", :desc => "The shell search path. Defaults to whatever is inherited from the parent process.", :call_on_define => true, # Call our hook with the default value, so we always get the libdir set. :hook => proc do |value| ENV["PATH"] = "" if ENV["PATH"].nil? ENV["PATH"] = value unless value == "none" paths = ENV["PATH"].split(File::PATH_SEPARATOR) %w{/usr/sbin /sbin}.each do |path| ENV["PATH"] += File::PATH_SEPARATOR + path unless paths.include?(path) end value end }, :libdir => {:default => "$vardir/lib", :desc => "An extra search path for Puppet. This is only useful for those files that Puppet will load on demand, and is only guaranteed to work for those cases. In fact, the autoload mechanism is responsible for making sure this directory is in Ruby's search path", :call_on_define => true, # Call our hook with the default value, so we always get the libdir set. :hook => proc do |value| $LOAD_PATH.delete(@oldlibdir) if defined?(@oldlibdir) and $LOAD_PATH.include?(@oldlibdir) @oldlibdir = value $LOAD_PATH << value end }, :ignoreimport => [false, "A parameter that can be used in commit hooks, since it enables you to parse-check a single file rather than requiring that all files exist."], :authconfig => [ "$confdir/namespaceauth.conf", "The configuration file that defines the rights to the different namespaces and methods. This can be used as a coarse-grained authorization system for both `puppet agent` and `puppet master`." ], :environment => {:default => "production", :desc => "The environment Puppet is running in. For clients (e.g., `puppet agent`) this determines the environment itself, which is used to find modules and much more. For servers (i.e., `puppet master`) this provides the default environment for nodes we know nothing about." }, :diff_args => ["-u", "Which arguments to pass to the diff command when printing differences between files."], :diff => ["diff", "Which diff command to use when printing differences between files."], :show_diff => [false, "Whether to print a contextual diff when files are being replaced. The diff is printed on stdout, so this option is meaningless unless you are running Puppet interactively. This feature currently requires the `diff/lcs` Ruby library."], :daemonize => { :default => (Puppet.features.microsoft_windows? ? false : true), :desc => "Send the process into the background. This is the default.", :short => "D", :hook => proc do |value| if value and Puppet.features.microsoft_windows? raise "Cannot daemonize on Windows" end end }, :maximum_uid => [4294967290, "The maximum allowed UID. Some platforms use negative UIDs but then ship with tools that do not know how to handle signed ints, so the UIDs show up as huge numbers that can then not be fed back into the system. This is a hackish way to fail in a slightly more useful way when that happens."], :route_file => ["$confdir/routes.yaml", "The YAML file containing indirector route configuration."], :node_terminus => ["plain", "Where to find information about nodes."], :catalog_terminus => ["compiler", "Where to get node catalogs. This is useful to change if, for instance, you'd like to pre-compile catalogs and store them in memcached or some other easily-accessed store."], :facts_terminus => { :default => Puppet.application_name.to_s == "master" ? 'yaml' : 'facter', :desc => "The node facts terminus.", :hook => proc do |value| require 'puppet/node/facts' # Cache to YAML if we're uploading facts away if %w[rest inventory_service].include? value.to_s Puppet::Node::Facts.indirection.cache_class = :yaml end end }, :inventory_terminus => [ "$facts_terminus", "Should usually be the same as the facts terminus" ], :httplog => { :default => "$logdir/http.log", :owner => "root", :mode => 0640, :desc => "Where the puppet agent web server logs." }, :http_proxy_host => ["none", "The HTTP proxy host to use for outgoing connections. Note: You may need to use a FQDN for the server hostname when using a proxy."], :http_proxy_port => [3128, "The HTTP proxy port to use for outgoing connections"], :filetimeout => [ 15, "The minimum time to wait (in seconds) between checking for updates in configuration files. This timeout determines how quickly Puppet checks whether a file (such as manifests or templates) has changed on disk." ], :queue_type => ["stomp", "Which type of queue to use for asynchronous processing."], :queue_type => ["stomp", "Which type of queue to use for asynchronous processing."], :queue_source => ["stomp://localhost:61613/", "Which type of queue to use for asynchronous processing. If your stomp server requires authentication, you can include it in the URI as long as your stomp client library is at least 1.1.1"], :async_storeconfigs => {:default => false, :desc => "Whether to use a queueing system to provide asynchronous database integration. Requires that `puppetqd` be running and that 'PSON' support for ruby be installed.", :hook => proc do |value| if value # This reconfigures the terminii for Node, Facts, and Catalog Puppet.settings[:storeconfigs] = true # But then we modify the configuration Puppet::Resource::Catalog.indirection.cache_class = :queue else raise "Cannot disable asynchronous storeconfigs in a running process" end end }, :thin_storeconfigs => {:default => false, :desc => "Boolean; wether storeconfigs store in the database only the facts and exported resources. If true, then storeconfigs performance will be higher and still allow exported/collected resources, but other usage external to Puppet might not work", :hook => proc do |value| Puppet.settings[:storeconfigs] = true if value end }, :config_version => ["", "How to determine the configuration version. By default, it will be the time that the configuration is parsed, but you can provide a shell script to override how the version is determined. The output of this script will be added to every log message in the reports, allowing you to correlate changes on your hosts to the source version on the server."], :zlib => [true, "Boolean; whether to use the zlib library", ], :prerun_command => ["", "A command to run before every agent run. If this command returns a non-zero return code, the entire Puppet run will fail."], :postrun_command => ["", "A command to run after every agent run. If this command returns a non-zero return code, the entire Puppet run will be considered to have failed, even though it might have performed work during the normal run."], :freeze_main => [false, "Freezes the 'main' class, disallowing any code to be added to it. This essentially means that you can't have any code outside of a node, class, or definition other than in the site manifest."] ) hostname = Facter["hostname"].value domain = Facter["domain"].value if domain and domain != "" fqdn = [hostname, domain].join(".") else fqdn = hostname end Puppet.setdefaults( :main, # We have to downcase the fqdn, because the current ssl stuff (as oppsed to in master) doesn't have good facilities for # manipulating naming. :certname => {:default => fqdn.downcase, :desc => "The name to use when handling certificates. Defaults to the fully qualified domain name.", :call_on_define => true, # Call our hook with the default value, so we're always downcased :hook => proc { |value| raise(ArgumentError, "Certificate names must be lower case; see #1168") unless value == value.downcase }}, :certdnsnames => { :default => '', :hook => proc do |value| unless value.nil? or value == '' then Puppet.warning < < { :default => '', :desc => < { :default => "$ssldir/certs", :owner => "service", :desc => "The certificate directory." }, :ssldir => { :default => "$confdir/ssl", :mode => 0771, :owner => "service", :desc => "Where SSL certificates are kept." }, :publickeydir => { :default => "$ssldir/public_keys", :owner => "service", :desc => "The public key directory." }, :requestdir => { :default => "$ssldir/certificate_requests", :owner => "service", :desc => "Where host certificate requests are stored." }, :privatekeydir => { :default => "$ssldir/private_keys", :mode => 0750, :owner => "service", :desc => "The private key directory." }, :privatedir => { :default => "$ssldir/private", :mode => 0750, :owner => "service", :desc => "Where the client stores private certificate information." }, :passfile => { :default => "$privatedir/password", :mode => 0640, :owner => "service", :desc => "Where puppet agent stores the password for its private key. Generally unused." }, :hostcsr => { :default => "$ssldir/csr_$certname.pem", :mode => 0644, :owner => "service", :desc => "Where individual hosts store and look for their certificate requests." }, :hostcert => { :default => "$certdir/$certname.pem", :mode => 0644, :owner => "service", :desc => "Where individual hosts store and look for their certificates." }, :hostprivkey => { :default => "$privatekeydir/$certname.pem", :mode => 0600, :owner => "service", :desc => "Where individual hosts store and look for their private key." }, :hostpubkey => { :default => "$publickeydir/$certname.pem", :mode => 0644, :owner => "service", :desc => "Where individual hosts store and look for their public key." }, :localcacert => { :default => "$certdir/ca.pem", :mode => 0644, :owner => "service", :desc => "Where each client stores the CA certificate." }, :hostcrl => { :default => "$ssldir/crl.pem", :mode => 0644, :owner => "service", :desc => "Where the host's certificate revocation list can be found. This is distinct from the certificate authority's CRL." }, :certificate_revocation => [true, "Whether certificate revocation should be supported by downloading a Certificate Revocation List (CRL) to all clients. If enabled, CA chaining will almost definitely not work."] ) setdefaults( :ca, :ca_name => ["Puppet CA: $certname", "The name to use the Certificate Authority certificate."], :cadir => { :default => "$ssldir/ca", :owner => "service", :group => "service", :mode => 0770, :desc => "The root directory for the certificate authority." }, :cacert => { :default => "$cadir/ca_crt.pem", :owner => "service", :group => "service", :mode => 0660, :desc => "The CA certificate." }, :cakey => { :default => "$cadir/ca_key.pem", :owner => "service", :group => "service", :mode => 0660, :desc => "The CA private key." }, :capub => { :default => "$cadir/ca_pub.pem", :owner => "service", :group => "service", :desc => "The CA public key." }, :cacrl => { :default => "$cadir/ca_crl.pem", :owner => "service", :group => "service", :mode => 0664, :desc => "The certificate revocation list (CRL) for the CA. Will be used if present but otherwise ignored.", :hook => proc do |value| if value == 'false' Puppet.warning "Setting the :cacrl to 'false' is deprecated; Puppet will just ignore the crl if yours is missing" end end }, :caprivatedir => { :default => "$cadir/private", :owner => "service", :group => "service", :mode => 0770, :desc => "Where the CA stores private certificate information." }, :csrdir => { :default => "$cadir/requests", :owner => "service", :group => "service", :desc => "Where the CA stores certificate requests" }, :signeddir => { :default => "$cadir/signed", :owner => "service", :group => "service", :mode => 0770, :desc => "Where the CA stores signed certificates." }, :capass => { :default => "$caprivatedir/ca.pass", :owner => "service", :group => "service", :mode => 0660, :desc => "Where the CA stores the password for the private key" }, :serial => { :default => "$cadir/serial", :owner => "service", :group => "service", :mode => 0644, :desc => "Where the serial number for certificates is stored." }, :autosign => { :default => "$confdir/autosign.conf", :mode => 0644, :desc => "Whether to enable autosign. Valid values are true (which autosigns any key request, and is a very bad idea), false (which never autosigns any key request), and the path to a file, which uses that configuration file to determine which keys to sign."}, :allow_duplicate_certs => [false, "Whether to allow a new certificate request to overwrite an existing certificate."], :ca_days => ["", "How long a certificate should be valid. This parameter is deprecated, use ca_ttl instead"], :ca_ttl => ["5y", "The default TTL for new certificates; valid values must be an integer, optionally followed by one of the units 'y' (years of 365 days), 'd' (days), 'h' (hours), or 's' (seconds). The unit defaults to seconds. If this parameter is set, ca_days is ignored. Examples are '3600' (one hour) and '1825d', which is the same as '5y' (5 years) "], :ca_md => ["md5", "The type of hash used in certificates."], :req_bits => [2048, "The bit length of the certificates."], :keylength => [1024, "The bit length of keys."], :cert_inventory => { :default => "$cadir/inventory.txt", :mode => 0644, :owner => "service", :group => "service", :desc => "A Complete listing of all certificates" } ) # Define the config default. setdefaults( Puppet.settings[:name], :config => ["$confdir/puppet.conf", "The configuration file for #{Puppet[:name]}."], :pidfile => ["$rundir/$name.pid", "The pid file"], :bindaddress => ["", "The address a listening server should bind to. Mongrel servers default to 127.0.0.1 and WEBrick defaults to 0.0.0.0."], :servertype => {:default => "webrick", :desc => "The type of server to use. Currently supported options are webrick and mongrel. If you use mongrel, you will need a proxy in front of the process or processes, since Mongrel cannot speak SSL.", :call_on_define => true, # Call our hook with the default value, so we always get the correct bind address set. :hook => proc { |value| value == "webrick" ? Puppet.settings[:bindaddress] = "0.0.0.0" : Puppet.settings[:bindaddress] = "127.0.0.1" if Puppet.settings[:bindaddress] == "" } } ) setdefaults(:master, :user => ["puppet", "The user puppet master should run as."], :group => ["puppet", "The group puppet master should run as."], :manifestdir => ["$confdir/manifests", "Where puppet master looks for its manifests."], :manifest => ["$manifestdir/site.pp", "The entry-point manifest for puppet master."], :code => ["", "Code to parse directly. This is essentially only used by `puppet`, and should only be set if you're writing your own Puppet executable"], :masterlog => { :default => "$logdir/puppetmaster.log", :owner => "service", :group => "service", :mode => 0660, :desc => "Where puppet master logs. This is generally not used, since syslog is the default log destination." }, :masterhttplog => { :default => "$logdir/masterhttp.log", :owner => "service", :group => "service", :mode => 0660, :create => true, :desc => "Where the puppet master web server logs." }, :masterport => [8140, "Which port puppet master listens on."], :node_name => ["cert", "How the puppet master determines the client's identity and sets the 'hostname', 'fqdn' and 'domain' facts for use in the manifest, in particular for determining which 'node' statement applies to the client. Possible values are 'cert' (use the subject's CN in the client's certificate) and 'facter' (use the hostname that the client reported in its facts)"], :bucketdir => { :default => "$vardir/bucket", :mode => 0750, :owner => "service", :group => "service", :desc => "Where FileBucket files are stored." }, :rest_authconfig => [ "$confdir/auth.conf", "The configuration file that defines the rights to the different rest indirections. This can be used as a fine-grained authorization system for `puppet master`." ], :ca => [true, "Wether the master should function as a certificate authority."], :modulepath => { :default => "$confdir/modules#{File::PATH_SEPARATOR}/usr/share/puppet/modules", :desc => "The search path for modules as a list of directories separated by the '#{File::PATH_SEPARATOR}' character.", :type => :setting # We don't want this to be considered a file, since it's multiple files. }, :ssl_client_header => ["HTTP_X_CLIENT_DN", "The header containing an authenticated client's SSL DN. Only used with Mongrel. This header must be set by the proxy to the authenticated client's SSL DN (e.g., `/CN=puppet.puppetlabs.com`). See http://projects.puppetlabs.com/projects/puppet/wiki/Using_Mongrel for more information."], :ssl_client_verify_header => ["HTTP_X_CLIENT_VERIFY", "The header containing the status message of the client verification. Only used with Mongrel. This header must be set by the proxy to 'SUCCESS' if the client successfully authenticated, and anything else otherwise. See http://projects.puppetlabs.com/projects/puppet/wiki/Using_Mongrel for more information."], # To make sure this directory is created before we try to use it on the server, we need # it to be in the server section (#1138). :yamldir => {:default => "$vardir/yaml", :owner => "service", :group => "service", :mode => "750", :desc => "The directory in which YAML data is stored, usually in a subdirectory."}, :server_datadir => {:default => "$vardir/server_data", :owner => "service", :group => "service", :mode => "750", :desc => "The directory in which serialized data is stored, usually in a subdirectory."}, :reports => ["store", "The list of reports to generate. All reports are looked for in `puppet/reports/name.rb`, and multiple report names should be comma-separated (whitespace is okay)." ], :reportdir => {:default => "$vardir/reports", :mode => 0750, :owner => "service", :group => "service", :desc => "The directory in which to store reports received from the client. Each client gets a separate subdirectory."}, :reporturl => ["http://localhost:3000/reports/upload", "The URL used by the http reports processor to send reports"], :fileserverconfig => ["$confdir/fileserver.conf", "Where the fileserver configuration is stored."], :strict_hostname_checking => [false, "Whether to only search for the complete hostname as it is in the certificate when searching for node information in the catalogs."] ) setdefaults(:metrics, :rrddir => {:default => "$vardir/rrd", :mode => 0750, :owner => "service", :group => "service", :desc => "The directory where RRD database files are stored. Directories for each reporting host will be created under this directory." }, :rrdinterval => ["$runinterval", "How often RRD should expect data. This should match how often the hosts report back to the server."] ) setdefaults(:device, :devicedir => {:default => "$vardir/devices", :mode => "750", :desc => "The root directory of devices' $vardir"}, :deviceconfig => ["$confdir/device.conf","Path to the device config file for puppet device"] ) setdefaults(:agent, :node_name_value => { :default => "$certname", :desc => "The explicit value used for the node name for all requests the agent makes to the master. WARNING: This setting is mutually exclusive with node_name_fact. Changing this setting also requires changes to the default auth.conf configuration on the Puppet Master. Please see http://links.puppetlabs.com/node_name_value for more information." }, :node_name_fact => { :default => "", :desc => "The fact name used to determine the node name used for all requests the agent makes to the master. WARNING: This setting is mutually exclusive with node_name_value. Changing this setting also requires changes to the default auth.conf configuration on the Puppet Master. Please see http://links.puppetlabs.com/node_name_fact for more information.", :hook => proc do |value| if !value.empty? and Puppet[:node_name_value] != Puppet[:certname] raise "Cannot specify both the node_name_value and node_name_fact settings" end end }, :localconfig => { :default => "$statedir/localconfig", :owner => "root", :mode => 0660, :desc => "Where puppet agent caches the local configuration. An extension indicating the cache format is added automatically."}, :statefile => { :default => "$statedir/state.yaml", :mode => 0660, :desc => "Where puppet agent and puppet master store state associated with the running configuration. In the case of puppet master, this file reflects the state discovered through interacting with clients." }, :clientyamldir => {:default => "$vardir/client_yaml", :mode => "750", :desc => "The directory in which client-side YAML data is stored."}, :client_datadir => {:default => "$vardir/client_data", :mode => "750", :desc => "The directory in which serialized data is stored on the client."}, :classfile => { :default => "$statedir/classes.txt", :owner => "root", :mode => 0644, :desc => "The file in which puppet agent stores a list of the classes associated with the retrieved configuration. Can be loaded in the separate `puppet` executable using the `--loadclasses` option."}, :resourcefile => { :default => "$statedir/resources.txt", :owner => "root", :mode => 0644, :desc => "The file in which puppet agent stores a list of the resources associated with the retrieved configuration." }, :puppetdlog => { :default => "$logdir/puppetd.log", :owner => "root", :mode => 0640, :desc => "The log file for puppet agent. This is generally not used." }, - :server => ["puppet", "The server to which server puppet agent should connect"], + :server => { + :default => "puppet", + :desc => "The server to which the puppet agent should connect" + }, + :use_srv_records => [true, "Whether the server will search for SRV records in DNS for the current domain."], + :srv_domain => [ "#{domain}", "The domain which will be queried to find the SRV records of servers to use."], :ignoreschedules => [false, "Boolean; whether puppet agent should ignore schedules. This is useful for initial puppet agent runs."], :puppetport => [8139, "Which port puppet agent listens on."], :noop => [false, "Whether puppet agent should be run in noop mode."], :runinterval => [1800, # 30 minutes "How often puppet agent applies the client configuration; in seconds. Note that a runinterval of 0 means \"run continuously\" rather than \"never run.\" If you want puppet agent to never run, you should start it with the `--no-client` option."], :listen => [false, "Whether puppet agent should listen for connections. If this is true, then puppet agent will accept incoming - REST API requests, subject to the default ACLs and the ACLs set in + REST API requests, subject to the default ACLs and the ACLs set in the `rest_authconfig` file. Puppet agent can respond usefully to requests on the `run`, `facts`, `certificate`, and `resource` endpoints."], :ca_server => ["$server", "The server to use for certificate authority requests. It's a separate server because it cannot and does not need to horizontally scale."], :ca_port => ["$masterport", "The port to use for the certificate authority."], :catalog_format => { :default => "", :desc => "(Deprecated for 'preferred_serialization_format') What format to use to dump the catalog. Only supports 'marshal' and 'yaml'. Only matters on the client, since it asks the server for a specific format.", :hook => proc { |value| if value Puppet.warning "Setting 'catalog_format' is deprecated; use 'preferred_serialization_format' instead." Puppet.settings[:preferred_serialization_format] = value end } }, :preferred_serialization_format => ["pson", "The preferred means of serializing ruby instances for passing over the wire. This won't guarantee that all instances will be serialized using this method, since not all classes can be guaranteed to support this format, but it will be used for all classes that support it."], :puppetdlockfile => [ "$statedir/puppetdlock", "A lock file to temporarily stop puppet agent from doing anything."], :usecacheonfailure => [true, "Whether to use the cached configuration when the remote configuration will not compile. This option is useful for testing new configurations, where you want to fix the broken configuration rather than reverting to a known-good one." ], :use_cached_catalog => [false, "Whether to only use the cached catalog rather than compiling a new catalog on every run. Puppet can be run with this enabled by default and then selectively disabled when a recompile is desired."], :ignorecache => [false, "Ignore cache and always recompile the configuration. This is useful for testing new configurations, where the local cache may in fact be stale even if the timestamps are up to date - if the facts change or if the server changes." ], :downcasefacts => [false, "Whether facts should be made all lowercase when sent to the server."], :dynamicfacts => ["memorysize,memoryfree,swapsize,swapfree", "Facts that are dynamic; these facts will be ignored when deciding whether changed facts should result in a recompile. Multiple facts should be comma-separated."], :splaylimit => ["$runinterval", "The maximum time to delay before runs. Defaults to being the same as the run interval."], :splay => [false, "Whether to sleep for a pseudo-random (but consistent) amount of time before a run."], :clientbucketdir => { :default => "$vardir/clientbucket", :mode => 0750, :desc => "Where FileBucket files are stored locally." }, :configtimeout => [120, "How long the client should wait for the configuration to be retrieved before considering it a failure. This can help reduce flapping if too many clients contact the server at one time." ], :reportserver => { :default => "$server", :call_on_define => false, :desc => "(Deprecated for 'report_server') The server to which to send transaction reports.", :hook => proc do |value| Puppet.settings[:report_server] = value if value end }, :report_server => ["$server", "The server to send transaction reports to." ], :report_port => ["$masterport", "The port to communicate with the report_server." ], :inventory_server => ["$server", "The server to send facts to." ], :inventory_port => ["$masterport", "The port to communicate with the inventory_server." ], :report => [true, "Whether to send reports after every transaction." ], :lastrunfile => { :default => "$statedir/last_run_summary.yaml", :mode => 0660, :desc => "Where puppet agent stores the last run report summary in yaml format." }, :lastrunreport => { :default => "$statedir/last_run_report.yaml", :mode => 0660, :desc => "Where puppet agent stores the last run report in yaml format." }, :graph => [false, "Whether to create dot graph files for the different configuration graphs. These dot files can be interpreted by tools like OmniGraffle or dot (which is part of ImageMagick)."], :graphdir => ["$statedir/graphs", "Where to store dot-outputted graphs."], :http_compression => [false, "Allow http compression in REST communication with the master. This setting might improve performance for agent -> master communications over slow WANs. Your puppet master needs to support compression (usually by activating some settings in a reverse-proxy in front of the puppet master, which rules out webrick). It is harmless to activate this settings if your master doesn't support compression, but if it supports it, this setting might reduce performance on high-speed LANs."] ) setdefaults(:inspect, :archive_files => [false, "During an inspect run, whether to archive files whose contents are audited to a file bucket."], :archive_file_server => ["$server", "During an inspect run, the file bucket server to archive files to if archive_files is set."] ) # Plugin information. setdefaults( :main, :plugindest => ["$libdir", "Where Puppet should store plugins that it pulls down from the central server."], :pluginsource => ["puppet://$server/plugins", "From where to retrieve plugins. The standard Puppet `file` type is used for retrieval, so anything that is a valid file source can be used here."], :pluginsync => [false, "Whether plugins should be synced with the central server."], :pluginsignore => [".svn CVS .git", "What files to ignore when pulling down plugins."] ) # Central fact information. setdefaults( :main, :factpath => {:default => "$vardir/lib/facter#{File::PATH_SEPARATOR}$vardir/facts", :desc => "Where Puppet should look for facts. Multiple directories should be colon-separated, like normal PATH variables.", :call_on_define => true, # Call our hook with the default value, so we always get the value added to facter. :type => :setting, # Don't consider it a file, because it could be multiple colon-separated files :hook => proc { |value| Facter.search(value) if Facter.respond_to?(:search) }}, :factdest => ["$vardir/facts/", "Where Puppet should store facts that it pulls down from the central server."], :factsource => ["puppet://$server/facts/", "From where to retrieve facts. The standard Puppet `file` type is used for retrieval, so anything that is a valid file source can be used here."], :factsync => [false, "Whether facts should be synced with the central server."], :factsignore => [".svn CVS", "What files to ignore when pulling down facts."] ) setdefaults( :tagmail, :tagmap => ["$confdir/tagmail.conf", "The mapping between reporting tags and email addresses."], :sendmail => [which('sendmail') || '', "Where to find the sendmail binary with which to send email."], :reportfrom => ["report@" + [Facter["hostname"].value, Facter["domain"].value].join("."), "The 'from' email address for the reports."], :smtpserver => ["none", "The server through which to send email reports."] ) setdefaults( :rails, :dblocation => { :default => "$statedir/clientconfigs.sqlite3", :mode => 0660, :owner => "service", :group => "service", :desc => "The database cache for client configurations. Used for querying within the language." }, :dbadapter => [ "sqlite3", "The type of database to use." ], :dbmigrate => [ false, "Whether to automatically migrate the database." ], :dbname => [ "puppet", "The name of the database to use." ], :dbserver => [ "localhost", "The database server for caching. Only used when networked databases are used."], :dbport => [ "", "The database password for caching. Only used when networked databases are used."], :dbuser => [ "puppet", "The database user for caching. Only used when networked databases are used."], :dbpassword => [ "puppet", "The database password for caching. Only used when networked databases are used."], :dbconnections => [ '', "The number of database connections for networked databases. Will be ignored unless the value is a positive integer."], :dbsocket => [ "", "The database socket location. Only used when networked databases are used. Will be ignored if the value is an empty string."], :railslog => {:default => "$logdir/rails.log", :mode => 0600, :owner => "service", :group => "service", :desc => "Where Rails-specific logs are sent" }, :rails_loglevel => ["info", "The log level for Rails connections. The value must be a valid log level within Rails. Production environments normally use `info` and other environments normally use `debug`."] ) setdefaults( :couchdb, :couchdb_url => ["http://127.0.0.1:5984/puppet", "The url where the puppet couchdb database will be created"] ) setdefaults( :transaction, :tags => ["", "Tags to use to find resources. If this is set, then only resources tagged with the specified tags will be applied. Values must be comma-separated."], :evaltrace => [false, "Whether each resource should log when it is being evaluated. This allows you to interactively see exactly what is being done."], :summarize => [false, "Whether to print a transaction summary." ] ) setdefaults( :main, :external_nodes => ["none", "An external command that can produce node information. The output must be a YAML dump of a hash, and that hash must have one or both of `classes` and `parameters`, where `classes` is an array and `parameters` is a hash. For unknown nodes, the commands should exit with a non-zero exit code. This command makes it straightforward to store your node mapping information in other data sources like databases."]) setdefaults( :ldap, :ldapnodes => [false, "Whether to search for node configurations in LDAP. See http://projects.puppetlabs.com/projects/puppet/wiki/LDAP_Nodes for more information."], :ldapssl => [false, "Whether SSL should be used when searching for nodes. Defaults to false because SSL usually requires certificates to be set up on the client side."], :ldaptls => [false, "Whether TLS should be used when searching for nodes. Defaults to false because TLS usually requires certificates to be set up on the client side."], :ldapserver => ["ldap", "The LDAP server. Only used if `ldapnodes` is enabled."], :ldapport => [389, "The LDAP port. Only used if `ldapnodes` is enabled."], :ldapstring => ["(&(objectclass=puppetClient)(cn=%s))", "The search string used to find an LDAP node."], :ldapclassattrs => ["puppetclass", "The LDAP attributes to use to define Puppet classes. Values should be comma-separated."], :ldapstackedattrs => ["puppetvar", "The LDAP attributes that should be stacked to arrays by adding the values in all hierarchy elements of the tree. Values should be comma-separated."], :ldapattrs => ["all", "The LDAP attributes to include when querying LDAP for nodes. All returned attributes are set as variables in the top-level scope. Multiple values should be comma-separated. The value 'all' returns all attributes."], :ldapparentattr => ["parentnode", "The attribute to use to define the parent node."], :ldapuser => ["", "The user to use to connect to LDAP. Must be specified as a full DN."], :ldappassword => ["", "The password to use to connect to LDAP."], :ldapbase => ["", "The search base for LDAP searches. It's impossible to provide a meaningful default here, although the LDAP libraries might have one already set. Generally, it should be the 'ou=Hosts' branch under your main directory."] ) setdefaults(:master, :storeconfigs => { :default => false, :desc => "Whether to store each client's configuration, including catalogs, facts, and related data. This also enables the import and export of resources in the Puppet language - a mechanism for exchange resources between nodes. By default this uses ActiveRecord and an SQL database to store and query the data; this, in turn, will depend on Rails being available. You can adjust the backend using the storeconfigs_backend setting.", # Call our hook with the default value, so we always get the libdir set. :call_on_define => true, :hook => proc do |value| require 'puppet/node' require 'puppet/node/facts' if value Puppet.settings[:async_storeconfigs] or Puppet::Resource::Catalog.indirection.cache_class = :store_configs Puppet::Node::Facts.indirection.cache_class = :store_configs Puppet::Node.indirection.cache_class = :store_configs Puppet::Resource.indirection.terminus_class = :store_configs end end }, :storeconfigs_backend => { :default => "active_record", :desc => "Configure the backend terminus used for StoreConfigs. By default, this uses the ActiveRecord store, which directly talks to the database from within the Puppet Master process." } ) # This doesn't actually work right now. setdefaults( :parser, :lexical => [false, "Whether to use lexical scoping (vs. dynamic)."], :templatedir => ["$vardir/templates", "Where Puppet looks for template files. Can be a list of colon-seperated directories." ] ) setdefaults( :puppetdoc, :document_all => [false, "Document all resources"] ) end diff --git a/lib/puppet/indirector/certificate/rest.rb b/lib/puppet/indirector/certificate/rest.rb index 921b85790..1cbf06afc 100644 --- a/lib/puppet/indirector/certificate/rest.rb +++ b/lib/puppet/indirector/certificate/rest.rb @@ -1,15 +1,16 @@ require 'puppet/ssl/certificate' require 'puppet/indirector/rest' class Puppet::SSL::Certificate::Rest < Puppet::Indirector::REST desc "Find and save certificates over HTTP via REST." use_server_setting(:ca_server) use_port_setting(:ca_port) + use_srv_service(:ca) def find(request) return nil unless result = super result.name = request.key unless result.name == request.key result end end diff --git a/lib/puppet/indirector/file_content/rest.rb b/lib/puppet/indirector/file_content/rest.rb index 59975d3ec..1f6bc6eb2 100644 --- a/lib/puppet/indirector/file_content/rest.rb +++ b/lib/puppet/indirector/file_content/rest.rb @@ -1,7 +1,9 @@ require 'puppet/file_serving/content' require 'puppet/indirector/file_content' require 'puppet/indirector/rest' class Puppet::Indirector::FileContent::Rest < Puppet::Indirector::REST desc "Retrieve file contents via a REST HTTP interface." + + use_srv_service(:fileserver) end diff --git a/lib/puppet/indirector/report/rest.rb b/lib/puppet/indirector/report/rest.rb index 601da9edb..a40a9f8e1 100644 --- a/lib/puppet/indirector/report/rest.rb +++ b/lib/puppet/indirector/report/rest.rb @@ -1,7 +1,8 @@ require 'puppet/indirector/rest' class Puppet::Transaction::Report::Rest < Puppet::Indirector::REST desc "Get server report over HTTP via REST." use_server_setting(:report_server) use_port_setting(:report_port) + use_srv_service(:report) end diff --git a/lib/puppet/indirector/request.rb b/lib/puppet/indirector/request.rb index f2b9573f8..98b79f9ca 100644 --- a/lib/puppet/indirector/request.rb +++ b/lib/puppet/indirector/request.rb @@ -1,251 +1,277 @@ require 'cgi' require 'uri' require 'puppet/indirector' require 'puppet/util/pson' +require 'puppet/network/resolver' # This class encapsulates all of the information you need to make an # Indirection call, and as a a result also handles REST calls. It's somewhat # analogous to an HTTP Request object, except tuned for our Indirector. class Puppet::Indirector::Request attr_accessor :key, :method, :options, :instance, :node, :ip, :authenticated, :ignore_cache, :ignore_terminus attr_accessor :server, :port, :uri, :protocol attr_reader :indirection_name OPTION_ATTRIBUTES = [:ip, :node, :authenticated, :ignore_terminus, :ignore_cache, :instance, :environment] # Load json before trying to register. Puppet.features.pson? and ::PSON.register_document_type('IndirectorRequest',self) def self.from_pson(json) raise ArgumentError, "No indirection name provided in json data" unless indirection_name = json['type'] raise ArgumentError, "No method name provided in json data" unless method = json['method'] raise ArgumentError, "No key provided in json data" unless key = json['key'] request = new(indirection_name, method, key, json['attributes']) if instance = json['instance'] klass = Puppet::Indirector::Indirection.instance(request.indirection_name).model if instance.is_a?(klass) request.instance = instance else request.instance = klass.from_pson(instance) end end request end def to_pson(*args) result = { 'document_type' => 'IndirectorRequest', 'data' => { 'type' => indirection_name, 'method' => method, 'key' => key } } data = result['data'] attributes = {} OPTION_ATTRIBUTES.each do |key| next unless value = send(key) attributes[key] = value end options.each do |opt, value| attributes[opt] = value end data['attributes'] = attributes unless attributes.empty? data['instance'] = instance if instance result.to_pson(*args) end # Is this an authenticated request? def authenticated? # Double negative, so we just get true or false ! ! authenticated end def environment @environment ||= Puppet::Node::Environment.new end def environment=(env) @environment = if env.is_a?(Puppet::Node::Environment) env else Puppet::Node::Environment.new(env) end end def escaped_key URI.escape(key) end # LAK:NOTE This is a messy interface to the cache, and it's only # used by the Configurer class. I decided it was better to implement # it now and refactor later, when we have a better design, than # to spend another month coming up with a design now that might # not be any better. def ignore_cache? ignore_cache end def ignore_terminus? ignore_terminus end def initialize(indirection_name, method, key_or_instance, options_or_instance = {}) if options_or_instance.is_a? Hash options = options_or_instance @instance = nil else options = {} @instance = options_or_instance end self.indirection_name = indirection_name self.method = method options = options.inject({}) { |hash, ary| hash[ary[0].to_sym] = ary[1]; hash } set_attributes(options) @options = options if key_or_instance.is_a?(String) || key_or_instance.is_a?(Symbol) key = key_or_instance else @instance ||= key_or_instance end if key # If the request key is a URI, then we need to treat it specially, # because it rewrites the key. We could otherwise strip server/port/etc # info out in the REST class, but it seemed bad design for the REST # class to rewrite the key. if key.to_s =~ /^\w+:\// and not Puppet::Util.absolute_path?(key.to_s) # it's a URI set_uri_key(key) else @key = key end end @key = @instance.name if ! @key and @instance end # Look up the indirection based on the name provided. def indirection Puppet::Indirector::Indirection.instance(indirection_name) end def indirection_name=(name) @indirection_name = name.to_sym end def model raise ArgumentError, "Could not find indirection '#{indirection_name}'" unless i = indirection i.model end # Should we allow use of the cached object? def use_cache? if defined?(@use_cache) ! ! use_cache else true end end # Are we trying to interact with multiple resources, or just one? def plural? method == :search end # Create the query string, if options are present. def query_string return "" unless options and ! options.empty? "?" + options.collect do |key, value| case value when nil; next when true, false; value = value.to_s when Fixnum, Bignum, Float; value = value # nothing when String; value = CGI.escape(value) when Symbol; value = CGI.escape(value.to_s) when Array; value = CGI.escape(YAML.dump(value)) else raise ArgumentError, "HTTP REST queries cannot handle values of type '#{value.class}'" end "#{key}=#{value}" end.join("&") end def to_hash result = options.dup OPTION_ATTRIBUTES.each do |attribute| if value = send(attribute) result[attribute] = value end end result end def to_s return(uri ? uri : "/#{indirection_name}/#{key}") end + def do_request(srv_service=:puppet, default_server=Puppet.settings[:server], default_port=Puppet.settings[:masterport], &block) + # We were given a specific server to use, so just use that one. + # This happens if someone does something like specifying a file + # source using a puppet:// URI with a specific server. + return yield self if !self.server.nil? + + if Puppet.settings[:use_srv_records] + Puppet::Network::Resolver.each_srv_record(Puppet.settings[:srv_domain], srv_service) do |srv_server, srv_port| + begin + self.server = srv_server + self.port = srv_port + return yield self + rescue SystemCallError => e + Puppet.warning "Error connecting to #{srv_server}:#{srv_port}: #{e.message}" + end + end + end + + # ... Fall back onto the default server. + Puppet.debug "No more servers left, falling back to #{default_server}:#{default_port}" if Puppet.settings[:use_srv_records] + self.server = default_server + self.port = default_port + return yield self + end + private def set_attributes(options) OPTION_ATTRIBUTES.each do |attribute| if options.include?(attribute.to_sym) send(attribute.to_s + "=", options[attribute]) options.delete(attribute) end end end # Parse the key as a URI, setting attributes appropriately. def set_uri_key(key) @uri = key begin uri = URI.parse(URI.escape(key)) rescue => detail raise ArgumentError, "Could not understand URL #{key}: #{detail}" end # Just short-circuit these to full paths if uri.scheme == "file" @key = Puppet::Util.uri_to_path(uri) return end @server = uri.host if uri.host # If the URI class can look up the scheme, it will provide a port, # otherwise it will default to '0'. if uri.port.to_i == 0 and uri.scheme == "puppet" @port = Puppet.settings[:masterport].to_i else @port = uri.port.to_i end @protocol = uri.scheme if uri.scheme == 'puppet' @key = URI.unescape(uri.path.sub(/^\//, '')) return end env, indirector, @key = URI.unescape(uri.path.sub(/^\//, '')).split('/',3) @key ||= '' self.environment = env unless env == '' end end diff --git a/lib/puppet/indirector/rest.rb b/lib/puppet/indirector/rest.rb index c8a45d76d..6e5725fc6 100644 --- a/lib/puppet/indirector/rest.rb +++ b/lib/puppet/indirector/rest.rb @@ -1,160 +1,186 @@ require 'net/http' require 'uri' require 'puppet/network/http_pool' require 'puppet/network/http/api/v1' require 'puppet/network/http/compression' # Access objects via REST class Puppet::Indirector::REST < Puppet::Indirector::Terminus include Puppet::Network::HTTP::API::V1 include Puppet::Network::HTTP::Compression.module class << self attr_reader :server_setting, :port_setting end # Specify the setting that we should use to get the server name. def self.use_server_setting(setting) @server_setting = setting end - def self.server - Puppet.settings[server_setting || :server] - end - # Specify the setting that we should use to get the port. def self.use_port_setting(setting) @port_setting = setting end + # Specify the service to use when doing SRV record lookup + def self.use_srv_service(service) + @srv_service = service + end + + def self.srv_service + @srv_service || :puppet + end + + def self.server + Puppet.settings[server_setting || :server] + end + def self.port Puppet.settings[port_setting || :masterport].to_i end # Figure out the content type, turn that into a format, and use the format # to extract the body of the response. def deserialize(response, multiple = false) case response.code when "404" return nil when /^2/ raise "No content type in http response; cannot parse" unless response['content-type'] content_type = response['content-type'].gsub(/\s*;.*$/,'') # strip any appended charset body = uncompress_body(response) # Convert the response to a deserialized object. if multiple model.convert_from_multiple(content_type, body) else model.convert_from(content_type, body) end else # Raise the http error if we didn't get a 'success' of some kind. raise convert_to_http_error(response) end end def convert_to_http_error(response) message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}" Net::HTTPError.new(message, response) end # Provide appropriate headers. def headers add_accept_encoding({"Accept" => model.supported_formats.join(", ")}) end def network(request) Puppet::Network::HttpPool.http_instance(request.server || self.class.server, request.port || self.class.port) end [:get, :post, :head, :delete, :put].each do |method| define_method "http_#{method}" do |request, *args| http_request(method, request, *args) end end def http_request(method, request, *args) http_connection = network(request) peer_certs = [] # We add the callback to collect the certificates for use in constructing # the error message if the verification failed. This is necessary since we # don't have direct access to the cert that we expected the connection to # use otherwise. # http_connection.verify_callback = proc do |preverify_ok, ssl_context| peer_certs << Puppet::SSL::Certificate.from_s(ssl_context.current_cert.to_pem) preverify_ok end http_connection.send(method, *args) rescue OpenSSL::SSL::SSLError => error if error.message.include? "certificate verify failed" raise Puppet::Error, "#{error.message}. This is often because the time is out of sync on the server or client" elsif error.message.include? "hostname was not match" raise unless cert = peer_certs.find { |c| c.name !~ /^puppet ca/i } valid_certnames = [cert.name, *cert.subject_alt_names].uniq msg = valid_certnames.length > 1 ? "one of #{valid_certnames.join(', ')}" : valid_certnames.first raise Puppet::Error, "Server hostname '#{http_connection.address}' did not match server certificate; expected #{msg}" else raise end end def find(request) uri, body = request_to_uri_and_body(request) uri_with_query_string = "#{uri}?#{body}" - # WEBrick in Ruby 1.9.1 only supports up to 1024 character lines in an HTTP request - # http://redmine.ruby-lang.org/issues/show/3991 - response = if "GET #{uri_with_query_string} HTTP/1.1\r\n".length > 1024 - http_post(request, uri, body, headers) - else - http_get(request, uri_with_query_string, headers) + + response = request.do_request(self.class.srv_service, self.class.server, self.class.port) do |request| + # WEBrick in Ruby 1.9.1 only supports up to 1024 character lines in an HTTP request + # http://redmine.ruby-lang.org/issues/show/3991 + if "GET #{uri_with_query_string} HTTP/1.1\r\n".length > 1024 + http_post(request, uri, body, headers) + else + http_get(request, uri_with_query_string, headers) + end end - result = deserialize response + result = deserialize(response) + + return nil unless result + result.name = request.key if result.respond_to?(:name=) result end def head(request) - response = http_head(request, indirection2uri(request), headers) + response = request.do_request(self.class.srv_service, self.class.server, self.class.port) do |request| + http_head(request, indirection2uri(request), headers) + end + case response.code when "404" return false when /^2/ return true else # Raise the http error if we didn't get a 'success' of some kind. raise convert_to_http_error(response) end end def search(request) - unless result = deserialize(http_get(request, indirection2uri(request), headers), true) - return [] + result = request.do_request(self.class.srv_service, self.class.server, self.class.port) do |request| + deserialize(http_get(request, indirection2uri(request), headers), true) end - result + + # result from the server can be nil, but we promise to return an array... + result || [] end def destroy(request) raise ArgumentError, "DELETE does not accept options" unless request.options.empty? - deserialize http_delete(request, indirection2uri(request), headers) + + request.do_request(self.class.srv_service, self.class.server, self.class.port) do |request| + return deserialize(http_delete(request, indirection2uri(request), headers)) + end end def save(request) raise ArgumentError, "PUT does not accept options" unless request.options.empty? - deserialize http_put(request, indirection2uri(request), request.instance.render, headers.merge({ "Content-Type" => request.instance.mime })) + + request.do_request(self.class.srv_service, self.class.server, self.class.port) do |request| + deserialize http_put(request, indirection2uri(request), request.instance.render, headers.merge({ "Content-Type" => request.instance.mime })) + end end private def environment Puppet::Node::Environment.new end end diff --git a/lib/puppet/network/resolver.rb b/lib/puppet/network/resolver.rb new file mode 100644 index 000000000..0e0460eb1 --- /dev/null +++ b/lib/puppet/network/resolver.rb @@ -0,0 +1,85 @@ +require 'resolv' +module Puppet::Network; end + +module Puppet::Network::Resolver + # Iterate through the list of servers that service this hostname + # and yield each server/port since SRV records have ports in them + # It will override whatever masterport setting is already set. + def self.each_srv_record(domain, service_name = :puppet, &block) + if (domain.nil? or domain.empty?) + Puppet.debug "Domain not known; skipping SRV lookup" + return + end + + Puppet.debug "Searching for SRV records for domain: #{domain}" + + case service_name + when :puppet then service = '_x-puppet' + when :ca then service = '_x-puppet-ca' + when :report then service = '_x-puppet-report' + when :file then service = '_x-puppet-fileserver' + end + srv_record = "#{service}._tcp.#{domain}" + + resolver = Resolv::DNS.new + records = resolver.getresources(srv_record, Resolv::DNS::Resource::IN::SRV) + Puppet.debug "Found #{records.size} SRV records for: #{srv_record}" + + if records.size == 0 && service_name != :puppet + # Try the generic :puppet service if no SRV records were found + # for the specific service. + each_srv_record(domain, :puppet, &block) + else + each_priority(records) do |priority, records| + while next_rr = records.delete(find_weighted_server(records)) + Puppet.debug "Yielding next server of #{next_rr.target.to_s}:#{next_rr.port}" + yield next_rr.target.to_s, next_rr.port + end + end + end + end + + private + + def self.each_priority(records) + pri_hash = records.inject({}) do |groups, element| + groups[element.priority] ||= [] + groups[element.priority] << element + groups + end + + pri_hash.keys.sort.each do |key| + yield key, pri_hash[key] + end + end + + def self.find_weighted_server(records) + return nil if records.nil? || records.empty? + return records.first if records.size == 1 + + # Calculate the sum of all weights in the list of resource records, + # This is used to then select hosts until the weight exceeds what + # random number we selected. For example, if we have weights of 1 8 and 3: + # + # |-|---|--------| + # ^ + # We generate a random number 5, and iterate through the records, adding + # the current record's weight to the accumulator until the weight of the + # current record plus previous records is greater than the random number. + + total_weight = records.inject(0) { |sum,record| + sum + weight(record) + } + current_weight = 0 + chosen_weight = 1 + Kernel.rand(total_weight) + + records.each do |record| + current_weight += weight(record) + return record if current_weight >= chosen_weight + end + end + + def self.weight(record) + record.weight == 0 ? 1 : record.weight * 10 + end +end diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb index 8f3b8b48a..81edecacf 100755 --- a/lib/puppet/type/file/content.rb +++ b/lib/puppet/type/file/content.rb @@ -1,225 +1,233 @@ require 'net/http' require 'uri' require 'tempfile' require 'puppet/util/checksums' require 'puppet/network/http/api/v1' require 'puppet/network/http/compression' module Puppet Puppet::Type.type(:file).newproperty(:content) do include Puppet::Util::Diff include Puppet::Util::Checksums include Puppet::Network::HTTP::API::V1 include Puppet::Network::HTTP::Compression.module attr_reader :actual_content desc "Specify the contents of a file as a string. Newlines, tabs, and spaces can be specified using standard escaped syntax in double-quoted strings (e.g., \\n for a newline). With very small files, you can construct strings directly... define resolve(nameserver1, nameserver2, domain, search) { $str = \"search $search domain $domain nameserver $nameserver1 nameserver $nameserver2 \" file { \"/etc/resolv.conf\": content => $str } } ...but for larger files, this attribute is more useful when combined with the [template](http://docs.puppetlabs.com/references/latest/function.html#template) function." # Store a checksum as the value, rather than the actual content. # Simplifies everything. munge do |value| if value == :absent value elsif checksum?(value) # XXX This is potentially dangerous because it means users can't write a file whose # entire contents are a plain checksum value else @actual_content = value resource.parameter(:checksum).sum(value) end end # Checksums need to invert how changes are printed. def change_to_s(currentvalue, newvalue) # Our "new" checksum value is provided by the source. if source = resource.parameter(:source) and tmp = source.checksum newvalue = tmp end if currentvalue == :absent return "defined content as '#{newvalue}'" elsif newvalue == :absent return "undefined content from '#{currentvalue}'" else return "content changed '#{currentvalue}' to '#{newvalue}'" end end def checksum_type if source = resource.parameter(:source) result = source.checksum else checksum = resource.parameter(:checksum) result = resource[:checksum] end if result =~ /^\{(\w+)\}.+/ return $1.to_sym else return result end end def length (actual_content and actual_content.length) || 0 end def content self.should end # Override this method to provide diffs if asked for. # Also, fix #872: when content is used, and replace is true, the file # should be insync when it exists def insync?(is) if resource.should_be_file? return false if is == :absent else return true end return true if ! @resource.replace? result = super if ! result and Puppet[:show_diff] write_temporarily do |path| notice "\n" + diff(@resource[:path], path) end end result end def retrieve return :absent unless stat = @resource.stat ftype = stat.ftype # Don't even try to manage the content on directories or links return nil if ["directory","link"].include?(ftype) begin resource.parameter(:checksum).sum_file(resource[:path]) rescue => detail raise Puppet::Error, "Could not read #{ftype} #{@resource.title}: #{detail}" end end # Make sure we're also managing the checksum property. def should=(value) @resource.newattr(:checksum) unless @resource.parameter(:checksum) super end # Just write our content out to disk. def sync return_event = @resource.stat ? :file_changed : :file_created # We're safe not testing for the 'source' if there's no 'should' # because we wouldn't have gotten this far if there weren't at least # one valid value somewhere. @resource.write(:content) return_event end def write_temporarily tempfile = Tempfile.new("puppet-file") tempfile.open write(tempfile) tempfile.close yield tempfile.path tempfile.delete end def write(file) resource.parameter(:checksum).sum_stream { |sum| each_chunk_from(actual_content || resource.parameter(:source)) { |chunk| sum << chunk file.print chunk } } end def self.standalone? Puppet.settings[:name] == "apply" end # the content is munged so if it's a checksum source_or_content is nil # unless the checksum indirectly comes from source def each_chunk_from(source_or_content) if source_or_content.is_a?(String) yield source_or_content elsif content_is_really_a_checksum? && source_or_content.nil? yield read_file_from_filebucket elsif source_or_content.nil? yield '' elsif self.class.standalone? yield source_or_content.content elsif source_or_content.local? chunk_file_from_disk(source_or_content) { |chunk| yield chunk } else chunk_file_from_source(source_or_content) { |chunk| yield chunk } end end private def content_is_really_a_checksum? checksum?(should) end def chunk_file_from_disk(source_or_content) File.open(source_or_content.full_path, "rb") do |src| while chunk = src.read(8192) yield chunk end end end - def chunk_file_from_source(source_or_content) + def get_from_source(source_or_content) request = Puppet::Indirector::Request.new(:file_content, :find, source_or_content.full_path.sub(/^\//,'')) - connection = Puppet::Network::HttpPool.http_instance(source_or_content.server, source_or_content.port) - connection.request_get(indirection2uri(request), add_accept_encoding({"Accept" => "raw"})) do |response| + + request.do_request(:fileserver) do |req| + connection = Puppet::Network::HttpPool.http_instance(req.server, req.port) + return yield connection.request_get(indirection2uri(req), add_accept_encoding({"Accept" => "raw"})) + end + end + + + def chunk_file_from_source(source_or_content) + get_from_source(source_or_content) do |response| case response.code when /^2/; uncompress(response) { |uncompressor| response.read_body { |chunk| yield uncompressor.uncompress(chunk) } } else # Raise the http error if we didn't get a 'success' of some kind. message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}" raise Net::HTTPError.new(message, response) end end end def read_file_from_filebucket raise "Could not get filebucket from file" unless dipper = resource.bucket sum = should.sub(/\{\w+\}/, '') dipper.getfile(sum) rescue => detail fail "Could not retrieve content for #{should} from filebucket: #{detail}" end end end diff --git a/lib/puppet/type/file/source.rb b/lib/puppet/type/file/source.rb index 9375550a9..244f5159f 100755 --- a/lib/puppet/type/file/source.rb +++ b/lib/puppet/type/file/source.rb @@ -1,204 +1,208 @@ require 'puppet/file_serving/content' require 'puppet/file_serving/metadata' module Puppet # Copy files from a local or remote source. This state *only* does any work # when the remote file is an actual file; in that case, this state copies # the file down. If the remote file is a dir or a link or whatever, then # this state, during retrieval, modifies the appropriate other states # so that things get taken care of appropriately. Puppet::Type.type(:file).newparam(:source) do include Puppet::Util::Diff attr_accessor :source, :local desc <<-EOT Copy a file over the current file. Uses `checksum` to determine when a file should be copied. Valid values are either fully qualified paths to files, or URIs. Currently supported URI types are *puppet* and *file*. This is one of the primary mechanisms for getting content into applications that Puppet does not directly support and is very useful for those configuration files that don't change much across sytems. For instance: class sendmail { file { "/etc/mail/sendmail.cf": source => "puppet://server/modules/module_name/sendmail.cf" } } You can also leave out the server name, in which case `puppet agent` will fill in the name of its configuration server and `puppet apply` will use the local filesystem. This makes it easy to use the same configuration in both local and centralized forms. Currently, only the `puppet` scheme is supported for source URL's. Puppet will connect to the file server running on `server` to retrieve the contents of the file. If the `server` part is empty, the behavior of the command-line interpreter (`puppet apply`) and the client demon (`puppet agent`) differs slightly: `apply` will look such a file up on the module path on the local host, whereas `agent` will connect to the puppet server that it received the manifest from. See the [fileserver configuration documentation](http://docs.puppetlabs.com/guides/file_serving.html) for information on how to configure and use file services within Puppet. If you specify multiple file sources for a file, then the first source that exists will be used. This allows you to specify what amount to search paths for files: file { "/path/to/my/file": source => [ "/modules/nfs/files/file.$host", "/modules/nfs/files/file.$operatingsystem", "/modules/nfs/files/file" ] } This will use the first found file as the source. You cannot currently copy links using this mechanism; set `links` to `follow` if any remote sources are links. EOT validate do |sources| sources = [sources] unless sources.is_a?(Array) sources.each do |source| next if Puppet::Util.absolute_path?(source) begin uri = URI.parse(URI.escape(source)) rescue => detail self.fail "Could not understand source #{source}: #{detail}" end self.fail "Cannot use relative URLs '#{source}'" unless uri.absolute? self.fail "Cannot use opaque URLs '#{source}'" unless uri.hierarchical? self.fail "Cannot use URLs of type '#{uri.scheme}' as source for fileserving" unless %w{file puppet}.include?(uri.scheme) end end SEPARATOR_REGEX = [Regexp.escape(File::SEPARATOR.to_s), Regexp.escape(File::ALT_SEPARATOR.to_s)].join munge do |sources| sources = [sources] unless sources.is_a?(Array) sources.map do |source| source = source.sub(/[#{SEPARATOR_REGEX}]+$/, '') if Puppet::Util.absolute_path?(source) URI.unescape(Puppet::Util.path_to_uri(source).to_s) else source end end end def change_to_s(currentvalue, newvalue) # newvalue = "{md5}#{@metadata.checksum}" if @resource.property(:ensure).retrieve == :absent return "creating from source #{metadata.source} with contents #{metadata.checksum}" else return "replacing from source #{metadata.source} with contents #{metadata.checksum}" end end def checksum metadata && metadata.checksum end # Look up (if necessary) and return remote content. def content return @content if @content raise Puppet::DevError, "No source for content was stored with the metadata" unless metadata.source unless tmp = Puppet::FileServing::Content.indirection.find(metadata.source) fail "Could not find any content at %s" % metadata.source end @content = tmp.content end # Copy the values from the source to the resource. Yay. def copy_source_values devfail "Somehow got asked to copy source values without any metadata" unless metadata # Take each of the stats and set them as states on the local file # if a value has not already been provided. [:owner, :mode, :group, :checksum].each do |metadata_method| param_name = (metadata_method == :checksum) ? :content : metadata_method next if metadata_method == :owner and !Puppet.features.root? next if metadata_method == :checksum and metadata.ftype == "directory" next if metadata_method == :checksum and metadata.ftype == "link" and metadata.links == :manage if resource[param_name].nil? or resource[param_name] == :absent resource[param_name] = metadata.send(metadata_method) end end if resource[:ensure] == :absent # We know all we need to elsif metadata.ftype != "link" resource[:ensure] = metadata.ftype elsif @resource[:links] == :follow resource[:ensure] = :present else resource[:ensure] = "link" resource[:target] = metadata.destination end end def found? ! (metadata.nil? or metadata.ftype.nil?) end attr_writer :metadata # Provide, and retrieve if necessary, the metadata for this file. Fail # if we can't find data about this host, and fail if there are any # problems in our query. def metadata return @metadata if @metadata return nil unless value value.each do |source| begin if data = Puppet::FileServing::Metadata.indirection.find(source) @metadata = data @metadata.source = source break end rescue => detail fail detail, "Could not retrieve file metadata for #{source}: #{detail}" end end fail "Could not retrieve information from environment #{Puppet[:environment]} source(s) #{value.join(", ")}" unless @metadata @metadata end def local? found? and scheme == "file" end def full_path Puppet::Util.uri_to_path(uri) if found? end + def server? + uri and uri.host + end + def server (uri and uri.host) or Puppet.settings[:server] end def port (uri and uri.port) or Puppet.settings[:masterport] end private def scheme (uri and uri.scheme) end def uri @uri ||= URI.parse(URI.escape(metadata.source)) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e0227beb0..aec998ee0 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,107 +1,107 @@ dir = File.expand_path(File.dirname(__FILE__)) $LOAD_PATH.unshift File.join(dir, 'lib') # Don't want puppet getting the command line arguments for rake or autotest ARGV.clear require 'puppet' require 'mocha' gem 'rspec', '>=2.0.0' require 'rspec/expectations' # So everyone else doesn't have to include this base constant. module PuppetSpec FIXTURE_DIR = File.join(dir = File.expand_path(File.dirname(__FILE__)), "fixtures") unless defined?(FIXTURE_DIR) end require 'pathname' require 'tmpdir' require 'puppet_spec/verbose' require 'puppet_spec/files' require 'puppet_spec/fixtures' require 'puppet_spec/matchers' require 'monkey_patches/alias_should_to_must' require 'monkey_patches/publicize_methods' Pathname.glob("#{dir}/shared_behaviours/**/*.rb") do |behaviour| require behaviour.relative_path_from(Pathname.new(dir)) end RSpec.configure do |config| include PuppetSpec::Fixtures config.mock_with :mocha config.before :each do GC.disable # We need to preserve the current state of all our indirection cache and # terminus classes. This is pretty important, because changes to these # are global and lead to order dependencies in our testing. # # We go direct to the implementation because there is no safe, sane public # API to manage restoration of these to their default values. This # should, once the value is proved, be moved to a standard API on the # indirector. # # To make things worse, a number of the tests stub parts of the # indirector. These stubs have very specific expectations that what # little of the public API we could use is, well, likely to explode # randomly in some tests. So, direct access. --daniel 2011-08-30 $saved_indirection_state = {} indirections = Puppet::Indirector::Indirection.send(:class_variable_get, :@@indirections) indirections.each do |indirector| $saved_indirection_state[indirector.name] = { :@terminus_class => indirector.instance_variable_get(:@terminus_class), :@cache_class => indirector.instance_variable_get(:@cache_class) } end # these globals are set by Application $puppet_application_mode = nil $puppet_application_name = nil # REVISIT: I think this conceals other bad tests, but I don't have time to # fully diagnose those right now. When you read this, please come tell me # I suck for letting this float. --daniel 2011-04-21 Signal.stubs(:trap) # Set the confdir and vardir to gibberish so that tests # have to be correctly mocked. Puppet[:confdir] = "/dev/null" Puppet[:vardir] = "/dev/null" # Avoid opening ports to the outside world Puppet.settings[:bindaddress] = "127.0.0.1" @logs = [] Puppet::Util::Log.newdestination(Puppet::Test::LogCollector.new(@logs)) @log_level = Puppet::Util::Log.level end config.after :each do Puppet.settings.clear Puppet::Node::Environment.clear Puppet::Util::Storage.clear Puppet::Util::ExecutionStub.reset PuppetSpec::Files.cleanup @logs.clear Puppet::Util::Log.close_all Puppet::Util::Log.level = @log_level # Restore the indirector configuration. See before hook. indirections = Puppet::Indirector::Indirection.send(:class_variable_get, :@@indirections) indirections.each do |indirector| $saved_indirection_state.fetch(indirector.name, {}).each do |variable, value| indirector.instance_variable_set(variable, value) end end - $saved_indirection_state = nil + $saved_indirection_state = {} GC.enable end end diff --git a/spec/unit/indirector/certificate/rest_spec.rb b/spec/unit/indirector/certificate/rest_spec.rb index 870d9e437..4ec475bc2 100755 --- a/spec/unit/indirector/certificate/rest_spec.rb +++ b/spec/unit/indirector/certificate/rest_spec.rb @@ -1,58 +1,62 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/indirector/certificate/rest' describe Puppet::SSL::Certificate::Rest do before do @searcher = Puppet::SSL::Certificate::Rest.new end it "should be a sublcass of Puppet::Indirector::REST" do Puppet::SSL::Certificate::Rest.superclass.should equal(Puppet::Indirector::REST) end it "should set server_setting to :ca_server" do Puppet::SSL::Certificate::Rest.server_setting.should == :ca_server end it "should set port_setting to :ca_port" do Puppet::SSL::Certificate::Rest.port_setting.should == :ca_port end + it "should use the :ca SRV service" do + Puppet::SSL::Certificate::Rest.srv_service.should == :ca + end + it "should make sure found certificates have their names set to the search string" do terminus = Puppet::SSL::Certificate::Rest.new # This has 'boo.com' in the CN cert_string = "-----BEGIN CERTIFICATE----- MIICPzCCAaigAwIBAgIBBDANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDDAtidWNr eS5sb2NhbDAeFw0wOTA5MTcxNzI1MzJaFw0xNDA5MTYxNzI1MzJaMBIxEDAOBgNV BAMMB2Jvby5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKG9B+DkTCNh F5xHchNDfnbC9NzWKM600oxrr84pgUVAG6B2wAZcdfoEtXszhsY9Jzpwqkvxk4Mx AbYqo9+TCi4UoiH6e+vAKOOJD3DHrlf+/RW4hGtyaI41DBhf4+B4/oFz5PH9mvKe NSfHFI/yPW+1IXYjxKLQNwF9E7q3JbnzAgMBAAGjgaAwgZ0wOAYJYIZIAYb4QgEN BCsWKVB1cHBldCBSdWJ5L09wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMAwG A1UdEwEB/wQCMAAwHQYDVR0OBBYEFJOxEUeyf4cNOBmf9zIaE1JTuNdLMAsGA1Ud DwQEAwIFoDAnBgNVHSUEIDAeBggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwME MA0GCSqGSIb3DQEBBQUAA4GBAFTJxKprMg6tfhGnvEvURPmlJrINn9c2b5Y4AGYp tO86PFFkWw/EIJvvJzbj3s+Butr+eUo//+f1xxX7UCwwGqGxKqjtVS219oU/wkx8 h7rW4Xk7MrLl0auSS1p4wLcAMm+ZImf94+j8Cj+tkr8eGozZceRV13b8+EkdaE3S rn/G -----END CERTIFICATE----- " network = stub 'network' terminus.stubs(:network).returns network response = stub 'response', :code => "200", :body => cert_string response.stubs(:[]).with('content-type').returns "text/plain" response.stubs(:[]).with('content-encoding') network.stubs(:verify_callback=) network.expects(:get).returns response request = Puppet::Indirector::Request.new(:certificate, :find, "foo.com") result = terminus.find(request) result.should_not be_nil result.name.should == "foo.com" end end diff --git a/spec/unit/indirector/file_content/rest_spec.rb b/spec/unit/indirector/file_content/rest_spec.rb index 06ad16e77..99925a842 100755 --- a/spec/unit/indirector/file_content/rest_spec.rb +++ b/spec/unit/indirector/file_content/rest_spec.rb @@ -1,10 +1,14 @@ #!/usr/bin/env rspec require 'spec_helper' -require 'puppet/indirector/file_content' +require 'puppet/indirector/file_content/rest' -describe "Puppet::Indirector::Content::Rest" do +describe Puppet::Indirector::FileContent::Rest do it "should add the node's cert name to the arguments" it "should set the content type to text/plain" + + it "should use the :fileserver SRV service" do + Puppet::Indirector::FileContent::Rest.srv_service.should == :fileserver + end end diff --git a/spec/unit/indirector/report/rest_spec.rb b/spec/unit/indirector/report/rest_spec.rb index 806ae6efd..8e969ab4c 100755 --- a/spec/unit/indirector/report/rest_spec.rb +++ b/spec/unit/indirector/report/rest_spec.rb @@ -1,27 +1,31 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/indirector/report/rest' describe Puppet::Transaction::Report::Rest do it "should be a subclass of Puppet::Indirector::REST" do Puppet::Transaction::Report::Rest.superclass.should equal(Puppet::Indirector::REST) end it "should use the :report_server setting in preference to :reportserver" do Puppet.settings[:reportserver] = "reportserver" Puppet.settings[:report_server] = "report_server" Puppet::Transaction::Report::Rest.server.should == "report_server" end it "should use the :report_server setting in preference to :server" do Puppet.settings[:server] = "server" Puppet.settings[:report_server] = "report_server" Puppet::Transaction::Report::Rest.server.should == "report_server" end it "should have a value for report_server and report_port" do Puppet::Transaction::Report::Rest.server.should_not be_nil Puppet::Transaction::Report::Rest.port.should_not be_nil end + + it "should use the :report SRV service" do + Puppet::Transaction::Report::Rest.srv_service.should == :report + end end diff --git a/spec/unit/indirector/request_spec.rb b/spec/unit/indirector/request_spec.rb index 430d301e5..bc6b59952 100755 --- a/spec/unit/indirector/request_spec.rb +++ b/spec/unit/indirector/request_spec.rb @@ -1,417 +1,517 @@ #!/usr/bin/env rspec require 'spec_helper' require 'matchers/json' require 'puppet/indirector/request' require 'puppet/util/pson' describe Puppet::Indirector::Request do describe "when registering the document type" do it "should register its document type with JSON" do PSON.registered_document_types["IndirectorRequest"].should equal(Puppet::Indirector::Request) end end describe "when initializing" do it "should require an indirection name, a key, and a method" do lambda { Puppet::Indirector::Request.new }.should raise_error(ArgumentError) end it "should always convert the indirection name to a symbol" do Puppet::Indirector::Request.new("ind", :method, "mykey").indirection_name.should == :ind end it "should use provided value as the key if it is a string" do Puppet::Indirector::Request.new(:ind, :method, "mykey").key.should == "mykey" end it "should use provided value as the key if it is a symbol" do Puppet::Indirector::Request.new(:ind, :method, :mykey).key.should == :mykey end it "should use the name of the provided instance as its key if an instance is provided as the key instead of a string" do instance = mock 'instance', :name => "mykey" request = Puppet::Indirector::Request.new(:ind, :method, instance) request.key.should == "mykey" request.instance.should equal(instance) end it "should support options specified as a hash" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key, :one => :two) }.should_not raise_error(ArgumentError) end it "should support nil options" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key, nil) }.should_not raise_error(ArgumentError) end it "should support unspecified options" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key) }.should_not raise_error(ArgumentError) end it "should use an empty options hash if nil was provided" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).options.should == {} end it "should default to a nil node" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).node.should be_nil end it "should set its node attribute if provided in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, :node => "foo.com").node.should == "foo.com" end it "should default to a nil ip" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).ip.should be_nil end it "should set its ip attribute if provided in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, :ip => "192.168.0.1").ip.should == "192.168.0.1" end it "should default to being unauthenticated" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).should_not be_authenticated end it "should set be marked authenticated if configured in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, :authenticated => "eh").should be_authenticated end it "should keep its options as a hash even if a node is specified" do Puppet::Indirector::Request.new(:ind, :method, :key, :node => "eh").options.should be_instance_of(Hash) end it "should keep its options as a hash even if another option is specified" do Puppet::Indirector::Request.new(:ind, :method, :key, :foo => "bar").options.should be_instance_of(Hash) end it "should treat options other than :ip, :node, and :authenticated as options rather than attributes" do Puppet::Indirector::Request.new(:ind, :method, :key, :server => "bar").options[:server].should == "bar" end it "should normalize options to use symbols as keys" do Puppet::Indirector::Request.new(:ind, :method, :key, "foo" => "bar").options[:foo].should == "bar" end describe "and the request key is a URI" do let(:file) { File.expand_path("/my/file with spaces") } describe "and the URI is a 'file' URI" do before do @request = Puppet::Indirector::Request.new(:ind, :method, "#{URI.unescape(Puppet::Util.path_to_uri(file).to_s)}") end it "should set the request key to the unescaped full file path" do @request.key.should == file end it "should not set the protocol" do @request.protocol.should be_nil end it "should not set the port" do @request.port.should be_nil end it "should not set the server" do @request.server.should be_nil end end it "should set the protocol to the URI scheme" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff").protocol.should == "http" end it "should set the server if a server is provided" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff").server.should == "host" end it "should set the server and port if both are provided" do Puppet::Indirector::Request.new(:ind, :method, "http://host:543/stuff").port.should == 543 end it "should default to the masterport if the URI scheme is 'puppet'" do Puppet.settings.expects(:value).with(:masterport).returns "321" Puppet::Indirector::Request.new(:ind, :method, "puppet://host/stuff").port.should == 321 end it "should use the provided port if the URI scheme is not 'puppet'" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff").port.should == 80 end it "should set the request key to the unescaped key part path from the URI" do Puppet::Indirector::Request.new(:ind, :method, "http://host/environment/terminus/stuff with spaces").key.should == "stuff with spaces" end it "should set the :uri attribute to the full URI" do Puppet::Indirector::Request.new(:ind, :method, "http:///stu ff").uri.should == 'http:///stu ff' end it "should not parse relative URI" do Puppet::Indirector::Request.new(:ind, :method, "foo/bar").uri.should be_nil end it "should not parse opaque URI" do Puppet::Indirector::Request.new(:ind, :method, "mailto:joe").uri.should be_nil end end it "should allow indication that it should not read a cached instance" do Puppet::Indirector::Request.new(:ind, :method, :key, :ignore_cache => true).should be_ignore_cache end it "should default to not ignoring the cache" do Puppet::Indirector::Request.new(:ind, :method, :key).should_not be_ignore_cache end it "should allow indication that it should not not read an instance from the terminus" do Puppet::Indirector::Request.new(:ind, :method, :key, :ignore_terminus => true).should be_ignore_terminus end it "should default to not ignoring the terminus" do Puppet::Indirector::Request.new(:ind, :method, :key).should_not be_ignore_terminus end end it "should look use the Indirection class to return the appropriate indirection" do ind = mock 'indirection' Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns ind request = Puppet::Indirector::Request.new(:myind, :method, :key) request.indirection.should equal(ind) end it "should use its indirection to look up the appropriate model" do ind = mock 'indirection' Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns ind request = Puppet::Indirector::Request.new(:myind, :method, :key) ind.expects(:model).returns "mymodel" request.model.should == "mymodel" end it "should fail intelligently when asked to find a model but the indirection cannot be found" do Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns nil request = Puppet::Indirector::Request.new(:myind, :method, :key) lambda { request.model }.should raise_error(ArgumentError) end it "should have a method for determining if the request is plural or singular" do Puppet::Indirector::Request.new(:myind, :method, :key).should respond_to(:plural?) end it "should be considered plural if the method is 'search'" do Puppet::Indirector::Request.new(:myind, :search, :key).should be_plural end it "should not be considered plural if the method is not 'search'" do Puppet::Indirector::Request.new(:myind, :find, :key).should_not be_plural end it "should use its uri, if it has one, as its string representation" do Puppet::Indirector::Request.new(:myind, :find, "foo://bar/baz").to_s.should == "foo://bar/baz" end it "should use its indirection name and key, if it has no uri, as its string representation" do Puppet::Indirector::Request.new(:myind, :find, "key") == "/myind/key" end it "should be able to return the URI-escaped key" do Puppet::Indirector::Request.new(:myind, :find, "my key").escaped_key.should == URI.escape("my key") end it "should have an environment accessor" do Puppet::Indirector::Request.new(:myind, :find, "my key", :environment => "foo").should respond_to(:environment) end it "should set its environment to an environment instance when a string is specified as its environment" do Puppet::Indirector::Request.new(:myind, :find, "my key", :environment => "foo").environment.should == Puppet::Node::Environment.new("foo") end it "should use any passed in environment instances as its environment" do env = Puppet::Node::Environment.new("foo") Puppet::Indirector::Request.new(:myind, :find, "my key", :environment => env).environment.should equal(env) end it "should use the default environment when none is provided" do Puppet::Indirector::Request.new(:myind, :find, "my key" ).environment.should equal(Puppet::Node::Environment.new) end it "should support converting its options to a hash" do Puppet::Indirector::Request.new(:myind, :find, "my key" ).should respond_to(:to_hash) end it "should include all of its attributes when its options are converted to a hash" do Puppet::Indirector::Request.new(:myind, :find, "my key", :node => 'foo').to_hash[:node].should == 'foo' end describe "when building a query string from its options" do before do @request = Puppet::Indirector::Request.new(:myind, :find, "my key") end it "should return an empty query string if there are no options" do @request.stubs(:options).returns nil @request.query_string.should == "" end it "should return an empty query string if the options are empty" do @request.stubs(:options).returns({}) @request.query_string.should == "" end it "should prefix the query string with '?'" do @request.stubs(:options).returns(:one => "two") @request.query_string.should =~ /^\?/ end it "should include all options in the query string, separated by '&'" do @request.stubs(:options).returns(:one => "two", :three => "four") @request.query_string.sub(/^\?/, '').split("&").sort.should == %w{one=two three=four}.sort end it "should ignore nil options" do @request.stubs(:options).returns(:one => "two", :three => nil) @request.query_string.should_not be_include("three") end it "should convert 'true' option values into strings" do @request.stubs(:options).returns(:one => true) @request.query_string.should == "?one=true" end it "should convert 'false' option values into strings" do @request.stubs(:options).returns(:one => false) @request.query_string.should == "?one=false" end it "should convert to a string all option values that are integers" do @request.stubs(:options).returns(:one => 50) @request.query_string.should == "?one=50" end it "should convert to a string all option values that are floating point numbers" do @request.stubs(:options).returns(:one => 1.2) @request.query_string.should == "?one=1.2" end it "should CGI-escape all option values that are strings" do escaping = CGI.escape("one two") @request.stubs(:options).returns(:one => "one two") @request.query_string.should == "?one=#{escaping}" end it "should YAML-dump and CGI-escape arrays" do escaping = CGI.escape(YAML.dump(%w{one two})) @request.stubs(:options).returns(:one => %w{one two}) @request.query_string.should == "?one=#{escaping}" end it "should convert to a string and CGI-escape all option values that are symbols" do escaping = CGI.escape("sym bol") @request.stubs(:options).returns(:one => :"sym bol") @request.query_string.should == "?one=#{escaping}" end it "should fail if options other than booleans or strings are provided" do @request.stubs(:options).returns(:one => {:one => :two}) lambda { @request.query_string }.should raise_error(ArgumentError) end end describe "when converting to json" do before do @request = Puppet::Indirector::Request.new(:facts, :find, "foo") end it "should produce a hash with the document_type set to 'request'" do @request.should set_json_document_type_to("IndirectorRequest") end - + it "should set the 'key'" do @request.should set_json_attribute("key").to("foo") end it "should include an attribute for its indirection name" do @request.should set_json_attribute("type").to("facts") end it "should include a 'method' attribute set to its method" do @request.should set_json_attribute("method").to("find") end it "should add all attributes under the 'attributes' attribute" do @request.ip = "127.0.0.1" @request.should set_json_attribute("attributes", "ip").to("127.0.0.1") end it "should add all options under the 'attributes' attribute" do @request.options["opt"] = "value" PSON.parse(@request.to_pson)["data"]['attributes']['opt'].should == "value" end it "should include the instance if provided" do facts = Puppet::Node::Facts.new("foo") @request.instance = facts PSON.parse(@request.to_pson)["data"]['instance'].should be_instance_of(Hash) end end describe "when converting from json" do before do @request = Puppet::Indirector::Request.new(:facts, :find, "foo") @klass = Puppet::Indirector::Request @format = Puppet::Network::FormatHandler.format('pson') end def from_json(json) @format.intern(Puppet::Indirector::Request, json) end it "should set the 'key'" do from_json(@request.to_pson).key.should == "foo" end it "should fail if no key is provided" do json = PSON.parse(@request.to_pson) json['data'].delete("key") lambda { from_json(json.to_pson) }.should raise_error(ArgumentError) end it "should set its indirector name" do from_json(@request.to_pson).indirection_name.should == :facts end it "should fail if no type is provided" do json = PSON.parse(@request.to_pson) json['data'].delete("type") lambda { from_json(json.to_pson) }.should raise_error(ArgumentError) end it "should set its method" do from_json(@request.to_pson).method.should == "find" end it "should fail if no method is provided" do json = PSON.parse(@request.to_pson) json['data'].delete("method") lambda { from_json(json.to_pson) }.should raise_error(ArgumentError) end it "should initialize with all attributes and options" do @request.ip = "127.0.0.1" @request.options["opt"] = "value" result = from_json(@request.to_pson) result.options[:opt].should == "value" result.ip.should == "127.0.0.1" end it "should set its instance as an instance if one is provided" do facts = Puppet::Node::Facts.new("foo") @request.instance = facts result = from_json(@request.to_pson) result.instance.should be_instance_of(Puppet::Node::Facts) end end + + context '#do_request' do + before :each do + @request = Puppet::Indirector::Request.new(:myind, :find, "my key") + end + + context 'when not using SRV records' do + before :each do + Puppet.settings[:use_srv_records] = false + end + + it "yields the request with the default server and port when no server or port were specified on the original request" do + count = 0 + rval = @request.do_request(:puppet, 'puppet.example.com', '90210') do |got| + count += 1 + got.server.should == 'puppet.example.com' + got.port.should == '90210' + 'Block return value' + end + count.should == 1 + + rval.should == 'Block return value' + end + end + + context 'when using SRV records' do + before :each do + Puppet.settings[:use_srv_records] = true + Puppet.settings[:srv_domain] = 'example.com' + end + + it "yields the request with the original server and port unmodified" do + @request.server = 'puppet.example.com' + @request.port = '90210' + + count = 0 + rval = @request.do_request do |got| + count += 1 + got.server.should == 'puppet.example.com' + got.port.should == '90210' + 'Block return value' + end + count.should == 1 + + rval.should == 'Block return value' + end + + context "when SRV returns servers" do + before :each do + @dns_mock = mock('dns') + Resolv::DNS.expects(:new).returns(@dns_mock) + + @port = 7205 + @host = '_x-puppet._tcp.example.com' + @srv_records = [Resolv::DNS::Resource::IN::SRV.new(0, 0, @port, @host)] + + @dns_mock.expects(:getresources). + with("_x-puppet._tcp.#{Puppet.settings[:srv_domain]}", Resolv::DNS::Resource::IN::SRV). + returns(@srv_records) + end + + it "yields a request using the server and port from the SRV record" do + count = 0 + rval = @request.do_request do |got| + count += 1 + got.server.should == '_x-puppet._tcp.example.com' + got.port.should == 7205 + + @block_return + end + count.should == 1 + + rval.should == @block_return + end + + it "should fall back to the default server when the block raises a SystemCallError" do + count = 0 + second_pass = nil + + rval = @request.do_request(:puppet, 'puppet', 8140) do |got| + count += 1 + + if got.server == '_x-puppet._tcp.example.com' then + raise SystemCallError, "example failure" + else + second_pass = got + end + + @block_return + end + + second_pass.server.should == 'puppet' + second_pass.port.should == 8140 + count.should == 2 + + rval.should == @block_return + end + end + end + end end diff --git a/spec/unit/indirector/rest_spec.rb b/spec/unit/indirector/rest_spec.rb index f18e99a3c..e664ef20a 100755 --- a/spec/unit/indirector/rest_spec.rb +++ b/spec/unit/indirector/rest_spec.rb @@ -1,567 +1,592 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/indirector/rest' shared_examples_for "a REST http call" do it "should accept a path" do lambda { @search.send(@method, *@arguments) }.should_not raise_error(ArgumentError) end it "should require a path" do lambda { @searcher.send(@method) }.should raise_error(ArgumentError) end it "should return the results of deserializing the response to the request" do conn = mock 'connection' conn.stubs(:put).returns @response conn.stubs(:delete).returns @response conn.stubs(:get).returns @response Puppet::Network::HttpPool.stubs(:http_instance).returns conn @searcher.expects(:deserialize).with(@response).returns "myobject" @searcher.send(@method, *@arguments).should == 'myobject' end end describe Puppet::Indirector::REST do before :all do Puppet::Indirector::Terminus.stubs(:register_terminus_class) @model = stub('model', :supported_formats => %w{}, :convert_from => nil) @instance = stub('model instance', :name= => nil) @indirection = stub('indirection', :name => :mystuff, :register_terminus_type => nil, :model => @model) Puppet::Indirector::Indirection.expects(:instance).returns(@indirection) module This module Is module A module Test end end end end @rest_class = class This::Is::A::Test::Class < Puppet::Indirector::REST self end end before :each do @response = stub('mock response', :body => 'result', :code => "200") @response.stubs(:[]).with('content-type').returns "text/plain" @response.stubs(:[]).with('content-encoding').returns nil @searcher = @rest_class.new @searcher.stubs(:model).returns @model end it "should include the v1 REST API module" do Puppet::Indirector::REST.ancestors.should be_include(Puppet::Network::HTTP::API::V1) end it "should have a method for specifying what setting a subclass should use to retrieve its server" do @rest_class.should respond_to(:use_server_setting) end it "should use any specified setting to pick the server" do @rest_class.expects(:server_setting).returns :servset Puppet.settings.expects(:value).with(:servset).returns "myserver" @rest_class.server.should == "myserver" end it "should default to :server for the server setting" do @rest_class.expects(:server_setting).returns nil Puppet.settings.expects(:value).with(:server).returns "myserver" @rest_class.server.should == "myserver" end it "should have a method for specifying what setting a subclass should use to retrieve its port" do @rest_class.should respond_to(:use_port_setting) end it "should use any specified setting to pick the port" do @rest_class.expects(:port_setting).returns :servset Puppet.settings.expects(:value).with(:servset).returns "321" @rest_class.port.should == 321 end it "should default to :port for the port setting" do @rest_class.expects(:port_setting).returns nil Puppet.settings.expects(:value).with(:masterport).returns "543" @rest_class.port.should == 543 end describe "when making http requests" do include PuppetSpec::Files it "should provide a suggestive error message when certificate verify failed" do connection = Net::HTTP.new('my_server', 8140) @searcher.stubs(:network).returns(connection) connection.stubs(:get).raises(OpenSSL::SSL::SSLError.new('certificate verify failed')) expect do @searcher.http_request(:get, stub('request')) end.to raise_error(/This is often because the time is out of sync on the server or client/) end it "should provide a helpful error message when hostname was not match with server certificate", :unless => Puppet.features.microsoft_windows? do Puppet[:confdir] = tmpdir('conf') cert = Puppet::SSL::CertificateAuthority.new.generate('not_my_server', :dns_alt_names => 'foo,bar,baz').content connection = Net::HTTP.new('my_server', 8140) @searcher.stubs(:network).returns(connection) ssl_context = OpenSSL::SSL::SSLContext.new ssl_context.stubs(:current_cert).returns(cert) connection.stubs(:get).with do connection.verify_callback.call(true, ssl_context) end.raises(OpenSSL::SSL::SSLError.new('hostname was not match with server certificate')) msg = /Server hostname 'my_server' did not match server certificate; expected one of (.+)/ expect { @searcher.http_request(:get, stub('request')) }.to( raise_error(Puppet::Error, msg) do |error| error.message =~ msg $1.split(', ').should =~ %w[DNS:foo DNS:bar DNS:baz DNS:not_my_server not_my_server] end ) end it "should pass along the error message otherwise" do connection = Net::HTTP.new('my_server', 8140) @searcher.stubs(:network).returns(connection) connection.stubs(:get).raises(OpenSSL::SSL::SSLError.new('some other message')) expect do @searcher.http_request(:get, stub('request')) end.to raise_error(/some other message/) end end + it 'should default to :puppet for the srv_service' do + Puppet::Indirector::REST.srv_service.should == :puppet + end + describe "when deserializing responses" do it "should return nil if the response code is 404" do response = mock 'response' response.expects(:code).returns "404" @searcher.deserialize(response).should be_nil end [300,400,403,405,500,501,502,503,504].each { |rc| describe "when the response code is #{rc}" do before :each do @model.expects(:convert_from).never @response = mock 'response' @response.stubs(:code).returns rc.to_s @response.stubs(:[]).with('content-encoding').returns nil @response.stubs(:message).returns "There was a problem (header)" end it "should fail" do @response.stubs(:body).returns nil lambda { @searcher.deserialize(@response) }.should raise_error(Net::HTTPError) end it "should take the error message from the body, if present" do @response.stubs(:body).returns "There was a problem (body)" lambda { @searcher.deserialize(@response) }.should raise_error(Net::HTTPError,"Error #{rc} on SERVER: There was a problem (body)") end it "should take the error message from the response header if the body is empty" do @response.stubs(:body).returns "" lambda { @searcher.deserialize(@response) }.should raise_error(Net::HTTPError,"Error #{rc} on SERVER: There was a problem (header)") end it "should take the error message from the response header if the body is absent" do @response.stubs(:body).returns nil lambda { @searcher.deserialize(@response) }.should raise_error(Net::HTTPError,"Error #{rc} on SERVER: There was a problem (header)") end describe "and with http compression" do it "should uncompress the body" do @response.stubs(:body).returns("compressed body") @searcher.expects(:uncompress_body).with(@response).returns("uncompressed") lambda { @searcher.deserialize(@response) }.should raise_error { |e| e.message =~ /uncompressed/ } end end end } it "should return the results of converting from the format specified by the content-type header if the response code is in the 200s" do @model.expects(:convert_from).with("myformat", "mydata").returns "myobject" response = mock 'response' response.stubs(:[]).with("content-type").returns "myformat" response.stubs(:[]).with("content-encoding").returns nil response.stubs(:body).returns "mydata" response.stubs(:code).returns "200" @searcher.deserialize(response).should == "myobject" end it "should convert and return multiple instances if the return code is in the 200s and 'multiple' is specified" do @model.expects(:convert_from_multiple).with("myformat", "mydata").returns "myobjects" response = mock 'response' response.stubs(:[]).with("content-type").returns "myformat" response.stubs(:[]).with("content-encoding").returns nil response.stubs(:body).returns "mydata" response.stubs(:code).returns "200" @searcher.deserialize(response, true).should == "myobjects" end it "should strip the content-type header to keep only the mime-type" do @model.expects(:convert_from).with("text/plain", "mydata").returns "myobject" response = mock 'response' response.stubs(:[]).with("content-type").returns "text/plain; charset=utf-8" response.stubs(:[]).with("content-encoding").returns nil response.stubs(:body).returns "mydata" response.stubs(:code).returns "200" @searcher.deserialize(response) end it "should uncompress the body" do @model.expects(:convert_from).with("myformat", "uncompressed mydata").returns "myobject" response = mock 'response' response.stubs(:[]).with("content-type").returns "myformat" response.stubs(:body).returns "compressed mydata" response.stubs(:code).returns "200" @searcher.expects(:uncompress_body).with(response).returns("uncompressed mydata") @searcher.deserialize(response).should == "myobject" end end describe "when creating an HTTP client" do before do Puppet.settings.stubs(:value).returns("rest_testing") end it "should use the class's server and port if the indirection request provides neither" do @request = stub 'request', :key => "foo", :server => nil, :port => nil @searcher.class.expects(:port).returns 321 @searcher.class.expects(:server).returns "myserver" Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn" @searcher.network(@request).should == "myconn" end it "should use the server from the indirection request if one is present" do @request = stub 'request', :key => "foo", :server => "myserver", :port => nil @searcher.class.stubs(:port).returns 321 Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn" @searcher.network(@request).should == "myconn" end it "should use the port from the indirection request if one is present" do @request = stub 'request', :key => "foo", :server => nil, :port => 321 @searcher.class.stubs(:server).returns "myserver" Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn" @searcher.network(@request).should == "myconn" end end describe "when doing a find" do before :each do @connection = stub('mock http connection', :get => @response, :verify_callback= => nil) @searcher.stubs(:network).returns(@connection) # neuter the network connection # Use a key with spaces, so we can test escaping @request = Puppet::Indirector::Request.new(:foo, :find, "foo bar", :environment => "myenv") end describe "with a large body" do it "should use the POST http method" do params = {} 'aa'.upto('zz') do |s| params[s] = 'foo' end # The request special-cases this parameter, and it # won't be passed on to the server, so we remove it here # to avoid a failure. params.delete('ip') @request = Puppet::Indirector::Request.new(:foo, :find, "foo bar", params.merge(:environment => "myenv")) @connection.expects(:post).with do |uri, body| uri == "/myenv/foo/foo%20bar" and body.split("&").sort == params.map {|key,value| "#{key}=#{value}"}.sort end.returns(@response) @searcher.find(@request) end end describe "with a small body" do it "should use the GET http method" do @searcher.expects(:network).returns @connection @connection.expects(:get).returns @response @searcher.find(@request) end end it "should deserialize and return the http response, setting name" do @connection.expects(:get).returns @response instance = stub 'object' instance.expects(:name=) @searcher.expects(:deserialize).with(@response).returns instance @searcher.find(@request).should == instance end it "should deserialize and return the http response, and not require name=" do @connection.expects(:get).returns @response instance = stub 'object' @searcher.expects(:deserialize).with(@response).returns instance @searcher.find(@request).should == instance end it "should use the URI generated by the Handler module" do @connection.expects(:get).with { |path, args| path == "/myenv/foo/foo%20bar?" }.returns(@response) @searcher.find(@request) end it "should provide an Accept header containing the list of supported formats joined with commas" do @connection.expects(:get).with { |path, args| args["Accept"] == "supported, formats" }.returns(@response) @searcher.model.expects(:supported_formats).returns %w{supported formats} @searcher.find(@request) end it "should add Accept-Encoding header" do @searcher.expects(:add_accept_encoding).returns({"accept-encoding" => "gzip"}) @connection.expects(:get).with { |path, args| args["accept-encoding"] == "gzip" }.returns(@response) @searcher.find(@request) end it "should deserialize and return the network response" do @searcher.expects(:deserialize).with(@response).returns @instance @searcher.find(@request).should equal(@instance) end it "should set the name of the resulting instance to the asked-for name" do @searcher.expects(:deserialize).with(@response).returns @instance @instance.expects(:name=).with "foo bar" @searcher.find(@request) end it "should generate an error when result data deserializes fails" do @searcher.expects(:deserialize).raises(ArgumentError) lambda { @searcher.find(@request) }.should raise_error(ArgumentError) end end describe "when doing a head" do before :each do @connection = stub('mock http connection', :head => @response, :verify_callback= => nil) @searcher.stubs(:network).returns(@connection) # Use a key with spaces, so we can test escaping @request = Puppet::Indirector::Request.new(:foo, :head, "foo bar") end it "should call the HEAD http method on a network connection" do @searcher.expects(:network).returns @connection @connection.expects(:head).returns @response @searcher.head(@request) end it "should return true if there was a successful http response" do @connection.expects(:head).returns @response @response.stubs(:code).returns "200" @searcher.head(@request).should == true end it "should return false if there was a successful http response" do @connection.expects(:head).returns @response @response.stubs(:code).returns "404" @searcher.head(@request).should == false end it "should use the URI generated by the Handler module" do @searcher.expects(:indirection2uri).with(@request).returns "/my/uri" @connection.expects(:head).with { |path, args| path == "/my/uri" }.returns(@response) @searcher.head(@request) end end describe "when doing a search" do before :each do @connection = stub('mock http connection', :get => @response, :verify_callback= => nil) @searcher.stubs(:network).returns(@connection) # neuter the network connection @model.stubs(:convert_from_multiple) @request = Puppet::Indirector::Request.new(:foo, :search, "foo bar") end it "should call the GET http method on a network connection" do @searcher.expects(:network).returns @connection @connection.expects(:get).returns @response @searcher.search(@request) end it "should deserialize as multiple instances and return the http response" do @connection.expects(:get).returns @response @searcher.expects(:deserialize).with(@response, true).returns "myobject" @searcher.search(@request).should == 'myobject' end it "should use the URI generated by the Handler module" do @searcher.expects(:indirection2uri).with(@request).returns "/mys/uri" @connection.expects(:get).with { |path, args| path == "/mys/uri" }.returns(@response) @searcher.search(@request) end it "should provide an Accept header containing the list of supported formats joined with commas" do @connection.expects(:get).with { |path, args| args["Accept"] == "supported, formats" }.returns(@response) @searcher.model.expects(:supported_formats).returns %w{supported formats} @searcher.search(@request) end it "should return an empty array if serialization returns nil" do @model.stubs(:convert_from_multiple).returns nil @searcher.search(@request).should == [] end it "should generate an error when result data deserializes fails" do @searcher.expects(:deserialize).raises(ArgumentError) lambda { @searcher.search(@request) }.should raise_error(ArgumentError) end end describe "when doing a destroy" do before :each do @connection = stub('mock http connection', :delete => @response, :verify_callback= => nil) @searcher.stubs(:network).returns(@connection) # neuter the network connection @request = Puppet::Indirector::Request.new(:foo, :destroy, "foo bar") end it "should call the DELETE http method on a network connection" do @searcher.expects(:network).returns @connection @connection.expects(:delete).returns @response @searcher.destroy(@request) end it "should fail if any options are provided, since DELETE apparently does not support query options" do @request.stubs(:options).returns(:one => "two", :three => "four") lambda { @searcher.destroy(@request) }.should raise_error(ArgumentError) end it "should deserialize and return the http response" do @connection.expects(:delete).returns @response @searcher.expects(:deserialize).with(@response).returns "myobject" @searcher.destroy(@request).should == 'myobject' end it "should use the URI generated by the Handler module" do @searcher.expects(:indirection2uri).with(@request).returns "/my/uri" @connection.expects(:delete).with { |path, args| path == "/my/uri" }.returns(@response) @searcher.destroy(@request) end it "should not include the query string" do @connection.stubs(:delete).returns @response @searcher.destroy(@request) end it "should provide an Accept header containing the list of supported formats joined with commas" do @connection.expects(:delete).with { |path, args| args["Accept"] == "supported, formats" }.returns(@response) @searcher.model.expects(:supported_formats).returns %w{supported formats} @searcher.destroy(@request) end it "should deserialize and return the network response" do @searcher.expects(:deserialize).with(@response).returns @instance @searcher.destroy(@request).should equal(@instance) end it "should generate an error when result data deserializes fails" do @searcher.expects(:deserialize).raises(ArgumentError) lambda { @searcher.destroy(@request) }.should raise_error(ArgumentError) end end describe "when doing a save" do before :each do @connection = stub('mock http connection', :put => @response, :verify_callback= => nil) @searcher.stubs(:network).returns(@connection) # neuter the network connection @instance = stub 'instance', :render => "mydata", :mime => "mime" @request = Puppet::Indirector::Request.new(:foo, :save, "foo bar") @request.instance = @instance end it "should call the PUT http method on a network connection" do @searcher.expects(:network).returns @connection @connection.expects(:put).returns @response @searcher.save(@request) end it "should fail if any options are provided, since DELETE apparently does not support query options" do @request.stubs(:options).returns(:one => "two", :three => "four") lambda { @searcher.save(@request) }.should raise_error(ArgumentError) end it "should use the URI generated by the Handler module" do @searcher.expects(:indirection2uri).with(@request).returns "/my/uri" @connection.expects(:put).with { |path, args| path == "/my/uri" }.returns(@response) @searcher.save(@request) end it "should serialize the instance using the default format and pass the result as the body of the request" do @instance.expects(:render).returns "serial_instance" @connection.expects(:put).with { |path, data, args| data == "serial_instance" }.returns @response @searcher.save(@request) end it "should deserialize and return the http response" do @connection.expects(:put).returns @response @searcher.expects(:deserialize).with(@response).returns "myobject" @searcher.save(@request).should == 'myobject' end it "should provide an Accept header containing the list of supported formats joined with commas" do @connection.expects(:put).with { |path, data, args| args["Accept"] == "supported, formats" }.returns(@response) @searcher.model.expects(:supported_formats).returns %w{supported formats} @searcher.save(@request) end it "should provide a Content-Type header containing the mime-type of the sent object" do @connection.expects(:put).with { |path, data, args| args['Content-Type'] == "mime" }.returns(@response) @instance.expects(:mime).returns "mime" @searcher.save(@request) end it "should deserialize and return the network response" do @searcher.expects(:deserialize).with(@response).returns @instance @searcher.save(@request).should equal(@instance) end it "should generate an error when result data deserializes fails" do @searcher.expects(:deserialize).raises(ArgumentError) lambda { @searcher.save(@request) }.should raise_error(ArgumentError) end end + + context 'dealing with SRV settings' do + [ + :destroy, + :find, + :head, + :save, + :search + ].each do |method| + it "##{method} passes the SRV service, and fall-back server & port to the request's do_request method" do + request = Puppet::Indirector::Request.new(:indirection, method, 'key') + stub_response = stub 'response' + stub_response.stubs(:code).returns('200') + @searcher.stubs(:deserialize) + + request.expects(:do_request).with(@searcher.class.srv_service, @searcher.class.server, @searcher.class.port).returns(stub_response) + + @searcher.send(method, request) + end + end + end end diff --git a/spec/unit/network/resolver_spec.rb b/spec/unit/network/resolver_spec.rb new file mode 100755 index 000000000..aedd9c001 --- /dev/null +++ b/spec/unit/network/resolver_spec.rb @@ -0,0 +1,207 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../spec_helper' +require 'puppet/network/resolver' + +describe Puppet::Network::Resolver do + before do + @dns_mock_object = mock('dns') + Resolv::DNS.stubs(:new).returns(@dns_mock_object) + + @rr_type = Resolv::DNS::Resource::IN::SRV + @test_srv_domain = "domain.com" + @test_a_hostname = "puppet.domain.com" + @test_port = 1000 + + # The records we should use. + @test_records = [ + # priority, weight, port, hostname + Resolv::DNS::Resource::IN::SRV.new(0, 20, 8140, "puppet1.domain.com"), + Resolv::DNS::Resource::IN::SRV.new(0, 80, 8140, "puppet2.domain.com"), + Resolv::DNS::Resource::IN::SRV.new(1, 1, 8140, "puppet3.domain.com"), + Resolv::DNS::Resource::IN::SRV.new(4, 1, 8140, "puppet4.domain.com") + ] + end + + describe 'when the domain is not known' do + before :each do + @dns_mock_object.stubs(:getresources).returns(@test_records) + end + + describe 'because domain is nil' do + it 'does not yield' do + Puppet::Network::Resolver.each_srv_record(nil) do |_,_,_| + raise Exception.new("nil domain caused SRV lookup") + end + end + end + + describe 'because domain is an empty string' do + it 'does not yield' do + Puppet::Network::Resolver.each_srv_record('') do |_,_,_| + raise Exception.new("nil domain caused SRV lookup") + end + end + end + end + + describe "when resolving a host without SRV records" do + it "should not yield anything" do + # No records returned for a DNS entry without any SRV records + @dns_mock_object.expects(:getresources).with( + "_x-puppet._tcp.#{@test_a_hostname}", + @rr_type + ).returns([]) + + Puppet::Network::Resolver.each_srv_record(@test_a_hostname) do |hostname, port, remaining| + raise Exception.new("host with no records passed block") + end + end + end + + describe "when resolving a host with SRV records" do + it "should iterate through records in priority order" do + # The order of the records that should be returned, + # an array means unordered (for weight) + order = { + 0 => ["puppet1.domain.com", "puppet2.domain.com"], + 1 => ["puppet3.domain.com"], + 2 => ["puppet4.domain.com"] + } + + @dns_mock_object.expects(:getresources).with( + "_x-puppet._tcp.#{@test_srv_domain}", + @rr_type + ).returns(@test_records) + + Puppet::Network::Resolver.each_srv_record(@test_srv_domain) do |hostname, port| + expected_priority = order.keys.min + + order[expected_priority].should include(hostname) + port.should_not be(@test_port) + + # Remove the host from our expected hosts + order[expected_priority].delete hostname + + # Remove this priority level if we're done with it + order.delete expected_priority if order[expected_priority] == [] + end + end + + it "should fall back to the :puppet service if no records are found for a more specific service" do + # The order of the records that should be returned, + # an array means unordered (for weight) + order = { + 0 => ["puppet1.domain.com", "puppet2.domain.com"], + 1 => ["puppet3.domain.com"], + 2 => ["puppet4.domain.com"] + } + + @dns_mock_object.expects(:getresources).with( + "_x-puppet-report._tcp.#{@test_srv_domain}", + @rr_type + ).returns([]) + + @dns_mock_object.expects(:getresources).with( + "_x-puppet._tcp.#{@test_srv_domain}", + @rr_type + ).returns(@test_records) + + Puppet::Network::Resolver.each_srv_record(@test_srv_domain, :report) do |hostname, port| + expected_priority = order.keys.min + + order[expected_priority].should include(hostname) + port.should_not be(@test_port) + + # Remove the host from our expected hosts + order[expected_priority].delete hostname + + # Remove this priority level if we're done with it + order.delete expected_priority if order[expected_priority] == [] + end + end + + it "should use SRV records from the specific service if they exist" do + # The order of the records that should be returned, + # an array means unordered (for weight) + order = { + 0 => ["puppet1.domain.com", "puppet2.domain.com"], + 1 => ["puppet3.domain.com"], + 2 => ["puppet4.domain.com"] + } + + bad_records = [ + # priority, weight, port, hostname + Resolv::DNS::Resource::IN::SRV.new(0, 20, 8140, "puppet1.bad.domain.com"), + Resolv::DNS::Resource::IN::SRV.new(0, 80, 8140, "puppet2.bad.domain.com"), + Resolv::DNS::Resource::IN::SRV.new(1, 1, 8140, "puppet3.bad.domain.com"), + Resolv::DNS::Resource::IN::SRV.new(4, 1, 8140, "puppet4.bad.domain.com") + ] + + @dns_mock_object.expects(:getresources).with( + "_x-puppet-report._tcp.#{@test_srv_domain}", + @rr_type + ).returns(@test_records) + + @dns_mock_object.stubs(:getresources).with( + "_x-puppet._tcp.#{@test_srv_domain}", + @rr_type + ).returns(bad_records) + + Puppet::Network::Resolver.each_srv_record(@test_srv_domain, :report) do |hostname, port| + expected_priority = order.keys.min + + order[expected_priority].should include(hostname) + port.should_not be(@test_port) + + # Remove the host from our expected hosts + order[expected_priority].delete hostname + + # Remove this priority level if we're done with it + order.delete expected_priority if order[expected_priority] == [] + end + end + end + + describe "when finding weighted servers" do + it "should return nil when no records were found" do + Puppet::Network::Resolver.find_weighted_server([]).should == nil + end + + it "should return the first record when one record is passed" do + result = Puppet::Network::Resolver.find_weighted_server([@test_records.first]) + result.should == @test_records.first + end + + { + "all have weights" => [1, 3, 2, 4], + "some have weights" => [2, 0, 1, 0], + "none have weights" => [0, 0, 0, 0], + }.each do |name, weights| + it "should return correct results when #{name}" do + records = [] + count = 0 + weights.each do |w| + count += 1 + # priority, weight, port, server + records << Resolv::DNS::Resource::IN::SRV.new(0, w, 1, count.to_s) + end + + seen = Hash.new(0) + total_weight = records.inject(0) do |sum, record| + sum + Puppet::Network::Resolver.weight(record) + end + + total_weight.times do |n| + Kernel.expects(:rand).once.with(total_weight).returns(n) + server = Puppet::Network::Resolver.find_weighted_server(records) + seen[server] += 1 + end + + seen.length.should == records.length + records.each do |record| + seen[record].should == Puppet::Network::Resolver.weight(record) + end + end + end + end +end diff --git a/spec/unit/type/file/content_spec.rb b/spec/unit/type/file/content_spec.rb index 108acc005..ce1eddd48 100755 --- a/spec/unit/type/file/content_spec.rb +++ b/spec/unit/type/file/content_spec.rb @@ -1,437 +1,439 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/network/http_pool' +require 'puppet/network/resolver' + content = Puppet::Type.type(:file).attrclass(:content) describe content do include PuppetSpec::Files before do @filename = tmpfile('testfile') @resource = Puppet::Type.type(:file).new :path => @filename File.open(@filename, 'w') {|f| f.write "initial file content"} content.stubs(:standalone?).returns(false) end describe "when determining the checksum type" do it "should use the type specified in the source checksum if a source is set" do @resource[:source] = File.expand_path("/foo") @resource.parameter(:source).expects(:checksum).returns "{md5lite}eh" @content = content.new(:resource => @resource) @content.checksum_type.should == :md5lite end it "should use the type specified by the checksum parameter if no source is set" do @resource[:checksum] = :md5lite @content = content.new(:resource => @resource) @content.checksum_type.should == :md5lite end end describe "when determining the actual content to write" do it "should use the set content if available" do @content = content.new(:resource => @resource) @content.should = "ehness" @content.actual_content.should == "ehness" end it "should not use the content from the source if the source is set" do source = mock 'source' @resource.expects(:parameter).never.with(:source).returns source @content = content.new(:resource => @resource) @content.actual_content.should be_nil end end describe "when setting the desired content" do it "should make the actual content available via an attribute" do @content = content.new(:resource => @resource) @content.stubs(:checksum_type).returns "md5" @content.should = "this is some content" @content.actual_content.should == "this is some content" end it "should store the checksum as the desired content" do @content = content.new(:resource => @resource) digest = Digest::MD5.hexdigest("this is some content") @content.stubs(:checksum_type).returns "md5" @content.should = "this is some content" @content.should.must == "{md5}#{digest}" end it "should not checksum 'absent'" do @content = content.new(:resource => @resource) @content.should = :absent @content.should.must == :absent end it "should accept a checksum as the desired content" do @content = content.new(:resource => @resource) digest = Digest::MD5.hexdigest("this is some content") string = "{md5}#{digest}" @content.should = string @content.should.must == string end end describe "when retrieving the current content" do it "should return :absent if the file does not exist" do @content = content.new(:resource => @resource) @resource.expects(:stat).returns nil @content.retrieve.should == :absent end it "should not manage content on directories" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "directory" @resource.expects(:stat).returns stat @content.retrieve.should be_nil end it "should not manage content on links" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "link" @resource.expects(:stat).returns stat @content.retrieve.should be_nil end it "should always return the checksum as a string" do @content = content.new(:resource => @resource) @resource[:checksum] = :mtime stat = mock 'stat', :ftype => "file" @resource.expects(:stat).returns stat time = Time.now @resource.parameter(:checksum).expects(:mtime_file).with(@resource[:path]).returns time @content.retrieve.should == "{mtime}#{time}" end it "should return the checksum of the file if it exists and is a normal file" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "file" @resource.expects(:stat).returns stat @resource.parameter(:checksum).expects(:md5_file).with(@resource[:path]).returns "mysum" @content.retrieve.should == "{md5}mysum" end end describe "when testing whether the content is in sync" do before do @resource[:ensure] = :file @content = content.new(:resource => @resource) end it "should return true if the resource shouldn't be a regular file" do @resource.expects(:should_be_file?).returns false @content.should = "foo" @content.must be_safe_insync("whatever") end it "should return false if the current content is :absent" do @content.should = "foo" @content.should_not be_safe_insync(:absent) end it "should return false if the file should be a file but is not present" do @resource.expects(:should_be_file?).returns true @content.should = "foo" @content.should_not be_safe_insync(:absent) end describe "and the file exists" do before do @resource.stubs(:stat).returns mock("stat") end it "should return false if the current contents are different from the desired content" do @content.should = "some content" @content.should_not be_safe_insync("other content") end it "should return true if the sum for the current contents is the same as the sum for the desired content" do @content.should = "some content" @content.must be_safe_insync("{md5}" + Digest::MD5.hexdigest("some content")) end describe "and Puppet[:show_diff] is set" do before do Puppet[:show_diff] = true end it "should display a diff if the current contents are different from the desired content" do @content.should = "some content" @content.expects(:diff).returns("my diff").once @content.expects(:notice).with("\nmy diff").once @content.safe_insync?("other content") end it "should not display a diff if the sum for the current contents is the same as the sum for the desired content" do @content.should = "some content" @content.expects(:diff).never @content.safe_insync?("{md5}" + Digest::MD5.hexdigest("some content")) end end end describe "and :replace is false" do before do @resource.stubs(:replace?).returns false end it "should be insync if the file exists and the content is different" do @resource.stubs(:stat).returns mock('stat') @content.must be_safe_insync("whatever") end it "should be insync if the file exists and the content is right" do @resource.stubs(:stat).returns mock('stat') @content.must be_safe_insync("something") end it "should not be insync if the file does not exist" do @content.should = "foo" @content.should_not be_safe_insync(:absent) end end end describe "when changing the content" do before do @content = content.new(:resource => @resource) @content.should = "some content" @resource.stubs(:[]).with(:path).returns "/boo" @resource.stubs(:stat).returns "eh" end it "should use the file's :write method to write the content" do @resource.expects(:write).with(:content) @content.sync end it "should return :file_changed if the file already existed" do @resource.expects(:stat).returns "something" @resource.stubs(:write) @content.sync.should == :file_changed end it "should return :file_created if the file did not exist" do @resource.expects(:stat).returns nil @resource.stubs(:write) @content.sync.should == :file_created end end describe "when writing" do before do @content = content.new(:resource => @resource) end it "should attempt to read from the filebucket if no actual content nor source exists" do @fh = File.open(@filename, 'wb') @content.should = "{md5}foo" @content.resource.bucket.class.any_instance.stubs(:getfile).returns "foo" @content.write(@fh) @fh.close end describe "from actual content" do before(:each) do @content.stubs(:actual_content).returns("this is content") end it "should write to the given file handle" do @fh.expects(:print).with("this is content") @content.write(@fh) end it "should return the current checksum value" do @resource.parameter(:checksum).expects(:sum_stream).returns "checksum" @content.write(@fh).should == "checksum" end end describe "from a file bucket" do it "should fail if a file bucket cannot be retrieved" do @content.should = "{md5}foo" @content.resource.expects(:bucket).returns nil lambda { @content.write(@fh) }.should raise_error(Puppet::Error) end it "should fail if the file bucket cannot find any content" do @content.should = "{md5}foo" bucket = stub 'bucket' @content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").raises "foobar" lambda { @content.write(@fh) }.should raise_error(Puppet::Error) end it "should write the returned content to the file" do @content.should = "{md5}foo" bucket = stub 'bucket' @content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").returns "mycontent" @fh.expects(:print).with("mycontent") @content.write(@fh) end end describe "from local source" do before(:each) do @sourcename = tmpfile('source') @resource = Puppet::Type.type(:file).new :path => @filename, :backup => false, :source => @sourcename @source_content = "source file content\r\n"*10000 @sourcefile = File.open(@sourcename, 'wb') {|f| f.write @source_content} @content = @resource.newattr(:content) @source = @resource.parameter :source #newattr(:source) end it "should copy content from the source to the file" do @resource.write(@source) Puppet::Util.binread(@filename).should == @source_content end it "should return the checksum computed" do File.open(@filename, 'wb') do |file| @content.write(file).should == "{md5}#{Digest::MD5.hexdigest(@source_content)}" end end end describe "from remote source" do before(:each) do @resource = Puppet::Type.type(:file).new :path => @filename, :backup => false @response = stub_everything 'response', :code => "200" @source_content = "source file content\n"*10000 @response.stubs(:read_body).multiple_yields(*(["source file content\n"]*10000)) @conn = stub_everything 'connection' - @conn.stubs(:request_get).yields(@response) + @conn.stubs(:request_get).returns(@response) Puppet::Network::HttpPool.stubs(:http_instance).returns @conn @content = @resource.newattr(:content) @sourcename = "puppet:///test/foo" @source = @resource.newattr(:source) @source.stubs(:metadata).returns stub_everything('metadata', :source => @sourcename, :ftype => 'file') end it "should write the contents to the file" do @resource.write(@source) Puppet::Util.binread(@filename).should == @source_content end it "should not write anything if source is not found" do @response.stubs(:code).returns("404") lambda {@resource.write(@source)}.should raise_error(Net::HTTPError) { |e| e.message =~ /404/ } File.read(@filename).should == "initial file content" end it "should raise an HTTP error in case of server error" do @response.stubs(:code).returns("500") lambda { @content.write(@fh) }.should raise_error { |e| e.message.include? @source_content } end it "should return the checksum computed" do File.open(@filename, 'w') do |file| @content.write(file).should == "{md5}#{Digest::MD5.hexdigest(@source_content)}" end end end # These are testing the implementation rather than the desired behaviour; while that bites, there are a whole # pile of other methods in the File type that depend on intimate details of this implementation and vice-versa. # If these blow up, you are gonna have to review the callers to make sure they don't explode! --daniel 2011-02-01 describe "each_chunk_from should work" do before do @content = content.new(:resource => @resource) end it "when content is a string" do @content.each_chunk_from('i_am_a_string') { |chunk| chunk.should == 'i_am_a_string' } end # The following manifest is a case where source and content.should are both set # file { "/tmp/mydir" : # source => '/tmp/sourcedir', # recurse => true, # } it "when content checksum comes from source" do source_param = Puppet::Type.type(:file).attrclass(:source) source = source_param.new(:resource => @resource) @content.should = "{md5}123abcd" @content.expects(:chunk_file_from_source).returns('from_source') @content.each_chunk_from(source) { |chunk| chunk.should == 'from_source' } end it "when no content, source, but ensure present" do @resource[:ensure] = :present @content.each_chunk_from(nil) { |chunk| chunk.should == '' } end # you might do this if you were just auditing it "when no content, source, but ensure file" do @resource[:ensure] = :file @content.each_chunk_from(nil) { |chunk| chunk.should == '' } end it "when source_or_content is nil and content not a checksum" do @content.each_chunk_from(nil) { |chunk| chunk.should == '' } end # the content is munged so that if it's a checksum nil gets passed in it "when content is a checksum it should try to read from filebucket" do @content.should = "{md5}123abcd" @content.expects(:read_file_from_filebucket).once.returns('im_a_filebucket') @content.each_chunk_from(nil) { |chunk| chunk.should == 'im_a_filebucket' } end it "when running as puppet apply" do @content.class.expects(:standalone?).returns true source_or_content = stubs('source_or_content') source_or_content.expects(:content).once.returns :whoo @content.each_chunk_from(source_or_content) { |chunk| chunk.should == :whoo } end it "when running from source with a local file" do source_or_content = stubs('source_or_content') source_or_content.expects(:local?).returns true @content.expects(:chunk_file_from_disk).with(source_or_content).once.yields 'woot' @content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' } end it "when running from source with a remote file" do source_or_content = stubs('source_or_content') source_or_content.expects(:local?).returns false @content.expects(:chunk_file_from_source).with(source_or_content).once.yields 'woot' @content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' } end end end end diff --git a/test/lib/puppettest.rb b/test/lib/puppettest.rb index 6bae80a01..4d32f1408 100755 --- a/test/lib/puppettest.rb +++ b/test/lib/puppettest.rb @@ -1,318 +1,317 @@ # Add .../test/lib testlib = File.expand_path(File.dirname(__FILE__)) $LOAD_PATH.unshift(testlib) unless $LOAD_PATH.include?(testlib) # Add .../lib mainlib = File.expand_path(File.join(File.dirname(__FILE__), '../../lib')) $LOAD_PATH.unshift(mainlib) unless $LOAD_PATH.include?(mainlib) require 'puppet' require 'mocha' # Only load the test/unit class if we're not in the spec directory. # Else we get the bogus 'no tests, no failures' message. unless Dir.getwd =~ /spec/ require 'test/unit' end # Yay; hackish but it works if ARGV.include?("-d") ARGV.delete("-d") $console = true end require File.expand_path(File.join(File.dirname(__FILE__), '../../spec/monkey_patches/publicize_methods')) module PuppetTest # These need to be here for when rspec tests use these # support methods. @@tmpfiles = [] # Munge cli arguments, so we can enable debugging if we want # and so we can run just specific methods. def self.munge_argv require 'getoptlong' result = GetoptLong.new( [ "--debug", "-d", GetoptLong::NO_ARGUMENT ], [ "--resolve", "-r", GetoptLong::REQUIRED_ARGUMENT ], [ "-n", GetoptLong::REQUIRED_ARGUMENT ], [ "--help", "-h", GetoptLong::NO_ARGUMENT ] ) usage = "USAGE: TESTOPTS='[-n -n ...] [-d]' rake [target] [target] ..." opts = [] dir = method = nil result.each { |opt,arg| case opt when "--resolve" dir, method = arg.split(",") when "--debug" $puppet_debug = true Puppet::Util::Log.level = :debug Puppet::Util::Log.newdestination(:console) when "--help" puts usage exit else opts << opt << arg end } suites = nil args = ARGV.dup # Reset the options, so the test suite can deal with them (this is # what makes things like '-n' work). opts.each { |o| ARGV << o } args end # Find the root of the Puppet tree; this is not the test directory, but # the parent of that dir. def basedir(*list) unless defined?(@@basedir) Dir.chdir(File.dirname(__FILE__)) do @@basedir = File.dirname(File.dirname(Dir.getwd)) end end if list.empty? @@basedir else File.join(@@basedir, *list) end end def datadir(*list) File.join(basedir, "test", "data", *list) end def exampledir(*args) @@exampledir = File.join(basedir, "examples") unless defined?(@@exampledir) if args.empty? return @@exampledir else return File.join(@@exampledir, *args) end end module_function :basedir, :datadir, :exampledir def cleanup(&block) @@cleaners << block end # Rails clobbers RUBYLIB, thanks def libsetup curlibs = ENV["RUBYLIB"].split(":") $LOAD_PATH.reject do |dir| dir =~ /^\/usr/ end.each do |dir| curlibs << dir unless curlibs.include?(dir) end ENV["RUBYLIB"] = curlibs.join(":") end def logcollector collector = [] Puppet::Util::Log.newdestination(collector) cleanup do Puppet::Util::Log.close(collector) end collector end def rake? $0 =~ /test_loader/ end # Redirect stdout and stderr def redirect @stderr = tempfile @stdout = tempfile $stderr = File.open(@stderr, "w") $stdout = File.open(@stdout, "w") cleanup do $stderr = STDERR $stdout = STDOUT end end def setup ENV["PATH"] += File::PATH_SEPARATOR + "/usr/sbin" unless ENV["PATH"].split(File::PATH_SEPARATOR).include?("/usr/sbin") @memoryatstart = Puppet::Util.memory if defined?(@@testcount) @@testcount += 1 else @@testcount = 0 end - @configpath = File.join( - tmpdir, - + @configpath = File.join( + tmpdir, "configdir" + @@testcount.to_s + "/" ) unless defined? $user and $group $user = nonrootuser.uid.to_s $group = nonrootgroup.gid.to_s end Puppet.settings.clear Puppet[:user] = $user Puppet[:group] = $group Puppet[:confdir] = @configpath Puppet[:vardir] = @configpath Dir.mkdir(@configpath) unless File.exists?(@configpath) @@tmpfiles << @configpath << tmpdir @@tmppids = [] @@cleaners = [] @logs = [] # If we're running under rake, then disable debugging and such. #if rake? or ! Puppet[:debug] #if defined?($puppet_debug) or ! rake? Puppet[:color] = false if textmate? Puppet::Util::Log.newdestination(Puppet::Test::LogCollector.new(@logs)) if defined? $console Puppet.info @method_name Puppet::Util::Log.newdestination(:console) Puppet[:trace] = true end Puppet::Util::Log.level = :debug #$VERBOSE = 1 #else # Puppet::Util::Log.close # Puppet::Util::Log.newdestination(@logs) # Puppet[:httplog] = tempfile #end Puppet[:ignoreschedules] = true #@start = Time.now #Facter.stubs(:value).returns "stubbed_value" #Facter.stubs(:to_hash).returns({}) end def tempfile(suffix = '') if defined?(@@tmpfilenum) @@tmpfilenum += 1 else @@tmpfilenum = 1 end f = File.join(self.tmpdir, "tempfile_" + @@tmpfilenum.to_s + suffix) @@tmpfiles ||= [] @@tmpfiles << f f end def textmate? !!ENV["TM_FILENAME"] end def tstdir dir = tempfile Dir.mkdir(dir) dir end def tmpdir unless @tmpdir @tmpdir = case Facter["operatingsystem"].value - when "Darwin"; "/private/tmp" - when "SunOS"; "/var/tmp" + when "Darwin" then "/private/tmp" + when "SunOS" then "/var/tmp" else "/tmp" - end + end @tmpdir = File.join(@tmpdir, "puppettesting#{Process.pid}") unless File.exists?(@tmpdir) FileUtils.mkdir_p(@tmpdir) File.chmod(01777, @tmpdir) end end @tmpdir end def remove_tmp_files @@tmpfiles.each { |file| unless file =~ /tmp/ puts "Not deleting tmpfile #{file}" next end if FileTest.exists?(file) system("chmod -R 755 #{file}") system("rm -rf #{file}") end } @@tmpfiles.clear end def teardown #@stop = Time.now #File.open("/tmp/test_times.log", ::File::WRONLY|::File::CREAT|::File::APPEND) { |f| f.puts "%0.4f %s %s" % [@stop - @start, @method_name, self.class] } @@cleaners.each { |cleaner| cleaner.call } remove_tmp_files @@tmppids.each { |pid| %x{kill -INT #{pid} 2>/dev/null} } @@tmppids.clear Puppet::Util::Storage.clear Puppet.clear Puppet.settings.clear @memoryatend = Puppet::Util.memory diff = @memoryatend - @memoryatstart Puppet.info "#{self.class}##{@method_name} memory growth (#{@memoryatstart} to #{@memoryatend}): #{diff}" if diff > 1000 # reset all of the logs Puppet::Util::Log.close_all @logs.clear # Just in case there are processes waiting to die... require 'timeout' begin Timeout::timeout(5) do Process.waitall end rescue Timeout::Error # just move on end end def logstore @logs = [] Puppet::Util::Log.newdestination(Puppet::Test::LogCollector.new(@logs)) end end require 'puppettest/support' require 'puppettest/filetesting' require 'puppettest/fakes' require 'puppettest/exetest' require 'puppettest/parsertesting' require 'puppettest/servertest' require 'puppettest/testcase'