aptible-cli 0.26.4 → 0.26.6

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/lib/aptible/cli/helpers/app.rb +18 -16
  4. data/lib/aptible/cli/helpers/database.rb +20 -19
  5. data/lib/aptible/cli/helpers/environment.rb +5 -4
  6. data/lib/aptible/cli/helpers/vhost/option_set_builder.rb +154 -1
  7. data/lib/aptible/cli/helpers/vhost.rb +5 -2
  8. data/lib/aptible/cli/renderer/text.rb +3 -1
  9. data/lib/aptible/cli/resource_formatter.rb +6 -0
  10. data/lib/aptible/cli/subcommands/backup.rb +1 -1
  11. data/lib/aptible/cli/subcommands/endpoints.rb +27 -9
  12. data/lib/aptible/cli/version.rb +1 -1
  13. data/spec/aptible/cli/helpers/vhost/option_set_builder_spec.rb +94 -0
  14. data/spec/aptible/cli/resource_formatter_spec.rb +3 -0
  15. data/spec/aptible/cli/subcommands/apps_spec.rb +34 -21
  16. data/spec/aptible/cli/subcommands/backup_retention_policy_spec.rb +3 -1
  17. data/spec/aptible/cli/subcommands/backup_spec.rb +28 -3
  18. data/spec/aptible/cli/subcommands/config_spec.rb +3 -3
  19. data/spec/aptible/cli/subcommands/db_spec.rb +46 -44
  20. data/spec/aptible/cli/subcommands/deploy_spec.rb +8 -3
  21. data/spec/aptible/cli/subcommands/endpoints_spec.rb +198 -8
  22. data/spec/aptible/cli/subcommands/environment_spec.rb +4 -0
  23. data/spec/aptible/cli/subcommands/log_drain_spec.rb +9 -0
  24. data/spec/aptible/cli/subcommands/logs_spec.rb +16 -4
  25. data/spec/aptible/cli/subcommands/maintenance_spec.rb +7 -2
  26. data/spec/aptible/cli/subcommands/metric_drain_spec.rb +10 -2
  27. data/spec/aptible/cli/subcommands/services_spec.rb +3 -4
  28. data/spec/fabricators/setting_fabricator.rb +8 -0
  29. data/spec/fabricators/vhost_fabricator.rb +2 -0
  30. data/spec/spec_helper.rb +2 -0
  31. metadata +6 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c756f930a26ab5b060f01270b80c9ec2963c8f739be4ffcd2a5d56e9dac2860
4
- data.tar.gz: 55fdde5cde257bd36c6c16a17e7f639745f85947b1cf40269c0d73ab0093615c
3
+ metadata.gz: 96e4d08173fd6d471b8a0a4f625d5bc1b3cf22af4c6cf2bb71fb3da109bc27af
4
+ data.tar.gz: 53ff531ac50e1a925a7c6ddc01bf5cdc7c972333e4f7ef72e5af83b0001c5ae0
5
5
  SHA512:
6
- metadata.gz: 01122dab06813f9b67100c11e6e59ec6942f5263340efc26bbf73440b51c39c7cd0be7258b987fa39056092b29bf181ded4d14423d27ff795cd18bf85b97fe96
7
- data.tar.gz: d7ed67882c43c958dcc60c7ef5ba518f3eb6ad72ca014f4c0b761478f2748054af64286aca26b8ac97b5ca5ea03964bc34fd422d3cf42f4996b4c6949815d870
6
+ metadata.gz: dec6c2d62bbe07eb89627dad9c1984133e7768f4b87316ac3029a1fd94f71d22418ca6b3ad73efa445425e60cd620d75d9653b23e4da10c7f28a62b6fc460d69
7
+ data.tar.gz: fd1b7ab666de7e4e13300ba4057d265edf919f40abaddcac96a90f9437b8ad23144775f4fc49124bae2af06f668aea57bdf976eeffa1fab60a7e01abb5ffd241
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- aptible-cli (0.26.4)
4
+ aptible-cli (0.26.6)
5
5
  activesupport (>= 4.0, < 6.0)
6
6
  aptible-api (~> 1.12)
7
7
  aptible-auth (~> 1.4)
