diff --git a/bin/fluent-cap-ctl b/bin/fluent-cap-ctl new file mode 100755 index 0000000000..b71c2fcea3 --- /dev/null +++ b/bin/fluent-cap-ctl @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +# -*- coding: utf-8 -*- +here = File.dirname(__FILE__) +$LOAD_PATH << File.expand_path(File.join(here, '..', 'lib')) +require 'fluent/command/cap_ctl' + +Fluent::CapCtl.new.call \ No newline at end of file diff --git a/lib/fluent/command/cap_ctl.rb b/lib/fluent/command/cap_ctl.rb new file mode 100644 index 0000000000..6c6a817a5f --- /dev/null +++ b/lib/fluent/command/cap_ctl.rb @@ -0,0 +1,174 @@ +# +# Fluentd +# +# 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. +# + +require 'optparse' +require 'fluent/log' +require 'fluent/env' +require 'fluent/capability' + +module Fluent + class CapCtl + def prepare_option_parser + @op = OptionParser.new + + @op.on('--clear', "Clear Fluentd Ruby capability") {|s| + @opts[:clear_capabilities] = true + } + + @op.on('--add [CAPABILITITY1,CAPABILITY2, ...]', "Add capabilities into Fluentd Ruby") {|s| + @opts[:add_capabilities] = s + } + + @op.on('--drop [CAPABILITITY1,CAPABILITY2, ...]', "Drop capabilities from Fluentd Ruby") {|s| + @opts[:drop_capabilities] = s + } + + @op.on('--get', "Get capabilities for Fluentd Ruby") {|s| + @opts[:get_capabilities] = true + } + + @op.on('-f', '--file FILE', "Specify target file to add Linux capabilities") {|s| + @opts[:target_file] = s + } + end + + def usage(msg) + puts @op.to_s + puts "error: #{msg}" if msg + exit 1 + end + + def initialize(argv = ARGV) + @opts = {} + @argv = argv + + if Fluent.linux? + begin + require 'capng' + + @capng = CapNG.new + rescue LoadError + puts "Error: capng_c is not loaded. Please install it first." + exit 1 + end + else + puts "Error: This environment is not supported." + exit 2 + end + + prepare_option_parser + end + + def call + parse_options!(@argv) + + target_file = if !!@opts[:target_file] + @opts[:target_file] + else + File.readlink("/proc/self/exe") + end + + if @opts[:clear_capabilities] + clear_capabilities(@opts, target_file) + elsif @opts[:add_capabilities] + add_capabilities(@opts, target_file) + elsif @opts[:drop_capabilities] + drop_capabilities(@opts, target_file) + end + if @opts[:get_capabilities] + get_capabilities(@opts, target_file) + end + end + + def clear_capabilities(opts, target_file) + if !!opts[:clear_capabilities] + @capng.clear(:caps) + ret = @capng.apply_caps_file(target_file) + puts "Clear capabilities #{ret ? 'done' : 'fail'}." + end + end + + def add_capabilities(opts, target_file) + if add_caps = opts[:add_capabilities] + @capng.clear(:caps) + @capng.caps_file(target_file) + capabilities = add_caps.split(/\s*,\s*/) + check_capabilities(capabilities, get_valid_capabilities) + ret = @capng.update(:add, + CapNG::Type::EFFECTIVE | CapNG::Type::INHERITABLE | CapNG::Type::PERMITTED, + capabilities) + puts "Updating #{add_caps} #{ret ? 'done' : 'fail'}." + ret = @capng.apply_caps_file(target_file) + puts "Adding #{add_caps} #{ret ? 'done' : 'fail'}." + end + end + + def drop_capabilities(opts, target_file) + if drop_caps = opts[:drop_capabilities] + @capng.clear(:caps) + @capng.caps_file(target_file) + capabilities = drop_caps.split(/\s*,\s*/) + check_capabilities(capabilities, get_valid_capabilities) + ret = @capng.update(:drop, + CapNG::Type::EFFECTIVE | CapNG::Type::INHERITABLE | CapNG::Type::PERMITTED, + capabilities) + puts "Updating #{drop_caps} #{ret ? 'done' : 'fail'}." + @capng.apply_caps_file(target_file) + puts "Dropping #{drop_caps} #{ret ? 'done' : 'fail'}." + end + end + + def get_capabilities(opts, target_file) + if opts[:get_capabilities] + @capng.caps_file(target_file) + print = CapNG::Print.new + puts "Capabilities in '#{target_file}'," + puts "Effective: #{print.caps_text(:buffer, :effective)}" + puts "Inheritable: #{print.caps_text(:buffer, :inheritable)}" + puts "Permitted: #{print.caps_text(:buffer, :permitted)}" + end + end + + def get_valid_capabilities + capabilities = [] + cap = CapNG::Capability.new + cap.each do |_code, capability| + capabilities << capability + end + capabilities + end + + def check_capabilities(capabilities, valid_capabilities) + capabilities.each do |capability| + unless valid_capabilities.include?(capability) + raise ArgumentError, "'#{capability}' is not valid capability. Valid Capabilities are: #{valid_capabilities.join(", ")}" + end + end + end + + def parse_options!(argv) + begin + rest = @op.parse(argv) + + if rest.length != 0 + usage nil + end + rescue + usage $!.to_s + end + end + end +end diff --git a/test/command/test_cap_ctl.rb b/test/command/test_cap_ctl.rb new file mode 100644 index 0000000000..262b8bcc05 --- /dev/null +++ b/test/command/test_cap_ctl.rb @@ -0,0 +1,100 @@ +require_relative '../helper' + +require 'tempfile' +require 'fluent/command/cap_ctl' + +class TestFluentCapCtl < Test::Unit::TestCase + setup do + omit "This environment does not handle Linux capability" unless defined?(CapNG) + end + + sub_test_case "success" do + test "clear capability" do + logs = capture_stdout do + Fluent::CapCtl.new(["--clear"]).call + end + expression = /\AClear capabilities .*\n/m + assert_match expression, logs + end + + test "add capability" do + logs = capture_stdout do + Fluent::CapCtl.new(["--add", "dac_override"]).call + end + expression = /\AUpdating .* done.\nAdding .*\n/m + assert_match expression, logs + end + + test "drop capability" do + logs = capture_stdout do + Fluent::CapCtl.new(["--drop", "chown"]).call + end + expression = /\AUpdating .* done.\nDropping .*\n/m + assert_match expression, logs + end + + test "get capability" do + logs = capture_stdout do + Fluent::CapCtl.new(["--get"]).call + end + expression = /\ACapabilities in .*,\nEffective: .*\nInheritable: .*\nPermitted: .*/m + assert_match expression, logs + end + end + + sub_test_case "success with file" do + test "clear capability" do + logs = capture_stdout do + Tempfile.create("fluent-cap-") do |tempfile| + Fluent::CapCtl.new(["--clear-cap", "-f", tempfile.path]).call + end + end + expression = /\AClear capabilities .*\n/m + assert_match expression, logs + end + + test "add capability" do + logs = capture_stdout do + Tempfile.create("fluent-cap-") do |tempfile| + Fluent::CapCtl.new(["--add", "dac_override", "-f", tempfile.path]).call + end + end + expression = /\AUpdating .* done.\nAdding .*\n/m + assert_match expression, logs + end + + test "drop capability" do + logs = capture_stdout do + Tempfile.create("fluent-cap-") do |tempfile| + Fluent::CapCtl.new(["--drop", "chown", "-f", tempfile.path]).call + end + end + expression = /\AUpdating .* done.\nDropping .*\n/m + assert_match expression, logs + end + + test "get capability" do + logs = capture_stdout do + Tempfile.create("fluent-cap-") do |tempfile| + Fluent::CapCtl.new(["--get", "-f", tempfile.path]).call + end + end + expression = /\ACapabilities in .*,\nEffective: .*\nInheritable: .*\nPermitted: .*/m + assert_match expression, logs + end + end + + sub_test_case "invalid" do + test "add capability" do + assert_raise(ArgumentError) do + Fluent::CapCtl.new(["--add", "nonexitent"]).call + end + end + + test "drop capability" do + assert_raise(ArgumentError) do + Fluent::CapCtl.new(["--drop", "invalid"]).call + end + end + end +end