chef/cookbooks/cpe_filebeat/resources/cpe_filebeat.rb (270 lines of code) (raw):

# # Cookbook:: cpe_filebeat # Resources:: cpe_filebeat # # vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 # # Copyright:: (c) 2019-present, Uber Technologies, Inc. # All rights reserved. # # This source code is licensed under the Apache 2.0 license found in the # LICENSE file in the root directory of this source tree. # unified_mode true resource_name :cpe_filebeat provides :cpe_filebeat, :os => ['darwin', 'linux', 'windows'] default_action :manage action :manage do install if install? configure if configure? cleanup if !install? && !configure? end action_class do # rubocop:disable Metrics/BlockLength def install? node['cpe_filebeat']['install'] end def configure? node['cpe_filebeat']['configure'] end def filebeat_exist? # Check if Filebeat Exist Dir.exist?(filebeat_dir) && ::File.exist?(filebeat_bin) end def current_version? # False if Filebeat Excutable missing or not latest version status = false if filebeat_exist? if macos? || debian? cmd = shell_out('/opt/filebeat/filebeat version').stdout status = cmd.include? zip_info['version'] return status elsif windows? cmd = powershell_out("(Get-Item #{filebeat_bin}).VersionInfo.FileVersion").stdout status = cmd.include?(zip_info['version']) # Guarding false positive on older filebeat version if cmd.nil? || cmd.empty? && zip_info['version'].include?('6.4.2') status = true end return status end end status end def unix_binary? # If filebeat exist but we cant extrapulate version return false status = false if filebeat_exist? cmd = shell_out('sudo /opt/filebeat/filebeat version').stdout status = cmd.include? zip_info['version'] return status end status end def install zip_info.reject { |_k, v| v.nil? } return if zip_info.nil? || zip_info.empty? # Self-remediation and repair logic unhealthy_count = filebeat_unhealthy_count reinstall_count = filebeat_reinstall_count repair = false filebeat_running? ? unhealthy_count = 0 : unhealthy_count += 1 if unhealthy_count > unhealthy_limit unhealthy_count = 0 reinstall_count += 1 repair = true end # Reinstall if service is broken or needs an upgrade cleanup if !current_version? || repair zip_name = value_for_platform_family( 'mac_os_x' => "filebeat-#{zip_info['version']}-darwin-x86_64.zip", 'debian' => "filebeat-#{zip_info['version']}-linux-x86_64.zip", 'windows' => "filebeat-#{zip_info['version']}-windows-x86_64.zip", 'default' => nil, ) # Create the filebeat directory if it does not exist create_filebeat_directory # Extract the Filebeat files to the filebeat directory cpe_remote_zip 'filebeat_zip' do zip_name zip_name zip_checksum zip_info['checksum'] folder_name 'filebeat' extract_location filebeat_dir end # If the filebeat executable is damaged update file properties file filebeat_bin do mode '0755' owner root_owner group node['root_group'] only_if { macos? } only_if { !unix_binary? } end # This will track the health history of the Filebeat service set_filebeat_health_history(unhealthy_count, reinstall_count) end def configure # Get info about filebeat config, rejecting unset values if filebeat_conf.empty? || filebeat_conf.nil? Chef::Log.warn('config is not populated, skipping configuration') return end # Place certificate if it does not exist cookbook_file certificate_path do source certificate owner root_owner group node['root_group'] mode '0644' unless windows? not_if { certificate.nil? } end setup_macos_service if macos? setup_debian_service if debian? prefix = node['cpe_launchd']['prefix'] || 'com.uber.chef' service_info = value_for_platform_family( 'mac_os_x' => { 'launchd' => "#{prefix}.filebeat" }, 'debian' => { 'systemd_unit' => 'filebeat.service' }, 'windows' => { 'windows_service' => 'Filebeat Service' }, 'default' => nil, ) service_type, service_name = service_info.first # Make a fake service to notify for macOS since we are using cpe_launchd if macos? launchd service_name do action :nothing only_if { ::File.exist?("/Library/LaunchDaemons/#{service_name}.plist") } subscribes :restart, 'cpe_remote_zip[filebeat_zip]' end end config = ::File.join(filebeat_dir, 'filebeat.chef.yml') file config do owner root_owner group node['root_group'] content YAML.dump(filebeat_conf) notifies :restart, "#{service_type}[#{service_name}]" end # Because windows services are annoying and start immediately, so this # must come after the config is placed setup_windows_service if windows? end def setup_windows_service exe_path = ::File.join(filebeat_dir, 'filebeat.exe') bin_path = "#{exe_path} -c #{filebeat_dir}\\filebeat.chef.yml" windows_service 'Filebeat Service' do action %i[create start] binary_path_name bin_path startup_type :automatic delayed_start true description 'Filebeat sends log files to Logstash or directly to Elasticsearch.' only_if { ::File.exists?(exe_path) } end end def setup_debian_service file filebeat_bin do mode '0755' end unit = { 'Unit' => { 'Description' => 'Filebeat sends log files to Logstash or directly to Elasticsearch.', 'Documentation' => ['https://www.elastic.co/products/beats/filebeat'], 'Wants' => 'network-online.target', 'After' => 'network-online.target', }, 'Service' => { 'Type' => 'simple', 'ExecStart' => "#{filebeat_dir}/filebeat" \ " -c #{filebeat_dir}/filebeat.chef.yml" \ ' -path.logs /var/log/filebeat', 'Restart' => 'always', }, 'Install' => { 'WantedBy' => 'multi-user.target', }, } systemd_unit 'filebeat.service' do content(unit) action %i[create start] subscribes :restart, 'cpe_remote_zip[filebeat_zip]' end end def setup_macos_service ld = { 'program_arguments' => [ "#{filebeat_dir}/filebeat", '-c', "#{filebeat_dir}/filebeat.chef.yml", ], 'disabled' => false, 'run_at_load' => true, 'keep_alive' => true, 'type' => 'daemon', } node.default['cpe_launchd']['filebeat'] = ld end def create_filebeat_directory directory filebeat_dir do recursive true owner root_owner group node['root_group'] end end def cleanup_windows windows_service 'Filebeat Service' do action %i[stop delete] end end def cleanup_debian systemd_unit 'filebeat.service.app' do action %i[stop delete] end end def cleanup # Delete the filebeat cache containing any prior versions of filebeat directory filebeat_cache do recursive true action :delete ignore_failure true if windows? end cleanup_debian if debian? cleanup_windows if windows? directory node['cpe_filebeat']['dir'] do action :delete recursive true ignore_failure true if windows? end end def filebeat_dir node['cpe_filebeat']['dir'] end def filebeat_running? if windows? status = powershell_out('(Get-Service "Filebeat Service" -ErrorAction SilentlyContinue).Status').stdout.to_s.chomp node.safe_nil_empty?(status) ? false : status&.include?('Running') elsif macos? node.daemon_running?('filebeat') else shell_out('systemctl status filebeat').run_command.stdout.to_s[/Active: active \(running\)/].nil? ? false : true end end def filebeat_bin ::File.join(filebeat_dir, node['cpe_filebeat']['bin']) end def filebeat_cache if macos? || debian? ::File.join(Chef::Config[:file_cache_path], 'remote_zip/opt/filebeat') elsif windows? ::File.join(Chef::Config[:file_cache_path], 'remote_zip\\C\\ProgramData\\filebeat') end end def filebeat_unhealthy_count get_filebeat_health_history['unhealthy_count'] end def filebeat_reinstall_count get_filebeat_health_history['reinstall_count'] end def get_filebeat_health_history json_path = ::File.join(filebeat_dir, 'cpe_filebeat.json') valid = false if ::File.exists?(json_path) json = node.parse_json(json_path) valid = %w(unhealthy_count reinstall_count).all? { |k| json.key?(k) && json[k].is_a?(Integer) } end valid ? json : { 'unhealthy_count' => 0, 'reinstall_count' => 0 } end def set_filebeat_health_history(unhealthy = 0, reinstalls = 0) json_path = ::File.join(filebeat_dir, 'cpe_filebeat.json') history = { 'unhealthy_count' => unhealthy, 'reinstall_count' => reinstalls, } file json_path do action :create content Chef::JSONCompat.to_json_pretty(history) end end def zip_info node['cpe_filebeat']['zip_info'][node['platform_family']] end def unhealthy_limit node['cpe_filebeat']['unhealthy_limit'] end def certificate node['cpe_filebeat']['certificate'] end def certificate_path ::File.join(node['cpe_filebeat']['dir'], certificate) if certificate end def filebeat_conf node['cpe_filebeat']['config'].to_h.reject { |_k, v| v.nil? } end end