@@ -110,12 +110,9 @@ module Aptible
110
110
  end
111
111
  end
112
112
 
113
- apps = apps_from_handle(s.app_handle, environment)
113
+ app = app_from_handle(s.app_handle, environment)
114
114
 
115
- case apps.count
116
- when 1
117
- return apps.first
118
- when 0
115
+ if app.nil?
119
116
  err_bits = ['Could not find app', s.app_handle]
120
117
  if environment
121
118
  err_bits << 'in environment'
@@ -125,11 +122,9 @@ module Aptible
125
122
  end
126
123
  err_bits << s.explain
127
124
  raise Thor::Error, err_bits.join(' ')
128
- else
129
- err = "Multiple apps named #{s.app_handle} exist, please specify " \
130
- 'with --environment'
131
- raise Thor::Error, err
132
125
  end
126
+
127
+ app
133
128
  end
134
129
 
135
130
  def ensure_service(options, type)
@@ -166,13 +161,20 @@ module Aptible
166
161
  )
167
162
  end
168
163
 
169
- def apps_from_handle(handle, environment)
170
- # TODO: This should probably use each_app for more efficiency.
171
- if environment
172
- environment.apps
173
- else
174
- apps_all
175
- end.select { |a| a.handle == handle }
164
+ def app_from_handle(handle, environment)
165
+ url = "/find/app?handle=#{handle}"
166
+ url += "&environment=#{environment.handle}" unless environment.nil?
167
+
168
+ Aptible::Api::App.find_by_url(
169
+ url,
170
+ token: fetch_token
171
+ )
172
+ rescue HyperResource::ClientError => e
173
+ raise unless e.body.is_a?(Hash) &&
174
+ e.body['error'] == 'multiple_resources_found'
175
+ raise Thor::Error,
176
+ "Multiple apps named #{handle} exist, please specify " \
177
+ 'with --environment'
176
178
  end
177
179
 
178
180
  def extract_env(args)
@@ -36,16 +36,10 @@ module Aptible
36
36
  raise Thor::Error,
37
37
  "Could not find environment #{environment_handle}"
38
38
  end
39
- databases = databases_from_handle(db_handle, environment)
40
- case databases.count
41
- when 1
42
- return databases.first
43
- when 0
44
- raise Thor::Error, "Could not find database #{db_handle}"
45
- else
46
- err = 'Multiple databases exist, please specify with --environment'
47
- raise Thor::Error, err
48
- end
39
+ db = database_from_handle(db_handle, environment)
40
+ raise Thor::Error, "Could not find database #{db_handle}" if db.nil?
41
+
42
+ db
49
43
  end
50
44
 
51
45
  def databases_href
@@ -140,20 +134,27 @@ module Aptible
140
134
  external_rds_databases_all.find { |a| a.handle == handle }
141
135
  end
142
136
 
143
- def databases_from_handle(handle, environment)
144
- databases = if environment
145
- environment.databases
146
- else
147
- databases_all
148
- end
149
- databases.select { |a| a.handle == handle }
137
+ def database_from_handle(handle, environment)
138
+ url = "/find/database?handle=#{handle}"
139
+ url += "&environment=#{environment.handle}" unless environment.nil?
140
+
141
+ Aptible::Api::Database.find_by_url(
142
+ url,
143
+ token: fetch_token
144
+ )
145
+ rescue HyperResource::ClientError => e
146
+ raise unless e.body.is_a?(Hash) &&
147
+ e.body['error'] == 'multiple_resources_found'
148
+ raise Thor::Error,
149
+ 'Multiple databases exist, please specify ' \
150
+ 'with --environment'
150
151
  end
151
152
 
152
153
  def clone_database(source, dest_handle)
153
154
  op = source.create_operation!(type: 'clone', handle: dest_handle)
154
155
  attach_to_operation_logs(op)
155
156
 
156
- databases_from_handle(dest_handle, source.account).first
157
+ database_from_handle(dest_handle, source.account)
157
158
  end
158
159
 
159
160
  def replicate_database(source, dest_handle, options)
@@ -177,7 +178,7 @@ module Aptible
177
178
  op = source.create_operation!(replication_params)
