# # Cookbook Name:: postgresql # Library:: default # Author:: David Crane () # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # include Chef::Mixin::ShellOut module Opscode module PostgresqlHelpers ####### # Function to truncate value to 4 significant bits, render human readable. # Used in recipes/config_initdb.rb to set this attribute: # # The memory settings (shared_buffers, effective_cache_size, work_mem, # maintenance_work_mem and wal_buffers) will be rounded down to keep # the 4 most significant bits, so that SHOW will be likely to use a # larger divisor. The output is actually a human readable string that # ends with "GB", "MB" or "kB" if over 1023, exactly what Postgresql # will expect in a postgresql.conf setting. The output may be up to # 6.25% less than the original value because of the rounding. def binaryround(value) # Keep a multiplier which grows through powers of 1 multiplier = 1 # Truncate value to 4 most significant bits while value >= 16 value = (value / 2).floor multiplier = multiplier * 2 end # Factor any remaining powers of 2 into the multiplier while value == 2*((value / 2).floor) value = (value / 2).floor multiplier = multiplier * 2 end # Factor enough powers of 2 back into the value to # leave the multiplier as a power of 1024 that can # be represented as units of "GB", "MB" or "kB". if multiplier >= 1024*1024*1024 while multiplier > 1024*1024*1024 value = 2*value multiplier = (multiplier/2).floor end multiplier = 1 units = "GB" elsif multiplier >= 1024*1024 while multiplier > 1024*1024 value = 2*value multiplier = (multiplier/2).floor end multiplier = 1 units = "MB" elsif multiplier >= 1024 while multiplier > 1024 value = 2*value multiplier = (multiplier/2).floor end multiplier = 1 units = "kB" else units = "" end # Now we can return a nice human readable string. return "#{multiplier * value}#{units}" end ####### # Locale Configuration # Function to test the date order. # Used in recipes/config_initdb.rb to set this attribute: # node.default['postgresql']['config']['datestyle'] def locale_date_order # Test locale conversion of mon=11, day=22, year=33 testtime = DateTime.new(2033,11,22,0,0,0,"-00:00") #=> # # %x - Preferred representation for the date alone, no time res = testtime.strftime("%x") if res.nil? return 'mdy' end posM = res.index("11") posD = res.index("22") posY = res.index("33") if (posM.nil? || posD.nil? || posY.nil?) return 'mdy' elseif (posY < posM && posM < posD) return 'ymd' elseif (posD < posM) return 'dmy' else return 'mdy' end end ####### # Timezone Configuration require 'find' # Function to determine where the system stored shared timezone data. # Used in recipes/config_initdb.rb to detemine where it should have # select_default_timezone(tzdir) search. def pg_TZDIR() # System time zone conversions are controlled by a timezone data file # identified through environment variables (TZ and TZDIR) and/or file # and directory naming conventions specific to the Linux distribution. # Each of these timezone names will have been loaded into the PostgreSQL # pg_timezone_names view by the package maintainer. # # Instead of using the timezone name configured as the system default, # the PostgreSQL server uses ones named in postgresql.conf settings # (timezone and log_timezone). The initdb utility does initialize those # settings to the timezone name that corresponds to the system default. # # The system's timezone name is actually a filename relative to the # shared zoneinfo directory. That is usually /usr/share/zoneinfo, but # it was /usr/lib/zoneinfo in older distributions and can be anywhere # if specified by the environment variable TZDIR. The tzset(3) manpage # seems to indicate the following precedence: tzdir = nil if ::File.directory?("/usr/lib/zoneinfo") tzdir = "/usr/lib/zoneinfo" else share_path = [ ENV['TZDIR'], "/usr/share/zoneinfo" ].compact.first if ::File.directory?(share_path) tzdir = share_path end end return tzdir end ####### # Function to support select_default_timezone(tzdir), which is # used in recipes/config_initdb.rb. def validate_zone(tzname) # PostgreSQL does not support leap seconds, so this function tests # the usual Linux tzname convention to avoid a misconfiguration. # Assume that the tzdata package maintainer has kept all timezone # data files with support for leap seconds is kept under the # so-named "right/" subdir of the shared zoneinfo directory. # # The original PostgreSQL initdb is not Unix-specific, so it did a # very complicated, thorough test in its pg_tz_acceptable() function # that I could not begin to understand how to do in ruby :). # # Testing the tzname is good enough, since a misconfiguration # will result in an immediate fatal error when the PostgreSQL # service is started, with pgstartup.log messages such as: # LOG: time zone "right/US/Eastern" appears to use leap seconds # DETAIL: PostgreSQL does not support leap seconds. if tzname.index("right/") == 0 return false else return true end end # Function to support select_default_timezone(tzdir), which is # used in recipes/config_initdb.rb. def scan_available_timezones(tzdir) # There should be an /etc/localtime zoneinfo file that is a link to # (or a copy of) a timezone data file under tzdir, which should have # been installed under the "share" directory by the tzdata package. # # The initdb utility determines which shared timezone file is being # used as the system's default /etc/localtime. The timezone name is # the timezone file path relative to the tzdir. bestzonename = nil if (tzdir.nil?) Chef::Log.error("The zoneinfo directory not found (looked for /usr/share/zoneinfo and /usr/lib/zoneinfo)") elsif !::File.exists?("/etc/localtime") Chef::Log.error("The system zoneinfo file not found (looked for /etc/localtime)") elsif ::File.directory?("/etc/localtime") Chef::Log.error("The system zoneinfo file not found (/etc/localtime is a directory instead)") elsif ::File.symlink?("/etc/localtime") # PostgreSQL initdb doesn't use the symlink target, but this # certainly will make sense to any system administrator. A full # scan of the tzdir to find the shortest filename could result # "US/Eastern" instead of "America/New_York" as bestzonename, # in spite of what the sysadmin had specified in the symlink. # (There are many duplicates under tzdir, with the same timezone # content appearing as an average of 2-3 different file names.) path = ::File.readlink("/etc/localtime") bestzonename = path.gsub("#{tzdir}/","") else # /etc/localtime is a file, so scan for it under tzdir localtime_content = File.read("/etc/localtime") Find.find(tzdir) do |path| # Only consider files (skip directories or symlinks) if !::File.directory?(path) && !::File.symlink?(path) # Ignore any file named "posixrules" or "localtime" if ::File.basename(path) != "posixrules" && ::File.basename(path) != "localtime" # Do consider if content exactly matches /etc/localtime. if localtime_content == File.read(path) tzname = path.gsub("#{tzdir}/","") if validate_zone(tzname) if (bestzonename.nil? || tzname.length < bestzonename.length || (tzname.length == bestzonename.length && (tzname <=> bestzonename) < 0) ) bestzonename = tzname end end end end end end end return bestzonename end # Function to support select_default_timezone(tzdir), which is # used in recipes/config_initdb.rb. def identify_system_timezone(tzdir) resultbuf = scan_available_timezones(tzdir) if !resultbuf.nil? # Ignore Olson's rather silly "Factory" zone; use GMT instead if (resultbuf <=> "Factory") == 0 resultbuf = nil end else # Did not find the timezone. Fallback to use a GMT zone. Note that the # Olson timezone database names the GMT-offset zones in POSIX style: plus # is west of Greenwich. testtime = DateTime.now std_ofs = testtime.strftime("%:z").split(":")[0].to_i resultbuf = [ "Etc/GMT", (-std_ofs > 0) ? "+" : "", (-std_ofs).to_s ].join('') end return resultbuf end ####### # Function to determine the name of the system's default timezone. # Used in recipes/config_initdb.rb to set these attributes: # node.default['postgresql']['config']['log_timezone'] # node.default['postgresql']['config']['timezone'] def select_default_timezone(tzdir) system_timezone = nil # Check TZ environment variable tzname = ENV['TZ'] if !tzname.nil? && !tzname.empty? && validate_zone(tzname) system_timezone = tzname else # Nope, so try to identify system timezone from /etc/localtime tzname = identify_system_timezone(tzdir) if validate_zone(tzname) system_timezone = tzname end end return system_timezone end ####### # Function to determine the name of the system's default timezone. def get_result_orig(query) # query could be a String or an Array of String if (query.is_a?(String)) stdin = query else stdin = query.join("\n") end @get_result ||= begin cmd = shell_out("cat", :input => stdin) cmd.stdout end end ####### # Function to execute an SQL statement in the default database. # Input: Query could be a single String or an Array of String. # Output: A String with |-separated columns and \n-separated rows. # Note an empty output could mean psql couldn't connect. # This is easiest for 1-field (1-row, 1-col) results, otherwise # it will be complex to parse the results. def execute_sql(query) db_name = node['postgresql']['database_name'] # query could be a String or an Array of String statement = query.is_a?(String) ? query : query.join("\n") @execute_sql ||= begin cmd = shell_out("psql -q --tuples-only --no-align -d #{db_name} -f -", :user => "postgres", :input => statement ) # If psql fails, generally the postgresql service is down. # Instead of aborting chef with a fatal error, let's just # pass these non-zero exitstatus back as empty cmd.stdout. if (cmd.exitstatus() == 0 and !cmd.stderr.empty?) # An SQL failure is still a zero exitstatus, but then the # stderr explains the error, so let's rais that as fatal. Chef::Log.fatal("psql failed executing this SQL statement:\n#{statement}") Chef::Log.fatal(cmd.stderr) raise "SQL ERROR" end cmd.stdout.chomp end end ####### # Function to determine if a standard contrib extension is already installed. # Input: Extension name # Output: true or false # Best use as a not_if gate on bash "install-#{pg_ext}-extension" resource. def extension_installed?(pg_ext) @extension_installed ||= begin installed=execute_sql("select 'installed' from pg_extension where extname = '#{pg_ext}';") installed =~ /^installed$/ end end ###################################### # Function to build information needed to install RPM for PGDG yum repository, # since PGDG supports several versions of PostgreSQL, platforms, platform versions # and architectures. # Links to RPMs for installation are in an attribute so that new versions/platforms # can be more easily added. (See attributes/default.rb) def pgdgrepo_rpm_info repo_rpm_url = node['postgresql']['pgdg']['repo_rpm_url']. fetch(node['postgresql']['version']). # e.g., fetch for "9.1" fetch(node['platform']). # e.g., fetch for "centos" fetch(node['platform_version'].to_f.to_i.to_s). # e.g., fetch for "5" (truncated "5.7") fetch(node['kernel']['machine']) # e.g., fetch for "i386" or "x86_64" # Extract the filename portion from the URL for the PGDG repository RPM. # E.g., repo_rpm_filename = "pgdg-centos92-9.2-6.noarch.rpm" repo_rpm_filename = File.basename(repo_rpm_url) # Extract the package name from the URL for the PGDG repository RPM. # E.g., repo_rpm_package = "pgdg-centos92" repo_rpm_package = repo_rpm_filename.split(/-/,3)[0..1].join('-') return [ repo_rpm_url, repo_rpm_filename, repo_rpm_package ] end # End the Opscode::PostgresqlHelpers module end end