From 4a357637d1fcea6d104265f50ad5a0d86edb07ef Mon Sep 17 00:00:00 2001 From: Mike Heffner Date: Fri, 30 Sep 2011 10:32:30 -0400 Subject: [PATCH] Add a simple configuration system that uses sysctl(8) variable format. --- lib/twke.rb | 7 ++- lib/twke/conf.rb | 122 ++++++++++++++++++++++++++++++++++++++++++++++ test/helper.rb | 1 + test/test_conf.rb | 51 +++++++++++++++++++ test/test_twke.rb | 7 --- 5 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 lib/twke/conf.rb create mode 100644 test/test_conf.rb delete mode 100644 test/test_twke.rb diff --git a/lib/twke.rb b/lib/twke.rb index 51624fc..2293535 100644 --- a/lib/twke.rb +++ b/lib/twke.rb @@ -1,8 +1,11 @@ module Twke require 'scamp' - require 'twke/routes.rb' - require 'twke/plugin.rb' + $:.unshift File.join(File.dirname(__FILE__), 'twke') + + require 'conf' + require 'routes' + require 'plugin' def self.version File.read(File.join(File.dirname(__FILE__), '../VERSION')).chomp diff --git a/lib/twke/conf.rb b/lib/twke/conf.rb new file mode 100644 index 0000000..28d28a2 --- /dev/null +++ b/lib/twke/conf.rb @@ -0,0 +1,122 @@ +require 'yaml' + +# +# Simple configuration system. +# +# Variable names are dot-separated, similar to how sysctl(8) names +# work. +# +# For example, a 'heroku' module could prefix all variables with +# "heroku.": +# +# heroku.sites.metrics-prod.giturl +# heroku.sites.metrics-prod.token +# +# The conf system can also lookup all the sub-variables rooted at a +# single prefix. So for example, assume the following variables are +# also set: +# +# heroku.sites.metrics-stg.giturl +# heroku.sites.metrics-stg.token +# +# A list command on 'heroku.sites' would return: +# ['metrics-prod', 'metrics-stg'] +# +###### + +module Twke + module Conf + class << self + attr_accessor :conf + + def conf + @conf ||= {} + end + + # Everytime a value is modified the config DB is written + def set(varname, value) + if varname =~ /^\./ || varname =~ /\.$/ || varname.length == 0 + raise "Invalid variable name" + end + + conf[varname.to_s] = value + + save + end + + def get(varname) + conf[varname.to_s] + end + + def exists?(varname) + conf.has_key?(varname) + end + + # + # This will return a list of the unique commands that begin with + # the varpfx prefix assumed to be a sub-command prefix. + # + # For example, assume the following variables are set: + # + # net.tcp.foobar => 1 + # net.tcp.barbar => 2 + # + # So: + # list('net.tcp') would return: ["barbar", "foobar"] + # list('net') would return: ["tcp"] + # + # list('net.tc') would return: [] + # Because there are no sub-commands with 'net.tc.' as + # their prefix + # + def list(varpfx) + # Strip leading/trailing periods + varpfx = varpfx[1, varpfx.length] if varpfx =~ /^\./ + varpfx = varpfx[0, varpfx.length - 1] if varpfx =~ /\.$/ + + # XXX: Really need a tree structure to do this efficiently + conf.keys.inject([]) do |ar, k| + if k =~ /^#{varpfx}\./ + ar << k.gsub(/^#{varpfx}\./, '').split(".")[0] + end + ar + end.sort.uniq + end + + def load + begin + yml = YAML.load_file(config_file) + @conf = yml + rescue Errno::ENOENT => err + @conf = {} + rescue => err + raise "Unknown error reading config file: #{err.message}" + end + end + + # + # Files are saved atomically. + # + def save + unless File.exist?(config_dir) + FileUtils.mkdir_p(config_dir, :mode => 0700) + end + + tmpfile = File.join(config_dir, "tmpconfig_#{rand 999999}") + File.open(tmpfile, "w") do |f| + YAML.dump(conf, f ) + end + + FileUtils.mv(tmpfile, config_file) + end + + def config_dir + File.join(ENV['HOME'], '.twke') + end + + def config_file + File.join(config_dir, 'config.yml') + end + end + end +end diff --git a/test/helper.rb b/test/helper.rb index 16efae8..fa9d20c 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -15,4 +15,5 @@ require 'twke' class Test::Unit::TestCase + include Twke end diff --git a/test/test_conf.rb b/test/test_conf.rb new file mode 100644 index 0000000..7714fa1 --- /dev/null +++ b/test/test_conf.rb @@ -0,0 +1,51 @@ +require 'helper' + +class TestConf < Test::Unit::TestCase + def setup + FileUtils.rm_rf(Conf::config_file) + end + + def teardown + FileUtils.rm_rf(Conf::config_file) + end + + def validate_settings + assert_equal(1, Conf::get('net.tcp.foobar')) + assert_equal('blah', Conf::get('net.tcp.barbar')) + assert(Conf::exists?('net.tcp.foobar')) + assert(Conf::exists?('net.tcp.barbar')) + assert(!Conf::exists?('net.tcp')) + + assert_equal(['barbar', 'foobar'].sort, Conf::list('net.tcp')) + assert_equal(['tcp'].sort, Conf::list('net')) + + assert_equal([], Conf::list('')) + assert_equal([], Conf::list('net.tc')) + end + + def test_config + Conf::set('net.tcp.foobar', 1) + Conf::set('net.tcp.barbar', "blah") + + validate_settings + + ['.sys.blah', 'sys.blah.', ''].each do |bad| + begin + Conf::set(bad, rand(5)) + rescue + # Should throw exception + else + assert(false, "Did not throw exception setting: #{bad}") + end + end + + assert(File.exist?(Conf::config_file), "Conf file doesn't exist") + + # Now reload + # + Conf::load + + # retest after reload + validate_settings + end +end diff --git a/test/test_twke.rb b/test/test_twke.rb deleted file mode 100644 index 4a21e01..0000000 --- a/test/test_twke.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'helper' - -class TestTwke < Test::Unit::TestCase - should "probably rename this file and start testing for real" do - flunk "hey buddy, you should probably rename this file and start testing for real" - end -end