178
179
  attach_to_operation_logs(op)
179
180
 
180
- replica = databases_from_handle(dest_handle, source.account).first
181
+ replica = database_from_handle(dest_handle, source.account)
181
182
  attach_to_operation_logs(replica.operations.last)
182
183
  replica
183
184
  end
@@ -42,10 +42,11 @@ module Aptible
42
42
 
43
43
  def environment_from_handle(handle)
44
44
  return nil unless handle
45
- href = environment_href
46
- Aptible::Api::Account.all(token: fetch_token, href: href).find do |a|
47
- a.handle == handle
48
- end
45
+
46
+ Aptible::Api::Account.find_by_url(
47
+ "/find/account?handle=#{handle}",
48
+ token: fetch_token
49
+ )
49
50
  end
50
51
 
51
52
  def environment_map(accounts)
@@ -14,6 +14,35 @@ module Aptible
14
14
  alb
15
15
  ).freeze
16
16
 
17
+ SSL_PROTOCOL_VALUES = [
18
+ 'TLSv1 TLSv1.1 TLSv1.2',
19
+ 'TLSv1 TLSv1.1 TLSv1.2 PFS',
20
+ 'TLSv1.1 TLSv1.2',
21
+ 'TLSv1.1 TLSv1.2 PFS',
22
+ 'TLSv1.2',
23
+ 'TLSv1.2 PFS',
24
+ 'TLSv1.2 PFS TLSv1.3',
25
+ 'TLSv1.3'
26
+ ].freeze
27
+
28
+ ALB_PROTOCOL_VALUES = SSL_PROTOCOL_VALUES
29
+
30
+ ELB_PROTOCOL_VALUES =
31
+ SSL_PROTOCOL_VALUES.reject { |v| v.include?(' PFS') }.freeze
32
+
33
+ SSL_PROTOCOL_ALB_DESC = (
34
+ 'Specify the allowed SSL protocols. Valid options: ' +
35
+ ALB_PROTOCOL_VALUES.map { |v| "\"#{v}\"" }.join(', ') +
36
+ '. PFS options require an HTTPS (ALB) endpoint. ' \
37
+ 'Use "default" to reset to the platform default'
38
+ ).freeze
39
+
40
+ SSL_PROTOCOL_ELB_DESC = (
41
+ 'Specify the allowed SSL protocols. Valid options: ' +
42
+ ELB_PROTOCOL_VALUES.map { |v| "\"#{v}\"" }.join(', ') +
43
+ '. Use "default" to reset to the platform default'
44
+ ).freeze
45
+
17
46
  def initialize(&block)
18
47
  FLAGS.each { |f| instance_variable_set("@#{f}", false) }
19
48
  instance_exec(&block) if block
@@ -53,6 +82,14 @@ module Aptible
53
82
  )
54
83
  end
55
84
 
85
+ option(
86
+ :idle_timeout,
87
+ type: :string,
88
+ desc: 'Timeout (seconds) to enforce idle timeouts while ' \
89
+ 'sending and receiving responses. Use "default" to ' \
90
+ 'reset to the platform default'
91
+ )
92
+
56
93
  if builder.alb?
57
94
  option(
58
95
  :load_balancing_algorithm_type,
@@ -69,6 +106,50 @@ module Aptible
69
106
  desc: "Share this Endpoint's load balancer with other " \
70
107
  'Endpoints'
71
108
  )
109
+
110
+ option(
111
+ :force_ssl,
112
+ type: :boolean,
113
+ desc: 'Redirect all HTTP requests to HTTPS, and ' \
114
+ 'enable the Strict-Transport-Security header (HSTS)'
115
+ )
116
+
117
+ option(
118
+ :maintenance_page_url,
119
+ type: :string,
120
+ desc: 'The URL of a maintenance page to cache and serve ' \
121
+ 'when requests time out, or your app is unhealthy. ' \
122
+ 'Use "default" to reset to the platform default'
123
+ )
124
+
125
+ option(
126
+ :release_healthcheck_timeout,
127
+ type: :string,
128
+ desc: 'Timeout (seconds) to wait for your app to ' \
129
+ 'respond to a release health check. Use "default" ' \
130
+ 'to reset to the platform default'
131
+ )
132
+
133
+ option(
134
+ :show_elb_healthchecks,
135
+ type: :boolean,
136
+ desc: 'Show all runtime health check requets in the ' \
137
+ "endpoint's logs"
138
+ )
139
+
140
+ option(
141
+ :ssl_protocols_override,
142
+ type: :string,
143
+ desc: SSL_PROTOCOL_ALB_DESC
144
+ )
145
+
146
+ option(
147
+ :strict_health_checks,
148
+ type: :boolean,
149
+ desc: 'Require containers to respond to health checks ' \
150
+ 'with a 200 OK HTTP response.'
151
+ )
152
+
72
153
  end
