knife-ec2 0.5.14 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,7 +16,7 @@ Gem::Specification.new do |s|
16
16
  s.files = `git ls-files`.split("\n")
17
17
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
- s.add_dependency "fog", "~> 1.3"
19
+ s.add_dependency "fog", "~> 1.6"
20
20
  s.add_dependency "chef", ">= 0.10.10"
21
21
  %w(rspec-core rspec-expectations rspec-mocks rspec_junit_formatter).each { |gem| s.add_development_dependency gem }
22
22
 
@@ -49,7 +49,6 @@ class Chef
49
49
  option :region,
50
50
  :long => "--region REGION",
51
51
  :description => "Your AWS region",
52
- :default => "us-east-1",
53
52
  :proc => Proc.new { |key| Chef::Config[:knife][:region] = key }
54
53
  end
55
54
  end
@@ -42,8 +42,7 @@ class Chef
42
42
  :short => "-f FLAVOR",
43
43
  :long => "--flavor FLAVOR",
44
44
  :description => "The flavor of server (m1.small, m1.medium, etc)",
45
- :proc => Proc.new { |f| Chef::Config[:knife][:flavor] = f },
46
- :default => "m1.small"
45
+ :proc => Proc.new { |f| Chef::Config[:knife][:flavor] = f }
47
46
 
48
47
  option :image,
49
48
  :short => "-I IMAGE",
@@ -73,13 +72,13 @@ class Chef
73
72
  :short => "-Z ZONE",
74
73
  :long => "--availability-zone ZONE",
75
74
  :description => "The Availability Zone",
76
- :default => "us-east-1b",
77
75
  :proc => Proc.new { |key| Chef::Config[:knife][:availability_zone] = key }
78
76
 
79
77
  option :chef_node_name,
80
78
  :short => "-N NAME",
81
79
  :long => "--node-name NAME",
82
- :description => "The Chef node name for your new node"
80
+ :description => "The Chef node name for your new node",
81
+ :proc => Proc.new { |key| Chef::Config[:knife][:chef_node_name] = key }
83
82
 
84
83
  option :ssh_key_name,
85
84
  :short => "-S KEY",
@@ -105,6 +104,13 @@ class Chef
105
104
  :default => "22",
106
105
  :proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key }
107
106
 
107
+ option :ssh_gateway,
108
+ :short => "-w GATEWAY",
109
+ :long => "--ssh-gateway GATEWAY",
110
+ :description => "The ssh gateway server",
111
+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key }
112
+
113
+
108
114
  option :identity_file,
109
115
  :short => "-i IDENTITY_FILE",
110
116
  :long => "--identity-file IDENTITY_FILE",
@@ -123,8 +129,7 @@ class Chef
123
129
  :short => "-d DISTRO",
124
130
  :long => "--distro DISTRO",
125
131
  :description => "Bootstrap a distro using a template; default is 'chef-full'",
126
- :proc => Proc.new { |d| Chef::Config[:knife][:distro] = d },
127
- :default => "chef-full"
132
+ :proc => Proc.new { |d| Chef::Config[:knife][:distro] = d }
128
133
 
129
134
  option :template_file,
130
135
  :long => "--template-file TEMPLATE",
@@ -136,30 +141,31 @@ class Chef
136
141
  :long => "--ebs-size SIZE",
137
142
  :description => "The size of the EBS volume in GB, for EBS-backed instances"
138
143
 
144
+ option :ebs_optimized,
145
+ :long => "--ebs-optimized",
146
+ :description => "Enabled optimized EBS I/O"
147
+
139
148
  option :ebs_no_delete_on_term,
140
149
  :long => "--ebs-no-delete-on-term",
141
- :description => "Do not delete EBS volumn on instance termination"
150
+ :description => "Do not delete EBS volume on instance termination"
142
151
 
143
152
  option :run_list,
144
153
  :short => "-r RUN_LIST",
145
154
  :long => "--run-list RUN_LIST",
146
155
  :description => "Comma separated list of roles/recipes to apply",
