zool 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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