73
154
  end
74
155
 
@@ -128,6 +209,27 @@ module Aptible
128
209
  desc: 'The fingerprint of an existing Certificate to use ' \
129
210
  'on this Endpoint'
130
211
  )
212
+
213
+ unless builder.alb?
214
+ option(
215
+ :ssl_protocols_override,
216
+ type: :string,
217
+ desc: SSL_PROTOCOL_ELB_DESC
218
+ )
219
+
220
+ option(
221
+ :ssl_ciphers_override,
222
+ type: :string,
223
+ desc: 'Specify the allowed SSL ciphers. ' \
224
+ 'Use "default" to reset to the platform default'
225
+ )
226
+
227
+ option(
228
+ :disable_weak_cipher_suites,
229
+ type: :boolean,
230
+ desc: 'Block the SSLv3 protocol and RC4 ciphers'
231
+ )
232
+ end
131
233
  end
132
234
  end
133
235
  end
@@ -137,6 +239,7 @@ module Aptible
137
239
  verify_option_conflicts(options)
138
240
 
139
241
  params = {}
242
+ settings = {}
140
243
 
141
244
  params[:ip_whitelist] = options.delete(:ip_whitelist) do
142
245
  create? ? [] : nil
@@ -203,6 +306,56 @@ module Aptible
203
306
  params[:shared] = options.delete(:shared)
204
307
  end
205
308
 
309
+ if (proto = options[:ssl_protocols_override]) &&
310
+ proto != 'default'
311
+ unless ALB_PROTOCOL_VALUES.include?(proto)
312
+ raise Thor::Error,
313
+ "Invalid --ssl-protocols-override: \"#{proto}\". " \
314
+ "Valid options are: #{ALB_PROTOCOL_VALUES.join(', ')}"
315
+ end
316
+
317
+ if !ELB_PROTOCOL_VALUES.include?(proto) && !alb?
318
+ raise Thor::Error,
319
+ "Invalid --ssl-protocols-override: \"#{proto}\". " \
320
+ 'PFS options are only valid for an HTTPS (ALB) endpoint. ' \
321
+ "Valid options are: #{ELB_PROTOCOL_VALUES.join(', ')}"
322
+ end
323
+ end
324
+
325
+ vhost_settings = %i(
326
+ idle_timeout
327
+ maintenance_page_url
328
+ release_healthcheck_timeout
329
+ ssl_protocols_override
330
+ ssl_ciphers_override
331
+ )
332
+
333
+ vhost_settings.each do |key|
334
+ val = options.delete(key)
335
+ next if val.nil?
336
+
337
+ settings[key.to_s.upcase] = case val
338
+ when 'default'
339
+ ''
340
+ else
341
+ val
342
+ end
343
+ end
344
+
345
+ boolean_vhost_settings = %i(
346
+ force_ssl
347
+ show_elb_healthchecks
348
+ strict_health_checks
349
+ disable_weak_cipher_suites
350
+ )
351
+
352
+ boolean_vhost_settings.each do |key|
353
+ value = options.delete(key)
354
+ next if value.nil?
355
+
356
+ settings[key.to_s.upcase] = value.to_s
357
+ end
358
+
206
359
  options.delete(:environment)
207
360
 
208
361
  # NOTE: This is here to ensure that specs don't test for options
@@ -210,7 +363,7 @@ module Aptible
210
363
  # this.
211
364
  raise "Unexpected options: #{options}" if options.any?
212
365
 