147
- :proc => lambda { |o| o.split(/[\s,]+/) },
148
- :default => []
156
+ :proc => lambda { |o| o.split(/[\s,]+/) }
149
157
 
150
158
  option :json_attributes,
151
159
  :short => "-j JSON",
152
160
  :long => "--json-attributes JSON",
153
161
  :description => "A JSON string to be added to the first run of chef-client",
154
- :proc => lambda { |o| JSON.parse(o) },
155
- :default => {}
156
-
162
+ :proc => lambda { |o| JSON.parse(o) }
157
163
 
158
164
  option :subnet_id,
159
165
  :short => "-s SUBNET-ID",
160
166
  :long => "--subnet SUBNET-ID",
161
167
  :description => "create node in this Virtual Private Cloud Subnet ID (implies VPC mode)",
162
- :default => false
168
+ :proc => Proc.new { |key| Chef::Config[:knife][:subnet_id] = key }
163
169
 
164
170
  option :host_key_verify,
165
171
  :long => "--[no-]host-key-verify",
@@ -174,8 +180,29 @@ class Chef
174
180
  :proc => Proc.new { |m| Chef::Config[:knife][:aws_user_data] = m },
175
181
  :default => nil
176
182
 
177
- def tcp_test_ssh(hostname)
178
- tcp_socket = TCPSocket.new(hostname, config[:ssh_port])
183
+ Chef::Config[:knife][:hints] ||= {"ec2" => {}}
184
+ option :hint,
185
+ :long => "--hint HINT_NAME[=HINT_FILE]",
186
+ :description => "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.",
187
+ :proc => Proc.new { |h|
188
+ name, path = h.split("=")
189
+ Chef::Config[:knife][:hints][name] = path ? JSON.parse(::File.read(path)) : Hash.new
190
+ }
191
+
192
+ option :ephemeral,
193
+ :long => "--ephemeral EPHEMERAL_DEVICES",
194
+ :description => "Comma separated list of device locations (eg - /dev/sdb) to map ephemeral devices",
195
+ :proc => lambda { |o| o.split(/[\s,]+/) },
196
+ :default => []
197
+
198
+ option :server_connect_attribute,
199
+ :long => "--server-connect-attribute ATTRIBUTE",
200
+ :short => "-a ATTRIBUTE",
201
+ :description => "The EC2 server attribute to use for SSH connection",
202
+ :default => nil
203
+
204
+ def tcp_test_ssh(hostname, ssh_port)
205
+ tcp_socket = TCPSocket.new(hostname, ssh_port)
179
206
  readable = IO.select([tcp_socket], nil, nil, 5)
180
207
  if readable
181
208
  Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
@@ -184,23 +211,10 @@ class Chef
184
211
  else
185
212
  false
186
213
  end
187
- rescue SocketError
214
+ rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
188
215
  sleep 2
189
216
  false
190
- rescue Errno::ETIMEDOUT
191
- false
192
- rescue Errno::EPERM
193
- false
194
- rescue Errno::ECONNREFUSED
195
- sleep 2
196
- false
197
- # This happens on EC2 quite often
198
- rescue Errno::EHOSTUNREACH
199
- sleep 2
200
- false
201
- # This happens on EC2 sometimes
202
- rescue Errno::ENETUNREACH
203
- sleep 2
217
+ rescue Errno::EPERM, Errno::ETIMEDOUT
204
218
  false
205
219
  ensure
206
220
  tcp_socket && tcp_socket.close
@@ -236,7 +250,7 @@ class Chef
236
250
  # default security group id at this point unless we look it up, hence
237
251
  # 'default' is printed if no id was specified.
238
252
  printed_security_groups = "default"
239
- printed_security_groups = @server.groups.join(", ") if @server.groups
253
+ printed_security_groups = @server.groups.join(", ") if @server.groups
240
254
  msg_pair("Security Groups", printed_security_groups) unless vpc_mode? or (@server.groups.nil? and @server.security_group_ids)
241
255
 
242
256
  printed_security_group_ids = "default"
