zool 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
1
+ require 'ruby-debug'
2
+ Debugger.start
3
+
4
+ module Zool
5
+ class KeyfileWriter
6
+ attr_accessor :out_directory
7
+
8
+ def self.keyname_for_key(key)
9
+ temp_name = key[/^\S*\s\S*\s([^@]+)\S*$/, 1]
10
+ if temp_name.nil?
11
+ logger.warn "key not parsable"
12
+ '1__not_parsable'
13
+ else
14
+ temp_name.gsub(/[^A-Z|^a-z|^0-9]/, '_').downcase
15
+ end
16
+ end
17
+
18
+ def initialize(out_directory = 'keys')
19
+ @out_directory = out_directory
20
+ end
21
+
22
+ def write_keys(keys)
23
+ keys.each do |key|
24
+ write key
25
+ end
26
+ end
27
+
28
+ def write(key, outname = nil)
29
+ key_name = outname || self.class.keyname_for_key(key)
30
+ key_count = Dir["#{out_directory}/#{key_name}*.pub"].size
31
+
32
+ key_name += "_#{key_count + 1}" if key_count > 0
33
+ key_path = "#{out_directory}/#{key_name}.pub"
34
+
35
+ File.open(key_path, 'w+') do |file|
36
+ file.puts key
37
+ end
38
+ end
39
+
40
+ def self.logger
41
+ DEFAULT_LOGGER
42
+ end
43
+
44
+ def logger
45
+ DEFAULT_LOGGER
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,116 @@
1
+ require 'net/sftp'
2
+ require 'net/scp'
3
+
4
+ module Zool
5
+ class Server
6
+ class ConnectionVerificationExecption < Exception; end
7
+ attr_reader :hostname
8
+ attr_accessor :keyfile_location
9
+
10
+ def initialize(hostname, options = {})
11
+ @options = {
12
+ :user => 'root',
13
+ :password => ''
14
+ }.update(options)
15
+ @hostname = hostname
16
+ @keyfile_location = default_keyfile_location
17
+ end
18
+
19
+ def fetch_keys
20
+ @keys = nil
21
+ @raw_authorized_keys = load_remote_file
22
+ end
23
+
24
+ def keys
25
+ @keys ||= begin
26
+ @raw_authorized_keys ||= fetch_keys
27
+ @raw_authorized_keys.split("\n").map {|key| key.strip}.uniq.reject {|key| key == ""}
28
+ end
29
+ end
30
+
31
+ def keys=(new_keys)
32
+ @keys = new_keys
33
+ end
34
+
35
+ def dump_keyfiles
36
+ key_writer = KeyfileWriter.new
37
+ key_writer.write_keys keys
38
+ end
39
+
40
+ def create_backup
41
+ begin
42
+ backup = load_remote_file
43
+ backup_filename = "#{@keyfile_location}_#{Time.now.to_i}"
44
+ Net::SCP.upload!(@hostname, @options[:user], StringIO.new(backup), backup_filename, :ssh => {:password => @options[:password]})
45
+ backup_filename
46
+ rescue Net::SCP::Error => e
47
+ logger.fatal "Error during backup of authorized keys file: #{e.message}"
48
+ raise
49
+ end
50
+ end
51
+
52
+ def upload_keys
53
+ remote_backup_file = create_backup
54
+ begin
55
+ backup_channel = Net::SSH.start(@hostname, @options[:user], :password => @options[:password])
56
+ main_channel = Net::SSH.start(@hostname, @options[:user], :password => @options[:password])
57
+ main_channel.scp.upload!(StringIO.new(keys.join("\n")), @keyfile_location)
58
+ main_channel.close
59
+ begin
60
+ logger.info "Trying to connect to #{@hostname} to see if I still have access"
61
+ Net::SSH.start(@hostname, @options[:user], :password => '')
62
+ logger.info "Backup channel connection succeeded. Assuming everything went fine!"
63
+ rescue Net::SSH::AuthenticationFailed => e
64
+ if !@rolled_back
65
+ logger.warn "!!!!!! Could not login to server after upload operation! Rolling back !!!!!!"
66
+ backup_channel.exec "mv #{remote_backup_file} #{@keyfile_location}"
67
+ backup_channel.loop
68
+ @rolled_back = true
69
+ retry
70
+ else
71
+ logger.fatal "Tried to role back... didnt work... giving up... sorry :("
72
+ raise e
73
+ end
74
+ end
75
+ ensure
76
+ main_channel.close unless main_channel.closed?
77
+ backup_channel.close unless backup_channel.closed?
78
+ end
79
+ raise ConnectionVerificationExecption.new("Error after uploading the keyfile to #{@hostname}") if @rolled_back
80
+ end
81
+
82
+ def to_s
83
+ "<Zool::Server #{hostname}>"
84
+ end
85
+
86
+ def user
87
+ @options[:user]
88
+ end
89
+
90
+ private
91
+ def load_remote_file
92
+ downloaded_file = StringIO.new
93
+ begin
94
+ Timeout::timeout(2) do
95
+ logger.info "Fetching key from #{@hostname}"
96
+ Net::SCP.download!(@hostname, @options[:user], @keyfile_location, downloaded_file, :ssh => {:password => @options[:password]})
97
+ end
98
+ rescue Net::SCP::Error
99
+ logger.warn "Warning! Empty keyfile" # logging? later... :P
100
+ rescue Net::SSH::AuthenticationFailed
101
+ logger.warn "No access to Server #{@hostname}"
102
+ rescue Errno::ETIMEDOUT, Timeout::Error
103
+ logger.warn "Access to server #{@hostname} timed out"
104
+ end
105
+ downloaded_file.string
106
+ end
107
+
108
+ def default_keyfile_location
109
+ '~/.ssh/authorized_keys'
110
+ end
111
+
112
+ def logger
113
+ DEFAULT_LOGGER
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,74 @@
1
+ module Zool
2
+ class ServerPool < Array
3
+ IP_FORMAT = /^(?:25[0-5]|(?:2[0-4]|1\d|[1-9])?\d)(?:\.(?:25[0-5]|(?:2[0-4]|1\d|[1-9])?\d)){3}$|^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+(?:[a-z]{2}|com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|museum)$/
4
+
5
+ def self.from_hostfile(hostsfile, options = {})
6
+ hosts = hostsfile.to_a.map { |host| host.split[0] }
7
+ hosts.uniq!
8
+ invalid_hosts = %w(127.0.0.1 255.255.255.255)
9
+ hosts.reject! { |host| host !~ IP_FORMAT }
10
+ hosts.reject! { |host| invalid_hosts.include?(host) }
11
+ pool = self.new
12
+
13
+ hosts.each do |host|
14
+ # puts host
15
+ server = Server.new(host, options)
16
+ # puts server.hostname
17
+ pool << server
18
+ end
19
+ pool
20
+ end
21
+
22
+ alias servers entries
23
+
24
+ def keys
25
+ @keys_proxy ||= KeysProxy.new(self)
26
+ end
27
+
28
+ def fetch_keys
29
+ call_for_pool(:fetch_keys)
30
+ @keys_proxy = nil
31
+ end
32
+
33
+ def upload_keys
34
+ call_for_pool(:upload_keys)
35
+ end
36
+
37
+ def <<(object)
38
+ raise TypeError.new 'Invalid Argument' unless object.instance_of?(Server)
39
+ super
40
+ end
41
+ alias add <<
42
+
43
+ def dump_keyfiles
44
+ writer = KeyfileWriter.new
45
+ keys.each do |key|
46
+ writer.write(key)
47
+ end
48
+ end
49
+
50
+ def inspect
51
+ "#<Zool::ServerPool @servers=[#{servers.join(', ')}]>"
52
+ end
53
+
54
+ private
55
+ def call_for_pool(method)
56
+ servers.map do |server|
57
+ server.send(method)
58
+ end.flatten.uniq
59
+ end
60
+ end
61
+
62
+ class KeysProxy < Array
63
+ def initialize(pool)
64
+ @pool = pool
65
+ super @pool.send(:call_for_pool, :keys)
66
+ end
67
+
68
+ def <<(key)
69
+ @pool.each do |server|
70
+ server.keys << key
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,55 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe PyConfigParser do
4
+ include PyConfigParserHelper
5
+
6
+ before :all do
7
+ @config_string = <<-CONF
8
+ [without_whitespace]
9
+ key:value
10
+ multiple_values=foo, bar, baz
11
+ sticky_values=blim,blam,blum
12
+
13
+ [with whitespace]
14
+ key2: value2
15
+ key3 : value3
16
+ multiple_values = foo, bar, baz
17
+ CONF
18
+ end
19
+
20
+ context "parsing a config" do
21
+ context "when invalid" do
22
+ it "should compile to nil" do
23
+ parse('strange stuff').should be_nil
24
+ end
25
+ end
26
+
27
+ context "when valid" do
28
+ it "should compile to a hash" do
29
+ parse("").build.should == {}
30
+ end
31
+
32
+ context "with sections" do
33
+ before :all do
34
+ @sections = parse(@config_string).build
35
+ end
36
+
37
+ it "should recognize sections" do
38
+ @sections["with whitespace"].should be_a(Hash)
39
+ @sections["without_whitespace"].should be_a(Hash)
40
+ end
41
+
42
+ it "should map key/value pairs separated by colons" do
43
+ @sections["with whitespace"]['key2'].should == 'value2'
44
+ @sections["without_whitespace"]['key'].should == 'value'
45
+ end
46
+
47
+ it "should map key/value pairs separated by an equal sign and devided by commas" do
48
+ @sections["with whitespace"]['multiple_values'].should == %w(foo bar baz)
49
+ @sections["without_whitespace"]['multiple_values'].should == %w(foo bar baz)
50
+ @sections["without_whitespace"]['sticky_values'].should == %w(blim blam blum)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,51 @@
1
+ require 'lib/zool'
2
+ require 'spec'
3
+ require 'fakefs'
4
+
5
+ module Net::SSH
6
+ def self.start
7
+ raise("unexpected call to SCP in test environment, see #{__FILE__}:#{__LINE__}")
8
+ end
9
+ end
10
+
11
+ class Net::SCP
12
+ class << self
13
+ def disallow_file_operation(a, b, c, d)
14
+ raise("unexpected call to SCP in test environment, see #{__FILE__}:#{__LINE__}")
15
+ end
16
+ alias upload! disallow_file_operation
17
+ alias upload disallow_file_operation
18
+ alias download! disallow_file_operation
19
+ alias download disallow_file_operation
20
+ end
21
+ end
22
+
23
+ class StringbufferMatcher
24
+ def initialize(expected)
25
+ @expected = expected
26
+ end
27
+
28
+ def ==(actual)
29
+ actual.string == @expected
30
+ end
31
+ end
32
+
33
+ def stringbuffer_with(content)
34
+ StringbufferMatcher.new(content)
35
+ end
36
+
37
+ def key_fixtures
38
+ @key_fixtures ||= {
39
+ :pascal => 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0YllcgPG3lFhW1R6g1zHIOOZhW8fl5MsBxNQYFJnkNUvwcqcH1CLFr5ybdEwgOfjqT2YDLt9qY/cn4Wa1xLvPEph7nkdx6NW7VzcxcIiakgtEEGI+F6K0ux/3bXPIEIDZcaAmlfcnw+OkoqyQR1PWppT/74mc+6+GkCoewqgIhxuajPmjLK9eAtDjNGnwsN1t0+gZkc9HNWOxWGGGNyfoSgRPlIzr4cTDnfuRPzxZDKJXLd75RJIAhr2PQwQTrdhPurCG2+48AHul/D1mg+BzWeaXifl3pd8on/Buo97A6iLM+jcx1VjDzhVil6esS/+30XSEUANh974PlIECZnIFw== pascal.friederich@nb-pfriederich.local',
40
+ :pascal_private => 'ssh-rsa fajfoijewaofjewofjaweofnlwkaenfakdjngkaldsjgndkjsnflkjdsfnjsadfkjlasdfnasnfamlfaj9efj09waj09j3f029j3029j3f2j3f2uhfuhgkashgkljdsagkjeahh3iuf2h398fh329f8h32f983h2fh3n29unfup3fhapw39fhpa93fha9w3fh983bf2fubkbawekjbfabf,ebfa,menbfiufbawefuwefiweafiubewafibefbiuwbgiu4gbiueraghaeiuhfsdiofuhasdifuhaw9e8fh9f8h238fh239fhpawh3fp9ahwpfhawp39fhp490f8hawf8ha9ef8hawp9haugbs== pascal.friederich@private',
41
+ :pascal_laptop => 'ssh-rsa fajfoijewaofjewofjaweofnlwkaenfakdjngkaldsjgndkjsnflkjdsfnjsadfkjlasdfnasnfamlfaj9efj09waj09j3f029j3029j3f2j3f2uhfuhgkashgkljdsagkjeahh3iuf2h398fh329f8h32f983h2fh3n29unfup3fhapw39fhpa93fha9w3fh983bf2fubkbawekjbfabf,ebfa,menbfiufbawefuwefiweafiubewafibefbiuwbgiu4gbiueraghaeiuhfsdiofuhasdifuhaw9e8fh9f8h238fh239fhpawh3fp9ahwpfhawp39fhp490f8hawf8ha9ef8hawp9haugbs== pascal.friederich@laptop',
42
+ :bob => 'ssh-rsa LKASJFLASJFLKASJFLAKSFNALSKVNasdfj0fj0Jf0j09Jf90jw0fj9w0fjJFIWJLFNlnfLNlknflewknaflefawelfhweaf8932y98ry239f832hfh3fh3fiuhkljdsfkjasbdfwhefhewkjfhenkhfnkejfhhdskfjhdskfjhsdkjfhskdjfhalksdjhfkjdfhalsdkfhklasdfhdskfhjdkfjhqufheufwhewiuf38h9fh3298fh2938fh9283hf9823hf9823hfk2j3hfkj23fkj23fkjh23kjfhljhaasdfsadfsadf90usdf90saudf09jas0f9jas0fj09wjf0932hf0923hf0h320f9h230f9h329h== bob.schneider@nb-pfriederich.local',
43
+ :upcase => 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0YllcgPG3lFhW1R6g1zHIOOZhW8fl5MsBxNQYFJnkNUvwcqcH1CLFr5ybdEwgOfjqT2YDLt9qY/cn4Wa1xLvPEph7nkdx6NW7VzcxcIiakgtEEGI+F6K0ux/3bXPIEIDZcaAmlfcnw+OkoqyQR1PWppT/74mc+6+GkCoewqgIhxuajPmjLK9eAtDjNGnwsN1t0+gZkc9HNWOxWGGGNyfoSgRPlIzr4cTDnfuRPzxZDKJXLd75RJIAhr2PQwQTrdhPurCG2+48AHul/D1mg+BzWeaXifl3pd8on/Buo97A6iLM+jcx1VjDzhVil6esS/+30XSEUANh974PlIECZnIFw== upcase.VaN@nb-UPCASE.StuFF'
44
+ }
45
+ end
46
+
47
+ module PyConfigParserHelper
48
+ def parse(string)
49
+ PyConfigParser.new.parse(string)
50
+ end
51
+ end
data/spec/zool.rb ADDED
@@ -0,0 +1,4 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Zool do
4
+ end
@@ -0,0 +1,170 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ module Zool
4
+ describe Configuration do
5
+ before :all do
6
+ @keyfile_stub_data = {
7
+ 'peter' => 'ssh-dsa adfsdfafef00if0i23f== peter@localhost',
8
+ 'paul' => 'ssh-dsa adfsdfafef00if0i23f== paul@horst',
9
+ 'system' => 'ssh-dsa adfsdfafef00if0i23f== system@admins',
10
+ 'log' => 'ssh-dsa adfsdfafef00if0i23f== log@admins',
11
+ }
12
+ end
13
+
14
+ context "building a configuration file from a serverpool" do
15
+ before :each do
16
+ server1_keys = [@keyfile_stub_data['peter'], @keyfile_stub_data['paul']]
17
+ server1 = stub(:hostname => 'server1', :keys => server1_keys)
18
+ server2_keys = [@keyfile_stub_data['system'], @keyfile_stub_data['log']]
19
+ server2 = stub(:hostname => 'server2', :keys => server2_keys)
20
+ @pool = ServerPool.new([server1, server2])
21
+ end
22
+
23
+ it "should write a server entry for every server in the pool and add it's keys" do
24
+ Configuration.build(@pool).should == <<-EXPECTED_CONF
25
+ [server server1]
26
+ keys = peter, paul
27
+
28
+ [server server2]
29
+ keys = system, log
30
+ EXPECTED_CONF
31
+ end
32
+ end
33
+
34
+ context ".parse" do
35
+ context "an invalid configuration" do
36
+ it "should raise an exception" do
37
+ lambda { Configuration.parse('asdf') }.should raise_error(Zool::Configuration::ParseError)
38
+ end
39
+
40
+ context "pointing the reason why the configuration is invalid" do
41
+ it "should complain about missing groups that are referenced in roles" do
42
+ conf = <<-CONF
43
+ [role app]
44
+ servers = 12.3.4.5
45
+ keys = &snafu
46
+ CONF
47
+ lambda { Configuration.parse(conf) }.should raise_error(Zool::Configuration::ParseError, /missing referenced group 'snafu'/)
48
+ end
49
+
50
+ it "should complain about missing keys" do
51
+ conf = <<-CONF
52
+ [role app]
53
+ servers = 12.3.4.5
54
+ keys = i_am_not_there
55
+ CONF
56
+ lambda { Configuration.parse(conf) }.should raise_error(Zool::Configuration::ParseError, /missing ssh key 'i_am_not_there'/)
57
+ end
58
+ end
59
+ end
60
+
61
+ context "a valid configuration" do
62
+ it "should return a configuration object with the parsed configuration hash" do
63
+ conf = <<-CONF
64
+ [role app]
65
+ servers = 13.9.6.1, 13.9.6.2
66
+ keys = &qa, peter
67
+
68
+ [group qa]
69
+ members = david
70
+ password : cleartext10)9292@*=-.?
71
+ CONF
72
+ writer = KeyfileWriter.new
73
+ FileUtils.rm_r(writer.out_directory)
74
+
75
+ writer.write 'davids key', 'david'
76
+ writer.write 'peters key', 'peter'
77
+ configuration = Configuration.parse(conf)
78
+ configuration.should be_a(Configuration)
79
+ end
80
+ end
81
+ end
82
+
83
+ context "instanciating a configuration" do
84
+ before :all do
85
+ writer = KeyfileWriter.new
86
+ FileUtils.rm_r(writer.out_directory)
87
+
88
+ @keyfile_stub_data.each do |key, value|
89
+ writer.write value, key
90
+ end
91
+
92
+ @conf_hash = {
93
+ "role app" => {
94
+ 'servers' => ['preview_server', 'production_server', 'edge_server'],
95
+ 'keys' => ['&qa'],
96
+ 'password' => "123456"
97
+ },
98
+ "role cron servers" => {
99
+ 'servers' => ['crn1', 'crn2', 'edge_server'],
100
+ 'keys' => ['system', 'log']
101
+ },
102
+ "group qa" => {
103
+ 'members' => ['peter', 'paul']
104
+ },
105
+ "server 13.9.6.1" => {
106
+ 'keys' => ['system'],
107
+ 'password' => "123456"
108
+ },
109
+ "server 13.9.6.2" => {
110
+ 'keys' => ['peter'],
111
+ 'user' => "admin"
112
+ }
113
+
114
+ }
115
+ @configuration = Configuration.new(@conf_hash)
116
+ end
117
+
118
+ it "should create a server for every server section" do
119
+ @configuration.servers['13.9.6.1'].keys.should include(@keyfile_stub_data['system'])
120
+ end
121
+
122
+ it "should create a serverpool for every role" do
123
+ @configuration.roles['app'].should be_a(ServerPool)
124
+ @configuration.roles['cron servers'].should be_a(ServerPool)
125
+ end
126
+
127
+ it "should read the keys from the key files" do
128
+ @configuration.keys.should have(4).keys
129
+ end
130
+
131
+ it "should add a groups keys to the serverpool" do
132
+ @configuration.servers['preview_server'].keys.should include(@keyfile_stub_data['peter'])
133
+ end
134
+
135
+ it "should have only one server object per hostname shared between groups" do
136
+ edge_servers_keys = @configuration.servers['edge_server'].keys
137
+ edge_servers_keys.should include(@keyfile_stub_data['system'])
138
+ edge_servers_keys.should include(@keyfile_stub_data['paul'])
139
+ edge_servers_keys.should include(@keyfile_stub_data['peter'])
140
+ edge_servers_keys.should include(@keyfile_stub_data['log'])
141
+ edge_servers_keys.should have(4).keys
142
+ end
143
+
144
+ context "with a configured user and/or a password" do
145
+ it "should use the user for servers" do
146
+ @configuration.servers['13.9.6.1'].send(:instance_variable_get, :@options)[:password].should == '123456'
147
+ end
148
+
149
+ it "should use the password for servers" do
150
+ @configuration.servers['13.9.6.2'].user.should == 'admin'
151
+ end
152
+ end
153
+
154
+ context "calling the upload_keys method" do
155
+ it "should upload the keys to every server in the configuration" do
156
+ @configuration.servers.values.each do |server|
157
+ server.should_receive(:upload_keys).and_return(nil)
158
+ end
159
+ @configuration.upload_keys
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ Spec::Matchers.define :have_role do |role|
167
+ match do |configuration|
168
+ !configuration.roles[role].nil?
169
+ end
170
+ end