213
- params.delete_if { |_, v| v.nil? }
366
+ [params.delete_if { |_, v| v.nil? }, settings]
214
367
  end
215
368
 
216
369
  FLAGS.each do |f|
@@ -2,8 +2,11 @@ module Aptible
2
2
  module CLI
3
3
  module Helpers
4
4
  module Vhost
5
- def provision_vhost_and_explain(service, vhost)
6
- op = vhost.create_operation!(type: 'provision')
5
+ def provision_vhost_and_explain(service, vhost, settings)
6
+ op = vhost.create_operation!(
7
+ type: 'provision',
8
+ **(settings.empty? ? {} : { settings: settings })
9
+ )
7
10
  attach_to_operation_logs(op)
8
11
 
9
12
  Formatter.render(Renderer.current) do |root|
@@ -7,7 +7,9 @@ module Aptible
7
7
  POST_PROCESSED_KEYS = {
8
8
  'Tls' => 'TLS',
9
9
  'Dns' => 'DNS',
10
- 'Ip' => 'IP'
10
+ 'Ip' => 'IP',
11
+ 'Ssl' => 'SSL',
12
+ 'Elb' => 'ELB'
11
13
  }.freeze
12
14
 
13
15
  def visit(node, io)
@@ -213,6 +213,12 @@ module Aptible
213
213
 
214
214
  node.value('internal', vhost.internal)
215
215
 
216
+ unless vhost.current_setting.nil?
217
+ vhost.current_setting.settings.each do |k, v|
218
+ node.value(k.downcase, v)
219
+ end
220
+ end
221
+
216
222
  ip_whitelist = if vhost.ip_whitelist.any?
217
223
  vhost.ip_whitelist.join(' ')
218
224
  else
@@ -67,7 +67,7 @@ module Aptible
67
67
 
68
68
  account = destination_account || backup.account
69
69
 
70
- database = databases_from_handle(handle, account).first
70
+ database = database_from_handle(handle, account)
71
71
  render_database(database, account)
72
72
  end
73
73
 
@@ -27,13 +27,18 @@ module Aptible
27
27
  service = database.service
28
28
  raise Thor::Error, 'Database is not provisioned' if service.nil?
29
29
 
30
+ prepared_params, settings = database_create_flags.prepare(
31
+ database.account,
32
+ options
33
+ )
34
+
30
35
  vhost = service.create_vhost!(
31
36
  type: 'tcp',
32
37
  platform: 'elb',
33
- **database_create_flags.prepare(database.account, options)
38
+ **prepared_params
34
39
  )
35
40
 
36
- provision_vhost_and_explain(service, vhost)
41
+ provision_vhost_and_explain(service, vhost, settings)
37
42
  end
38
43
 
39
44
  database_modify_flags = Helpers::Vhost::OptionSetBuilder.new do
@@ -49,9 +54,14 @@ module Aptible
49
54
 
50
55
  database = ensure_database(options.merge(db: options[:database]))
51
56
  vhost = find_vhost(each_service(database), hostname)
52
- vhost.update!(**database_modify_flags.prepare(database.account,
53
- options))
54
- provision_vhost_and_explain(vhost.service, vhost)
57
+
58
+ prepared_params, settings = database_modify_flags.prepare(
59
+ database.account,
60
+ options
61
+ )
62
+
63
+ vhost.update!(**prepared_params)
64
+ provision_vhost_and_explain(vhost.service, vhost, settings)
55
65
  end
56
66
 
57
67
  tcp_create_flags = Helpers::Vhost::OptionSetBuilder.new do
@@ -246,18 +256,26 @@ module Aptible
246
256
  no_commands do
247
257
  def create_app_vhost(flags, options, process_type, **attrs)
248
258
  service = ensure_service(options, process_type)
259
+
260
+ prepared_params, settings =
261
+ flags.prepare(service.account, options)
262
+
249
263
  vhost = service.create_vhost!(
250
- **flags.prepare(service.account, options),
264
+ **prepared_params,
251
265
  **attrs
252
266
  )
253
- provision_vhost_and_explain(service, vhost)
267
+ provision_vhost_and_explain(service, vhost, settings)
254
268
  end
255
269
 
256
270
  def modify_app_vhost(flags, options, hostname)