@@ -264,14 +278,9 @@ class Chef
264
278
 
265
279
  print "\n#{ui.color("Waiting for sshd", :magenta)}"
266
280
 
267
- fqdn = vpc_mode? ? @server.private_ip_address : @server.dns_name
281
+ wait_for_sshd(ssh_connect_host)
268
282
 
269
- print(".") until tcp_test_ssh(fqdn) {
270
- sleep @initial_sleep_delay ||= (vpc_mode? ? 40 : 10)
271
- puts("done")
272
- }
273
-
274
- bootstrap_for_node(@server,fqdn).run
283
+ bootstrap_for_node(@server,ssh_connect_host).run
275
284
 
276
285
  puts "\n"
277
286
  msg_pair("Instance ID", @server.id)
@@ -300,6 +309,9 @@ class Chef
300
309
  end
301
310
  end
302
311
  end
312
+ if config[:ebs_optimized]
313
+ msg_pair("EBS is Optimized", @server.ebs_optimized.to_s)
314
+ end
303
315
  if vpc_mode?
304
316
  msg_pair("Subnet ID", @server.subnet_id)
305
317
  else
@@ -309,22 +321,23 @@ class Chef
309
321
  end
310
322
  msg_pair("Private IP Address", @server.private_ip_address)
311
323
  msg_pair("Environment", config[:environment] || '_default')
312
- msg_pair("Run List", config[:run_list].join(', '))
313
- msg_pair("JSON Attributes",config[:json_attributes]) unless config[:json_attributes].empty?
324
+ msg_pair("Run List", (config[:run_list] || []).join(', '))
325
+ msg_pair("JSON Attributes",config[:json_attributes]) unless !config[:json_attributes] || config[:json_attributes].empty?
314
326
  end
315
327
 
316
- def bootstrap_for_node(server,fqdn)
328
+ def bootstrap_for_node(server,ssh_host)
317
329
  bootstrap = Chef::Knife::Bootstrap.new
318
- bootstrap.name_args = [fqdn]
319
- bootstrap.config[:run_list] = config[:run_list]
330
+ bootstrap.name_args = [ssh_host]
331
+ bootstrap.config[:run_list] = locate_config_value(:run_list) || []
320
332
  bootstrap.config[:ssh_user] = config[:ssh_user]
321
333
  bootstrap.config[:ssh_port] = config[:ssh_port]
334
+ bootstrap.config[:ssh_gateway] = config[:ssh_gateway]
322
335
  bootstrap.config[:identity_file] = config[:identity_file]
323
- bootstrap.config[:chef_node_name] = config[:chef_node_name] || server.id
336
+ bootstrap.config[:chef_node_name] = locate_config_value(:chef_node_name) || server.id
324
337
  bootstrap.config[:prerelease] = config[:prerelease]
325
338
  bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
326
- bootstrap.config[:first_boot_attributes] = config[:json_attributes]
327
- bootstrap.config[:distro] = locate_config_value(:distro)
339
+ bootstrap.config[:first_boot_attributes] = locate_config_value(:json_attributes) || {}
340
+ bootstrap.config[:distro] = locate_config_value(:distro) || "chef-full"
328
341
  bootstrap.config[:use_sudo] = true unless config[:ssh_user] == 'root'
329
342
  bootstrap.config[:template_file] = locate_config_value(:template_file)
330
343
  bootstrap.config[:environment] = config[:environment]
@@ -336,7 +349,7 @@ class Chef
336
349
  def vpc_mode?
337
350
  # Amazon Virtual Private Cloud requires a subnet_id. If
338
351
  # present, do a few things differently
339
- !!config[:subnet_id]
352
+ !!locate_config_value(:subnet_id)
340
353
  end
341
354
 
342
355
  def ami
@@ -351,7 +364,7 @@ class Chef
351
364
  ui.error("You have not provided a valid image (AMI) value. Please note the short option for this value recently changed from '-i' to '-I'.")
352
365
  exit 1
353
366
  end
354
-
367
+
355
368
  if vpc_mode? and !!config[:security_groups]
