knife-server 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.
- data/.gitignore +21 -0
- data/.rspec +1 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +8 -0
- data/LICENSE +201 -0
- data/README.md +267 -0
- data/Rakefile +16 -0
- data/knife-server.gemspec +25 -0
- data/lib/chef/knife/bootstrap/chef-server-debian.erb +127 -0
- data/lib/chef/knife/server_bootstrap_ec2.rb +235 -0
- data/lib/knife-server.rb +6 -0
- data/lib/knife/server/credentials.rb +64 -0
- data/lib/knife/server/ec2_security_group.rb +89 -0
- data/lib/knife/server/ssh.rb +34 -0
- data/lib/knife/server/version.rb +5 -0
- data/spec/chef/knife/server_bootstrap_ec2_spec.rb +259 -0
- data/spec/knife/server/credientials_spec.rb +104 -0
- data/spec/knife/server/ec2_security_group_spec.rb +164 -0
- data/spec/knife/server/ssh_spec.rb +62 -0
- metadata +135 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
module Knife
|
2
|
+
module Server
|
3
|
+
class Ec2SecurityGroup
|
4
|
+
def initialize(connection, ui)
|
5
|
+
@aws = connection
|
6
|
+
@ui = ui
|
7
|
+
end
|
8
|
+
|
9
|
+
def configure_chef_server_group(group_name, options = {})
|
10
|
+
group = find_or_create(group_name, options)
|
11
|
+
|
12
|
+
ip_permissions.each do |p|
|
13
|
+
if permission_exists?(group, p)
|
14
|
+
@ui.msg "Inbound security group rule " +
|
15
|
+
"#{p[:proto]}(#{p[:from]} -> #{p[:to]}) exists"
|
16
|
+
else
|
17
|
+
@ui.msg "Creating inbound security group rule for " +
|
18
|
+
"#{p[:proto]}(#{p[:from]} -> #{p[:to]})"
|
19
|
+
options = permission_options(group, p)
|
20
|
+
@aws.authorize_security_group_ingress(group.name, options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_or_create(name, options = {})
|
26
|
+
group = @aws.security_groups.find { |g| g.name == name }
|
27
|
+
|
28
|
+
if group.nil?
|
29
|
+
@ui.msg "Creating EC2 security group '#{name}'"
|
30
|
+
@aws.create_security_group(name, options[:description])
|
31
|
+
group = @aws.security_groups.find { |g| g.name == name }
|
32
|
+
else
|
33
|
+
@ui.msg "EC2 security group '#{name}' exists"
|
34
|
+
end
|
35
|
+
|
36
|
+
group
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def ip_permissions
|
42
|
+
[ {:proto => 'icmp', :from => -1, :to => -1},
|
43
|
+
{:proto => 'tcp', :from => 0, :to => 65535},
|
44
|
+
{:proto => 'udp', :from => 0, :to => 65535},
|
45
|
+
{:proto => 'tcp', :from => 22, :to => 22, :cidr => '0.0.0.0/0'},
|
46
|
+
{:proto => 'tcp', :from => 443, :to => 443, :cidr => '0.0.0.0/0'},
|
47
|
+
{:proto => 'tcp', :from => 444, :to => 444, :cidr => '0.0.0.0/0'},
|
48
|
+
].freeze
|
49
|
+
end
|
50
|
+
|
51
|
+
def permission_exists?(group, perm)
|
52
|
+
group.ip_permissions.find do |p|
|
53
|
+
p['ipProtocol'] == perm[:proto] &&
|
54
|
+
p['fromPort'] == perm[:from] &&
|
55
|
+
p['toPort'] == perm[:to]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def permission_options(group, opts)
|
60
|
+
options = {
|
61
|
+
'IpPermissions' => [
|
62
|
+
{
|
63
|
+
'IpProtocol' => opts[:proto],
|
64
|
+
'FromPort' => opts[:from],
|
65
|
+
'ToPort' => opts[:to]
|
66
|
+
}
|
67
|
+
]
|
68
|
+
}
|
69
|
+
|
70
|
+
if opts[:cidr]
|
71
|
+
options['IpPermissions'].first['IpRanges'] = [
|
72
|
+
{
|
73
|
+
'CidrIp' => opts[:cidr]
|
74
|
+
}
|
75
|
+
]
|
76
|
+
else
|
77
|
+
options['IpPermissions'].first['Groups'] = [
|
78
|
+
{
|
79
|
+
'GroupName' => group.name,
|
80
|
+
'UserId' => group.owner_id
|
81
|
+
}
|
82
|
+
]
|
83
|
+
end
|
84
|
+
|
85
|
+
options
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
|
3
|
+
module Knife
|
4
|
+
module Server
|
5
|
+
class SSH
|
6
|
+
DEFAULT_OPTIONS = { :user => "root", :port => "22" }.freeze
|
7
|
+
|
8
|
+
def initialize(params)
|
9
|
+
options = DEFAULT_OPTIONS.merge(params)
|
10
|
+
|
11
|
+
@host = options.delete(:host)
|
12
|
+
@user = options.delete(:user)
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def exec!(cmd)
|
17
|
+
if @user == "root"
|
18
|
+
full_cmd = cmd
|
19
|
+
else
|
20
|
+
full_cmd = [
|
21
|
+
%[sudo USER=root HOME="$(getent passwd root | cut -d : -f 6)"],
|
22
|
+
%[bash -c '#{cmd}']
|
23
|
+
].join(" ")
|
24
|
+
end
|
25
|
+
|
26
|
+
result = ""
|
27
|
+
Net::SSH.start(@host, @user, @options) do |ssh|
|
28
|
+
result = ssh.exec!(full_cmd)
|
29
|
+
end
|
30
|
+
result
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,259 @@
|
|
1
|
+
require 'chef/knife/server_bootstrap_ec2'
|
2
|
+
require 'chef/knife/ec2_server_create'
|
3
|
+
require 'fog'
|
4
|
+
require 'net/ssh'
|
5
|
+
require 'fakefs/spec_helpers'
|
6
|
+
|
7
|
+
describe Chef::Knife::ServerBootstrapEc2 do
|
8
|
+
include FakeFS::SpecHelpers
|
9
|
+
|
10
|
+
before do
|
11
|
+
Chef::Log.logger = Logger.new(StringIO.new)
|
12
|
+
@knife = Chef::Knife::ServerBootstrapEc2.new
|
13
|
+
@stdout = StringIO.new
|
14
|
+
@knife.ui.stub!(:stdout).and_return(@stdout)
|
15
|
+
@stderr = StringIO.new
|
16
|
+
@knife.ui.stub!(:stderr).and_return(@stderr)
|
17
|
+
@knife.config[:chef_node_name] = "yakky"
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:connection) { mock(Fog::Compute::AWS) }
|
21
|
+
|
22
|
+
describe "#ec2_bootstrap" do
|
23
|
+
before do
|
24
|
+
@knife.config[:chef_node_name] = "shave.yak"
|
25
|
+
@knife.config[:ssh_user] = "jdoe"
|
26
|
+
@knife.config[:ssh_port] = "2222"
|
27
|
+
@knife.config[:identity_file] = "~/.ssh/mykey_dsa"
|
28
|
+
@knife.config[:security_groups] = %w{x y z}
|
29
|
+
@knife.config[:tags] = %w{tag1=val1 tag2=val2}
|
30
|
+
@knife.config[:distro] = "distro-praha"
|
31
|
+
@knife.config[:ebs_size] = "42"
|
32
|
+
@knife.config[:webui_password] = "daweb"
|
33
|
+
@knife.config[:amqp_password] = "queueitup"
|
34
|
+
|
35
|
+
ENV['_SPEC_WEBUI_PASSWORD'] = ENV['WEBUI_PASSWORD']
|
36
|
+
ENV['_SPEC_AMQP_PASSWORD'] = ENV['AMQP_PASSWORD']
|
37
|
+
end
|
38
|
+
|
39
|
+
after do
|
40
|
+
ENV['WEBUI_PASSWORD'] = ENV.delete('_SPEC_WEBUI_PASSWORD')
|
41
|
+
ENV['AMQP_PASSWORD'] = ENV.delete('_SPEC_AMQP_PASSWORD')
|
42
|
+
end
|
43
|
+
|
44
|
+
let(:bootstrap) { @knife.ec2_bootstrap }
|
45
|
+
|
46
|
+
it "returns an Ec2ServerCreate instance" do
|
47
|
+
bootstrap.should be_a(Chef::Knife::Ec2ServerCreate)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "configs the bootstrap's chef_node_name" do
|
51
|
+
bootstrap.config[:chef_node_name].should eq("shave.yak")
|
52
|
+
end
|
53
|
+
|
54
|
+
it "configs the bootstrap's ssh_user" do
|
55
|
+
bootstrap.config[:ssh_user].should eq("jdoe")
|
56
|
+
end
|
57
|
+
|
58
|
+
it "configs the bootstrap's ssh_port" do
|
59
|
+
bootstrap.config[:ssh_port].should eq("2222")
|
60
|
+
end
|
61
|
+
|
62
|
+
it "configs the bootstrap's identity_file" do
|
63
|
+
bootstrap.config[:identity_file].should eq("~/.ssh/mykey_dsa")
|
64
|
+
end
|
65
|
+
|
66
|
+
it "configs the bootstrap's security_groups" do
|
67
|
+
bootstrap.config[:security_groups].should eq(["x", "y", "z"])
|
68
|
+
end
|
69
|
+
|
70
|
+
it "configs the bootstrap's ebs_size" do
|
71
|
+
bootstrap.config[:ebs_size].should eq("42")
|
72
|
+
end
|
73
|
+
|
74
|
+
it "configs the bootstrap's tags" do
|
75
|
+
bootstrap.config[:tags].should include("tag1=val1")
|
76
|
+
bootstrap.config[:tags].should include("tag2=val2")
|
77
|
+
end
|
78
|
+
|
79
|
+
it "adds Role=chef_server to the bootstrap's tags" do
|
80
|
+
bootstrap.config[:tags].should include("Role=chef_server")
|
81
|
+
end
|
82
|
+
|
83
|
+
it "configs the bootstrap's distro" do
|
84
|
+
bootstrap.config[:distro].should eq("distro-praha")
|
85
|
+
end
|
86
|
+
|
87
|
+
it "configs the bootstrap's distro to chef-server-debian by default" do
|
88
|
+
@knife.config.delete(:distro)
|
89
|
+
|
90
|
+
bootstrap.config[:distro].should eq("chef-server-debian")
|
91
|
+
end
|
92
|
+
|
93
|
+
it "configs the bootstrap's distro value driven off platform value" do
|
94
|
+
@knife.config.delete(:distro)
|
95
|
+
@knife.config[:platform] = "freebsd"
|
96
|
+
|
97
|
+
bootstrap.config[:distro].should eq("chef-server-freebsd")
|
98
|
+
end
|
99
|
+
|
100
|
+
it "configs the bootstrap's ENV with the webui password" do
|
101
|
+
bootstrap
|
102
|
+
ENV['WEBUI_PASSWORD'].should eq("daweb")
|
103
|
+
end
|
104
|
+
|
105
|
+
it "configs the bootstrap's ENV with the amqp password" do
|
106
|
+
bootstrap
|
107
|
+
ENV['AMQP_PASSWORD'].should eq("queueitup")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "#ec2_connection" do
|
112
|
+
before do
|
113
|
+
@before_config = Hash.new
|
114
|
+
@before_config[:knife] = Hash.new
|
115
|
+
[:aws_access_key_id, :aws_secret_access_key, :region].each do |attr|
|
116
|
+
@before_config[:knife][attr] = Chef::Config[:knife][attr]
|
117
|
+
end
|
118
|
+
|
119
|
+
Chef::Config[:knife][:aws_access_key_id] = "key"
|
120
|
+
Chef::Config[:knife][:aws_secret_access_key] = "secret"
|
121
|
+
Chef::Config[:knife][:region] = "hell-south-666"
|
122
|
+
end
|
123
|
+
|
124
|
+
after do
|
125
|
+
[:aws_access_key_id, :aws_secret_access_key, :region].each do |attr|
|
126
|
+
Chef::Config[:knife][attr] = @before_config[:knife][attr]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
it "constructs a connection" do
|
131
|
+
Fog::Compute.should_receive(:new).with({
|
132
|
+
:provider => 'AWS',
|
133
|
+
:aws_access_key_id => 'key',
|
134
|
+
:aws_secret_access_key => 'secret',
|
135
|
+
:region => 'hell-south-666'
|
136
|
+
})
|
137
|
+
|
138
|
+
@knife.ec2_connection
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe "#server_dns_name" do
|
143
|
+
before do
|
144
|
+
@knife.config[:chef_node_name] = 'shavemy.yak'
|
145
|
+
@knife.stub(:ec2_connection) { connection }
|
146
|
+
end
|
147
|
+
|
148
|
+
context "when server is found" do
|
149
|
+
before do
|
150
|
+
connection.stub(:servers) { [server] }
|
151
|
+
end
|
152
|
+
|
153
|
+
let(:server) do
|
154
|
+
stub(:dns_name => 'blahblah.aws.compute.com', :state => "running",
|
155
|
+
:tags => {'Name' => 'shavemy.yak', 'Role' => 'chef_server'})
|
156
|
+
end
|
157
|
+
|
158
|
+
it "returns the provisioned dns name" do
|
159
|
+
@knife.server_dns_name.should eq('blahblah.aws.compute.com')
|
160
|
+
end
|
161
|
+
|
162
|
+
it "ignores terminated instances" do
|
163
|
+
server.stub(:state) { "terminated" }
|
164
|
+
@knife.server_dns_name.should be_nil
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context "when server is not found" do
|
169
|
+
before do
|
170
|
+
connection.stub(:servers) { [] }
|
171
|
+
end
|
172
|
+
|
173
|
+
it "returns nil" do
|
174
|
+
@knife.server_dns_name.should be_nil
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
describe "#run" do
|
180
|
+
before do
|
181
|
+
@before_config = Hash.new
|
182
|
+
[:node_name, :client_key].each do |attr|
|
183
|
+
@before_config[attr] = Chef::Config[attr]
|
184
|
+
end
|
185
|
+
Chef::Config[:node_name] = "smithers"
|
186
|
+
Chef::Config[:client_key] = "/var/tmp/myclientkey.pem"
|
187
|
+
|
188
|
+
@knife.config[:security_groups] = ["mygroup"]
|
189
|
+
@knife.config[:validation_key] = "/var/tmp/validation.pem"
|
190
|
+
@knife.config[:ssh_port] = "2345"
|
191
|
+
@knife.config[:identity_file] = "~/.ssh/mykey_dsa"
|
192
|
+
@knife.stub(:ec2_connection) { connection }
|
193
|
+
@knife.stub(:server_dns_name) { "grapes.wrath" }
|
194
|
+
Chef::Knife::Ec2ServerCreate.stub(:new) { bootstrap }
|
195
|
+
Knife::Server::Ec2SecurityGroup.stub(:new) { security_group }
|
196
|
+
Knife::Server::SSH.stub(:new) { ssh }
|
197
|
+
Knife::Server::Credentials.stub(:new) { credentials }
|
198
|
+
security_group.stub(:configure_chef_server_group)
|
199
|
+
credentials.stub(:install_validation_key)
|
200
|
+
credentials.stub(:create_root_client)
|
201
|
+
end
|
202
|
+
|
203
|
+
after do
|
204
|
+
[:node_name, :client_key].each do |attr|
|
205
|
+
Chef::Config[attr] = @before_config[attr]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
let(:bootstrap) { stub(:run => true, :config => Hash.new) }
|
210
|
+
let(:security_group) { stub }
|
211
|
+
let(:ssh) { stub }
|
212
|
+
let(:credentials) { stub.as_null_object }
|
213
|
+
|
214
|
+
it "exits if node_name option is missing" do
|
215
|
+
def @knife.exit(code) ; end
|
216
|
+
@knife.config.delete(:chef_node_name)
|
217
|
+
|
218
|
+
@knife.should_receive(:exit)
|
219
|
+
@knife.run
|
220
|
+
end
|
221
|
+
|
222
|
+
it "configures the ec2 security group" do
|
223
|
+
Knife::Server::Ec2SecurityGroup.should_receive(:new).
|
224
|
+
with(connection, @knife.ui)
|
225
|
+
security_group.should_receive(:configure_chef_server_group).
|
226
|
+
with('mygroup', :description => 'mygroup group')
|
227
|
+
|
228
|
+
@knife.run
|
229
|
+
end
|
230
|
+
|
231
|
+
it "bootstraps an ec2 server" do
|
232
|
+
bootstrap.should_receive(:run)
|
233
|
+
@knife.run
|
234
|
+
end
|
235
|
+
|
236
|
+
it "installs a new validation.pem key from the server" do
|
237
|
+
Knife::Server::SSH.should_receive(:new).
|
238
|
+
with({ :host => "grapes.wrath", :user => "root", :port => "2345" })
|
239
|
+
Knife::Server::Credentials.should_receive(:new).
|
240
|
+
with(ssh, "/etc/chef/validation.pem")
|
241
|
+
credentials.should_receive(:install_validation_key)
|
242
|
+
|
243
|
+
@knife.run
|
244
|
+
end
|
245
|
+
|
246
|
+
it "create a root client key" do
|
247
|
+
credentials.should_receive(:create_root_client)
|
248
|
+
|
249
|
+
@knife.run
|
250
|
+
end
|
251
|
+
|
252
|
+
it "installs a client key" do
|
253
|
+
credentials.should_receive(:install_client_key).
|
254
|
+
with("smithers", "/var/tmp/myclientkey.pem")
|
255
|
+
|
256
|
+
@knife.run
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'knife/server/credentials'
|
2
|
+
require 'fakefs/spec_helpers'
|
3
|
+
|
4
|
+
describe Knife::Server::Credentials do
|
5
|
+
include FakeFS::SpecHelpers
|
6
|
+
|
7
|
+
let(:ssh) { stub("SSH Client") }
|
8
|
+
let(:validation_key_path) { "/tmp/validation.pem" }
|
9
|
+
let(:client_key_path) { "/tmp/client.pem" }
|
10
|
+
|
11
|
+
subject do
|
12
|
+
Knife::Server::Credentials.new(ssh, validation_key_path)
|
13
|
+
end
|
14
|
+
|
15
|
+
before do
|
16
|
+
FileUtils.mkdir_p(File.dirname(validation_key_path))
|
17
|
+
FileUtils.mkdir_p(File.dirname(client_key_path))
|
18
|
+
File.new(validation_key_path, "wb") { |f| f.write("thekey") }
|
19
|
+
File.new(client_key_path, "wb") { |f| f.write("clientkey") }
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#install_validation_key" do
|
23
|
+
before do
|
24
|
+
ssh.stub(:exec!).with("cat /etc/chef/validation.pem") { "newkey" }
|
25
|
+
end
|
26
|
+
|
27
|
+
it "creates a backup of the existing validation key file" do
|
28
|
+
original = File.open("/tmp/validation.pem", "rb") { |f| f.read }
|
29
|
+
subject.install_validation_key("old")
|
30
|
+
backup = File.open("/tmp/validation.old.pem", "rb") { |f| f.read }
|
31
|
+
|
32
|
+
original.should eq(backup)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "skips backup file creation if validation key file does not exist" do
|
36
|
+
FileUtils.rm_f(validation_key_path)
|
37
|
+
subject.install_validation_key("old")
|
38
|
+
|
39
|
+
File.exists?("/tmp/validation.old.pem").should_not be_true
|
40
|
+
end
|
41
|
+
|
42
|
+
it "copies the key back from the server into validation key file" do
|
43
|
+
subject.install_validation_key("old")
|
44
|
+
key_str = File.open("/tmp/validation.pem", "rb") { |f| f.read }
|
45
|
+
|
46
|
+
key_str.should eq("newkey")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#create_root_client" do
|
51
|
+
it "creates an initial client key on the server" do
|
52
|
+
ssh.should_receive(:exec!).with([
|
53
|
+
'knife configure --initial --server-url http://127.0.0.1:4000',
|
54
|
+
'--user root --repository "" --defaults --yes'
|
55
|
+
].join(" "))
|
56
|
+
|
57
|
+
subject.create_root_client
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "#install_client_key" do
|
62
|
+
before do
|
63
|
+
ssh.stub(:exec!)
|
64
|
+
ssh.stub(:exec!).with("cat /tmp/chef-client-bob.pem") { "bobkey" }
|
65
|
+
end
|
66
|
+
|
67
|
+
it "creates a user client key on the server" do
|
68
|
+
ssh.should_receive(:exec!).with([
|
69
|
+
"knife client create bob --admin",
|
70
|
+
"--file /tmp/chef-client-bob.pem --disable-editing",
|
71
|
+
].join(" "))
|
72
|
+
|
73
|
+
subject.install_client_key("bob", client_key_path)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "creates a backup of the existing client key file" do
|
77
|
+
original = File.open("/tmp/client.pem", "rb") { |f| f.read }
|
78
|
+
subject.install_client_key("bob", client_key_path, "old")
|
79
|
+
backup = File.open("/tmp/client.old.pem", "rb") { |f| f.read }
|
80
|
+
|
81
|
+
original.should eq(backup)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "skips backup file creation if client key file does not exist" do
|
85
|
+
FileUtils.rm_f(client_key_path)
|
86
|
+
subject.install_client_key("bob", client_key_path, "old")
|
87
|
+
|
88
|
+
File.exists?("/tmp/client.old.pem").should_not be_true
|
89
|
+
end
|
90
|
+
|
91
|
+
it "copies the key back from the server into client key file" do
|
92
|
+
subject.install_client_key("bob", client_key_path, "old")
|
93
|
+
key_str = File.open("/tmp/client.pem", "rb") { |f| f.read }
|
94
|
+
|
95
|
+
key_str.should eq("bobkey")
|
96
|
+
end
|
97
|
+
|
98
|
+
it "removes the user client key from the server" do
|
99
|
+
ssh.should_receive(:exec!).with("rm -f /tmp/chef-client-bob.pem")
|
100
|
+
|
101
|
+
subject.install_client_key("bob", client_key_path)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|