257
271
  app = ensure_app(options)
258
272
  vhost = find_vhost(each_service(app), hostname)
259
- vhost.update!(**flags.prepare(vhost.service.account, options))
260
- provision_vhost_and_explain(vhost.service, vhost)
273
+
274
+ prepared_params, settings =
275
+ flags.prepare(vhost.service.account, options)
276
+
277
+ vhost.update!(**prepared_params)
278
+ provision_vhost_and_explain(vhost.service, vhost, settings)
261
279
  end
262
280
  end
263
281
  end
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.26.4'.freeze
3
+ VERSION = '0.26.6'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Helpers::Vhost::OptionSetBuilder do
4
+ def register_options(builder)
5
+ klass = Class.new(Thor) { include Aptible::CLI::Helpers::App }
6
+ builder.declare_options(klass)
7
+ klass.instance_variable_get(:@method_options)
8
+ end
9
+
10
+ describe '--ssl-protocols-override option description' do
11
+ context 'HTTPS endpoints (ALB, alb! flag set)' do
12
+ let(:builder) do
13
+ described_class.new do
14
+ app!
15
+ tls!
16
+ alb!
17
+ end
18
+ end
19
+
20
+ it 'includes PFS values' do
21
+ desc = register_options(builder)[:ssl_protocols_override].description
22
+ expect(desc).to include('PFS')
23
+ end
24
+ end
25
+
26
+ context 'TLS endpoints (ELB, tls! without alb!)' do
27
+ let(:builder) do
28
+ described_class.new do
29
+ app!
30
+ tls!
31
+ end
32
+ end
33
+
34
+ it 'does not include PFS values' do
35
+ desc = register_options(builder)[:ssl_protocols_override].description
36
+ expect(desc).not_to include('PFS')
37
+ end
38
+
39
+ it 'is still present' do
40
+ expect(register_options(builder)).to have_key(:ssl_protocols_override)
41
+ end
42
+ end
43
+
44
+ context 'gRPC endpoints (ELB, tls! without alb!)' do
45
+ let(:builder) do
46
+ described_class.new do
47
+ app!
48
+ port!
49
+ tls!
50
+ end
51
+ end
52
+
53
+ it 'does not include PFS values' do
54
+ desc = register_options(builder)[:ssl_protocols_override].description
55
+ expect(desc).not_to include('PFS')
56
+ end
57
+ end
58
+
59
+ context 'TCP endpoints (no tls! flag)' do
60
+ let(:builder) do
61
+ described_class.new do
62
+ app!
63
+ ports!
64
+ end
65
+ end
66
+
67
+ it 'is absent' do
68
+ expect(register_options(builder)).not_to have_key(:ssl_protocols_override)
69
+ end
70
+ end
71
+ end
72
+
73
+ describe 'SSL_PROTOCOL_ALB_DESC' do
74
+ subject { described_class::SSL_PROTOCOL_ALB_DESC }
75
+
76
+ it 'lists all PFS protocol values' do
77
+ pfs_values = described_class::SSL_PROTOCOL_VALUES.select { |v| v.include?('PFS') }
78
+ pfs_values.each { |v| is_expected.to include(v) }
79
+ end
80
+ end
81
+
82
+ describe 'SSL_PROTOCOL_ELB_DESC' do
83
+ subject { described_class::SSL_PROTOCOL_ELB_DESC }
84
+
85
+ it 'contains no PFS values' do
86
+ is_expected.not_to include('PFS')
87
+ end
88
+
89
+ it 'lists all non-PFS protocol values' do
90
+ non_pfs_values = described_class::SSL_PROTOCOL_VALUES.reject { |v| v.include?('PFS') }
91
+ non_pfs_values.each { |v| is_expected.to include(v) }
92
+ end
93
+ end
94
+ end
@@ -25,6 +25,9 @@ describe Aptible::CLI::ResourceFormatter do
25
25
  shared: false
26
26
  )
27
27
 
28
+ vhost.current_configuration = Fabricate(:setting, settings: {},
29
+ vhost: vhost)
30
+
28
31
  expected = [
29
32
  'Id: 12',
30
33
  'Hostname: foo.io',