356
369
  ui.error("You are using a VPC, security groups specified with '-G' are not allowed, specify one or more security group ids with '-g' instead.")
357
370
  exit 1
@@ -372,12 +385,12 @@ class Chef
372
385
  server_def = {
373
386
  :image_id => locate_config_value(:image),
374
387
  :groups => config[:security_groups],
375
- :security_group_ids => config[:security_group_ids],
388
+ :security_group_ids => locate_config_value(:security_group_ids),
376
389
  :flavor_id => locate_config_value(:flavor),
377
390
  :key_name => Chef::Config[:knife][:aws_ssh_key_id],
378
391
  :availability_zone => locate_config_value(:availability_zone)
379
392
  }
380
- server_def[:subnet_id] = config[:subnet_id] if config[:subnet_id]
393
+ server_def[:subnet_id] = locate_config_value(:subnet_id) if vpc_mode?
381
394
 
382
395
  if Chef::Config[:knife][:aws_user_data]
383
396
  begin
@@ -387,6 +400,12 @@ class Chef
387
400
  end
388
401
  end
389
402
 
403
+ if config[:ebs_optimized]
404
+ server_def[:ebs_optimized] = "true"
405
+ else
406
+ server_def[:ebs_optimized] = "false"
407
+ end
408
+
390
409
  if ami.root_device_type == "ebs"
391
410
  ami_map = ami.block_device_mapping.first
392
411
  ebs_size = begin
@@ -405,6 +424,7 @@ class Chef
405
424
  else
406
425
  ami_map["deleteOnTermination"]
407
426
  end
427
+
408
428
  server_def[:block_device_mapping] =
409
429
  [{
410
430
  'DeviceName' => ami_map["deviceName"],
@@ -413,8 +433,55 @@ class Chef
413
433
  }]
414
434
  end
415
435
 
436
+ (config[:ephemeral] || []).each_with_index do |device_name, i|
437
+ server_def[:block_device_mapping] = (server_def[:block_device_mapping] || []) << {'VirtualName' => "ephemeral#{i}", 'DeviceName' => device_name}
438
+ end
439
+
416
440
  server_def
417
441
  end
442
+
443
+ def wait_for_sshd(hostname)
444
+ config[:ssh_gateway] ? wait_for_tunnelled_sshd(hostname) : wait_for_direct_sshd(hostname, config[:ssh_port])
445
+ end
446
+
447
+ def wait_for_tunnelled_sshd(hostname)
448
+ print(".")
449
+ print(".") until tunnel_test_ssh(ssh_connect_host) {
450
+ sleep @initial_sleep_delay ||= (vpc_mode? ? 40 : 10)
451
+ puts("done")
452
+ }
453
+ end
454
+
455
+ def tunnel_test_ssh(hostname, &block)
456
+ gw_host, gw_user = config[:ssh_gateway].split('@').reverse
457
+ gw_host, gw_port = gw_host.split(':')
458
+ gateway = Net::SSH::Gateway.new(gw_host, gw_user, :port => gw_port || 22)
459
+ status = false
460
+ gateway.open(hostname, config[:ssh_port]) do |local_tunnel_port|
461
+ status = tcp_test_ssh('localhost', local_tunnel_port, &block)
462
+ end
463
+ status
464
+ rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
465
+ sleep 2
466
+ false
467
+ rescue Errno::EPERM, Errno::ETIMEDOUT
468
+ false
469
+ end
470
+
471
+ def wait_for_direct_sshd(hostname, ssh_port)
472
+ print(".") until tcp_test_ssh(ssh_connect_host, ssh_port) {
473
+ sleep @initial_sleep_delay ||= (vpc_mode? ? 40 : 10)
474
+ puts("done")
475
+ }
476
+ end
477
+
478
+ def ssh_connect_host
479
+ @ssh_connect_host ||= if config[:server_connect_attribute]
480
+ server.send(config[:server_connect_attribute])
481
+ else
482
+ vpc_mode? ? server.private_ip_address : server.dns_name
483
+ end
484
+ end
418
485
  end
419
486
  end
420
487
  end
@@ -27,6 +27,18 @@ class Chef
27
27
 
28
28
  banner "knife ec2 server list (options)"
29
29
 
30
+ option :name,
31
+ :short => "-n",
32
+ :long => "--no-name",
33
+ :boolean => true,
34
+ :default => true,
35
+ :description => "Do not display name tag in output"
36
+
37
+ option :tags,
38
+ :short => "-t TAG1,TAG2",
39
+ :long => "--tags TAG1,TAG2",
40
+ :description => "List of tags to output"
41
+
30
42
  def run
31
43
  $stdout.sync = true
32
44
 
@@ -34,22 +46,49 @@ class Chef
34
46
 
35
47
  server_list = [
36
48
  ui.color('Instance ID', :bold),
49
+
50
+ if config[:name]
51
+ ui.color("Name", :bold)
52
+ end,
53
+
37
54
  ui.color('Public IP', :bold),
38
55
  ui.color('Private IP', :bold),
39
56
  ui.color('Flavor', :bold),
40
57
  ui.color('Image', :bold),
41
58
  ui.color('SSH Key', :bold),
42
59
  ui.color('Security Groups', :bold),
60
+
61
+ if config[:tags]
62
+ config[:tags].split(",").collect do |tag_name|
63
+ ui.color("Tag:#{tag_name}", :bold)
64
+ end
65
+ end,
66
+
43
67
  ui.color('State', :bold)
44
- ]
68
+ ].flatten.compact
69
+
70
+ output_column_count = server_list.length
71
+
45
72
  connection.servers.all.each do |server|
46
73
  server_list << server.id.to_s
74
+
75
+ if config[:name]
76
+ server_list << server.tags["Name"].to_s
77
+ end
78
+
47
79
  server_list << server.public_ip_address.to_s
48
80
  server_list << server.private_ip_address.to_s
49
81
  server_list << server.flavor_id.to_s
50
82
  server_list << server.image_id.to_s
51
83
  server_list << server.key_name.to_s
52
84
  server_list << server.groups.join(", ")
85
+
86
+ if config[:tags]
87
+ config[:tags].split(",").each do |tag_name|
88
+ server_list << server.tags[tag_name].to_s
89
+ end
90
+ end
91
+
53
92
  server_list << begin
54
93
  state = server.state.to_s.downcase
55
94
  case state
@@ -62,11 +101,9 @@ class Chef
62
101
  end
63
102
  end
64
103
  end
65
- puts ui.list(server_list, :uneven_columns_across, 8)
104
+ puts ui.list(server_list, :uneven_columns_across, output_column_count)
66
105
 
67
106
  end
68
107
  end
69
108
  end
70
109
  end
71
-
72
-
@@ -1,6 +1,6 @@
1
1
  module Knife
2
2
  module Ec2
3
- VERSION = "0.5.14"
3
+ VERSION = "0.6.0"
4
4
  MAJOR, MINOR, TINY = VERSION.split('.')
5
5
  end
6
6
  end
@@ -128,6 +128,7 @@ describe Chef::Knife::Ec2ServerCreate do
128
128
  @knife_ec2_create.config[:ssh_user] = "ubuntu"
129
129
  @knife_ec2_create.config[:identity_file] = "~/.ssh/aws-key.pem"
130
130
  @knife_ec2_create.config[:ssh_port] = 22
131
+ @knife_ec2_create.config[:ssh_gateway] = 'bastion.host.com'
131
132
  @knife_ec2_create.config[:chef_node_name] = "blarf"
132
133
  @knife_ec2_create.config[:template_file] = '~/.chef/templates/my-bootstrap.sh.erb'
133
134
  @knife_ec2_create.config[:distro] = 'ubuntu-10.04-magic-sparkles'
@@ -153,6 +154,10 @@ describe Chef::Knife::Ec2ServerCreate do
153
154
  @bootstrap.config[:ssh_user].should == 'ubuntu'
154
155
  end
155
156
 
157
+ it "configures the bootstrap to use the correct ssh_gateway host" do
158
+ @bootstrap.config[:ssh_gateway].should == 'bastion.host.com'
159
+ end
160
+
156
161
  it "configures the bootstrap to use the correct ssh identity file" do
157
162
  @bootstrap.config[:identity_file].should == "~/.ssh/aws-key.pem"
158
163
  end
@@ -192,6 +197,10 @@ describe Chef::Knife::Ec2ServerCreate do
192
197
  it "configured the bootstrap to use the desired template" do
193
198
  @bootstrap.config[:template_file].should == '~/.chef/templates/my-bootstrap.sh.erb'
194
199
  end
200
+
201
+ it "configured the bootstrap to set an ec2 hint (via Chef::Config)" do
202
+ Chef::Config[:knife][:hints]["ec2"].should_not be_nil
203
+ end
195
204
  end
196
205
 
197
206
  describe "when validating the command-line parameters" do
@@ -251,6 +260,47 @@ describe Chef::Knife::Ec2ServerCreate do
251
260
 
252
261
  server_def[:availability_zone].should == "dis-one"
253
262
  end
263
+
264
+ it "adds the specified ephemeral device mappings" do
265
+ @knife_ec2_create.config[:ephemeral] = [ "/dev/sdb", "/dev/sdc", "/dev/sdd", "/dev/sde" ]
266
+ server_def = @knife_ec2_create.create_server_def
267
+
268
+ server_def[:block_device_mapping].should == [{ "VirtualName" => "ephemeral0", "DeviceName" => "/dev/sdb" },
269
+ { "VirtualName" => "ephemeral1", "DeviceName" => "/dev/sdc" },
270
+ { "VirtualName" => "ephemeral2", "DeviceName" => "/dev/sdd" },
271
+ { "VirtualName" => "ephemeral3", "DeviceName" => "/dev/sde" }]
272
+ end
254
273
  end
255
274
 
275
+ describe "ssh_connect_host" do
276
+ before(:each) do
277
+ @new_ec2_server.stub!(
278
+ :dns_name => 'public_name',
279
+ :private_ip_address => 'private_ip',
280
+ :custom => 'custom'
281
+ )
282
+ @knife_ec2_create.stub!(:server => @new_ec2_server)
283
+ end
284
+
285
+ describe "by default" do
286
+ it 'should use public dns name' do
287
+ @knife_ec2_create.ssh_connect_host.should == 'public_name'
288
+ end
289
+ end
290
+
291
+ describe "with vpc_mode?" do
292
+ it 'should use private ip' do
293
+ @knife_ec2_create.stub!(:vpc_mode? => true)
294
+ @knife_ec2_create.ssh_connect_host.should == 'private_ip'
295
+ end
296
+
297
+ end
298
+
299
+ describe "with custom server attribute" do
300
+ it 'should use custom server attribute' do
301
+ @knife_ec2_create.config[:server_connect_attribute] = 'custom'
302
+ @knife_ec2_create.ssh_connect_host.should == 'custom'
303
+ end
304
+ end
305
+ end
256
306
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: knife-ec2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.14
4
+ version: 0.6.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-07-26 00:00:00.000000000 Z
13
+ date: 2012-10-16 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: fog
@@ -19,7 +19,7 @@ dependencies:
19
19
  requirements:
20
20
  - - ~>
21
21
  - !ruby/object:Gem::Version
22
- version: '1.3'
22
+ version: '1.6'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -27,7 +27,7 @@ dependencies:
27
27
  requirements:
28
28
  - - ~>
29
29
  - !ruby/object:Gem::Version
30
- version: '1.3'
30
+ version: '1.6'
31
31
  - !ruby/object:Gem::Dependency
32
32
  name: chef
33
33
  requirement: !ruby/object:Gem::Requirement
@@ -162,3 +162,4 @@ summary: EC2 Support for Chef's Knife Command
162
162
  test_files:
163
163
  - spec/spec_helper.rb
164
164
  - spec/unit/ec2_server_create_spec.rb
165
+ has_rdoc: true