leap_cli 1.5.6 → 1.6.2

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 (62) hide show
  1. data/bin/leap +29 -6
  2. data/lib/leap/platform.rb +36 -1
  3. data/lib/leap_cli/commands/ca.rb +97 -20
  4. data/lib/leap_cli/commands/compile.rb +49 -8
  5. data/lib/leap_cli/commands/db.rb +13 -4
  6. data/lib/leap_cli/commands/deploy.rb +138 -29
  7. data/lib/leap_cli/commands/env.rb +76 -0
  8. data/lib/leap_cli/commands/facts.rb +10 -3
  9. data/lib/leap_cli/commands/inspect.rb +2 -2
  10. data/lib/leap_cli/commands/list.rb +10 -10
  11. data/lib/leap_cli/commands/node.rb +7 -132
  12. data/lib/leap_cli/commands/node_init.rb +169 -0
  13. data/lib/leap_cli/commands/pre.rb +4 -27
  14. data/lib/leap_cli/commands/ssh.rb +152 -0
  15. data/lib/leap_cli/commands/test.rb +22 -13
  16. data/lib/leap_cli/commands/user.rb +12 -4
  17. data/lib/leap_cli/commands/vagrant.rb +4 -4
  18. data/lib/leap_cli/config/filter.rb +175 -0
  19. data/lib/leap_cli/config/manager.rb +130 -61
  20. data/lib/leap_cli/config/node.rb +32 -0
  21. data/lib/leap_cli/config/object.rb +69 -44
  22. data/lib/leap_cli/config/object_list.rb +44 -39
  23. data/lib/leap_cli/config/secrets.rb +24 -12
  24. data/lib/leap_cli/config/tag.rb +7 -0
  25. data/lib/{core_ext → leap_cli/core_ext}/boolean.rb +0 -0
  26. data/lib/{core_ext → leap_cli/core_ext}/hash.rb +0 -0
  27. data/lib/{core_ext → leap_cli/core_ext}/json.rb +0 -0
  28. data/lib/{core_ext → leap_cli/core_ext}/nil.rb +0 -0
  29. data/lib/{core_ext → leap_cli/core_ext}/string.rb +0 -0
  30. data/lib/leap_cli/core_ext/yaml.rb +29 -0
  31. data/lib/leap_cli/exceptions.rb +24 -0
  32. data/lib/leap_cli/leapfile.rb +60 -10
  33. data/lib/{lib_ext → leap_cli/lib_ext}/capistrano_connections.rb +0 -0
  34. data/lib/{lib_ext → leap_cli/lib_ext}/gli.rb +0 -0
  35. data/lib/leap_cli/log.rb +1 -1
  36. data/lib/leap_cli/logger.rb +18 -1
  37. data/lib/leap_cli/markdown_document_listener.rb +1 -1
  38. data/lib/leap_cli/override/json.rb +11 -0
  39. data/lib/leap_cli/path.rb +20 -6
  40. data/lib/leap_cli/remote/leap_plugin.rb +2 -2
  41. data/lib/leap_cli/remote/puppet_plugin.rb +1 -1
  42. data/lib/leap_cli/remote/rsync_plugin.rb +1 -1
  43. data/lib/leap_cli/remote/tasks.rb +1 -1
  44. data/lib/leap_cli/ssh_key.rb +63 -1
  45. data/lib/leap_cli/util/remote_command.rb +19 -2
  46. data/lib/leap_cli/util/secret.rb +1 -1
  47. data/lib/leap_cli/util/x509.rb +3 -2
  48. data/lib/leap_cli/util.rb +11 -3
  49. data/lib/leap_cli/version.rb +2 -2
  50. data/lib/leap_cli.rb +24 -14
  51. data/vendor/certificate_authority/lib/certificate_authority/certificate.rb +85 -29
  52. data/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb +5 -0
  53. data/vendor/certificate_authority/lib/certificate_authority/extensions.rb +406 -41
  54. data/vendor/certificate_authority/lib/certificate_authority/key_material.rb +0 -34
  55. data/vendor/certificate_authority/lib/certificate_authority/serial_number.rb +6 -0
  56. data/vendor/certificate_authority/lib/certificate_authority/signing_request.rb +36 -1
  57. metadata +25 -24
  58. data/lib/leap_cli/commands/shell.rb +0 -89
  59. data/lib/leap_cli/config/macros.rb +0 -430
  60. data/lib/leap_cli/constants.rb +0 -7
  61. data/lib/leap_cli/requirements.rb +0 -19
  62. data/lib/lib_ext/markdown_document_listener.rb +0 -122
data/bin/leap CHANGED
@@ -1,7 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- if ARGV.include?('--debug')
4
- require 'debugger'
3
+ if ARGV.include?('--debug') || ARGV.include?('-d')
4
+ DEBUG=true
5
+ begin
6
+ require 'debugger'
7
+ rescue LoadError
8
+ end
9
+ else
10
+ DEBUG=false
5
11
  end
6
12
 
7
13
  begin
@@ -18,7 +24,6 @@ rescue LoadError
18
24
  # This allows you to run the command directly while developing the gem, and also lets you
19
25
  # run from anywhere (I like to link 'bin/leap' to /usr/local/bin/leap).
20
26
  #
21
- require 'rubygems'
22
27
  base_dir = File.expand_path('..', File.dirname(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__))
23
28
  require File.join(base_dir, 'lib','leap_cli','load_paths')
24
29
  require 'leap_cli'
@@ -27,7 +32,7 @@ end
27
32
  require 'gli'
28
33
  require 'highline'
29
34
  require 'forwardable'
30
- require 'lib_ext/gli' # our custom extensions to gli
35
+ require 'leap_cli/lib_ext/gli' # our custom extensions to gli
31
36
 
32
37
  #
33
38
  # Typically, GLI and Highline methods are loaded into the global namespace.
@@ -78,9 +83,27 @@ module LeapCli::Commands
78
83
  exit(0)
79
84
  end
80
85
 
86
+ # disable GLI error catching
87
+ ENV['GLI_DEBUG'] = "true"
88
+ def error_message(msg)
89
+ end
90
+
81
91
  # load commands and run
82
92
  commands_from('leap_cli/commands')
83
93
  ORIGINAL_ARGV = ARGV.dup
84
- exit_status = run(ARGV)
85
- exit(LeapCli::Util.exit_status || exit_status)
94
+ begin
95
+ exit_status = run(ARGV)
96
+ exit(LeapCli::Util.exit_status || exit_status)
97
+ rescue StandardError => exc
98
+ if exc.respond_to? :log
99
+ exc.log
100
+ else
101
+ puts
102
+ LeapCli.log :error, "%s: %s" % [exc.class, exc.message]
103
+ puts
104
+ end
105
+ if DEBUG
106
+ raise exc
107
+ end
108
+ end
86
109
  end
data/lib/leap/platform.rb CHANGED
@@ -16,10 +16,25 @@ module Leap
16
16
  attr_accessor :monitor_username
17
17
  attr_accessor :reserved_usernames
18
18
 
19
+ attr_accessor :hiera_path
20
+ attr_accessor :files_dir
21
+ attr_accessor :leap_dir
22
+ attr_accessor :init_path
23
+
24
+ attr_accessor :default_puppet_tags
25
+
19
26
  def define(&block)
20
- # some sanity defaults:
27
+ # some defaults:
21
28
  @reserved_usernames = []
29
+ @hiera_path = '/etc/leap/hiera.yaml'
30
+ @leap_dir = '/srv/leap'
31
+ @files_dir = '/srv/leap/files'
32
+ @init_path = '/srv/leap/initialized'
33
+ @default_puppet_tags = []
34
+
22
35
  self.instance_eval(&block)
36
+
37
+ @version ||= Versionomy.parse("0.0")
23
38
  end
24
39
 
25
40
  def version=(version)
@@ -44,10 +59,30 @@ module Leap
44
59
  # return true if the platform version is within the specified range.
45
60
  #
46
61
  def version_in_range?(range)
62
+ if range.is_a? String
63
+ range = range.split('..')
64
+ end
47
65
  minimum_platform_version = Versionomy.parse(range.first)
48
66
  maximum_platform_version = Versionomy.parse(range.last)
49
67
  @version >= minimum_platform_version && @version <= maximum_platform_version
50
68
  end
69
+
70
+ def major_version
71
+ if @version.major == 0
72
+ "#{@version.major}.#{@version.minor}"
73
+ else
74
+ @version.major
75
+ end
76
+ end
77
+
78
+ def method_missing(method, *args)
79
+ puts
80
+ puts "WARNING:"
81
+ puts " leap_cli is out of date and does not understand `#{method}`."
82
+ puts " called from: #{caller.first}"
83
+ puts " please upgrade to a newer leap_cli"
84
+ end
85
+
51
86
  end
52
87
 
53
88
  end
@@ -1,6 +1,6 @@
1
- require 'openssl'
2
- require 'certificate_authority'
3
- require 'date'
1
+ autoload :OpenSSL, 'openssl'
2
+ autoload :CertificateAuthority, 'certificate_authority'
3
+ autoload :Date, 'date'
4
4
  require 'digest/md5'
5
5
 
6
6
  module LeapCli; module Commands
@@ -36,6 +36,7 @@ module LeapCli; module Commands
36
36
 
37
37
  nodes = manager.filter!(args)
38
38
  nodes.each_node do |node|
39
+ warn_if_commercial_cert_will_soon_expire(node)
39
40
  if !node.x509.use
40
41
  remove_file!([:node_x509_key, node.name])
41
42
  remove_file!([:node_x509_cert, node.name])
@@ -81,9 +82,19 @@ module LeapCli; module Commands
81
82
  # http://www.redkestrel.co.uk/Articles/CSR.html
82
83
  #
83
84
  cert.desc "Creates a CSR for use in buying a commercial X.509 certificate."
84
- cert.long_desc "Unless specified, the CSR is created for the provider's primary domain. The properties used for this CSR come from `provider.ca.server_certificates`."
85
+ cert.long_desc "Unless specified, the CSR is created for the provider's primary domain. "+
86
+ "The properties used for this CSR come from `provider.ca.server_certificates`, "+
87
+ "but may be overridden here."
85
88
  cert.command :csr do |csr|
86
89
  csr.flag 'domain', :arg_name => 'DOMAIN', :desc => 'Specify what domain to create the CSR for.'
90
+ csr.flag ['organization', 'O'], :arg_name => 'ORGANIZATION', :desc => "Override default O in distinguished name."
91
+ csr.flag ['unit', 'OU'], :arg_name => 'UNIT', :desc => "Set OU in distinguished name."
92
+ csr.flag 'email', :arg_name => 'EMAIL', :desc => "Set emailAddress in distinguished name."
93
+ csr.flag ['locality', 'L'], :arg_name => 'LOCALITY', :desc => "Set L in distinguished name."
94
+ csr.flag ['state', 'ST'], :arg_name => 'STATE', :desc => "Set ST in distinguished name."
95
+ csr.flag ['country', 'C'], :arg_name => 'COUNTRY', :desc => "Set C in distinguished name."
96
+ csr.flag :bits, :arg_name => 'BITS', :desc => "Override default certificate bit length"
97
+ csr.flag :digest, :arg_name => 'DIGEST', :desc => "Override default signature digest"
87
98
  csr.action do |global_options,options,args|
88
99
  assert_config! 'provider.domain'
89
100
  assert_config! 'provider.name'
@@ -97,24 +108,25 @@ module LeapCli; module Commands
97
108
 
98
109
  # RSA key
99
110
  keypair = CertificateAuthority::MemoryKeyMaterial.new
100
- log :generating, "%s bit RSA key" % server_certificates.bit_size do
101
- keypair.generate_key(server_certificates.bit_size)
111
+ bit_size = (options[:bits] || server_certificates.bit_size).to_i
112
+ log :generating, "%s bit RSA key" % bit_size do
113
+ keypair.generate_key(bit_size)
102
114
  write_file! [:commercial_key, domain], keypair.private_key.to_pem
103
115
  end
104
116
 
105
117
  # CSR
106
118
  dn = CertificateAuthority::DistinguishedName.new
107
- csr = CertificateAuthority::SigningRequest.new
108
- dn.common_name = domain
109
- dn.organization = provider.name[provider.default_language]
110
- dn.country = server_certificates['country'] # optional
111
- dn.state = server_certificates['state'] # optional
112
- dn.locality = server_certificates['locality'] # optional
113
-
114
- log :generating, "CSR with commonName => '%s', organization => '%s'" % [dn.common_name, dn.organization] do
115
- csr.distinguished_name = dn
116
- csr.key_material = keypair
117
- csr.digest = server_certificates.digest
119
+ dn.common_name = domain
120
+ dn.organization = options[:organization] || provider.name[provider.default_language]
121
+ dn.ou = options[:organizational_unit] # optional
122
+ dn.email_address = options[:email] # optional
123
+ dn.country = options[:country] || server_certificates['country'] # optional
124
+ dn.state = options[:state] || server_certificates['state'] # optional
125
+ dn.locality = options[:locality] || server_certificates['locality'] # optional
126
+
127
+ digest = options[:digest] || server_certificates.digest
128
+ log :generating, "CSR with #{digest} digest and #{print_dn(dn)}" do
129
+ csr = create_csr(dn, keypair, digest)
118
130
  request = csr.to_x509_csr
119
131
  write_file! [:commercial_csr, domain], csr.to_pem
120
132
  end
@@ -191,7 +203,7 @@ module LeapCli; module Commands
191
203
  return true
192
204
  else
193
205
  cert = load_certificate_file([:node_x509_cert, node.name])
194
- if cert.not_after < months_from_yesterday(1)
206
+ if cert.not_after < months_from_yesterday(2)
195
207
  log :updating, "cert for node '#{node.name}' because it will expire soon"
196
208
  return true
197
209
  end
@@ -208,11 +220,12 @@ module LeapCli; module Commands
208
220
  ips << $1 if value =~ /^IP Address:(.*)$/
209
221
  dns_names << $1 if value =~ /^DNS:(.*)$/
210
222
  end
223
+ dns_names.sort!
211
224
  if ips.first != node.ip_address
212
225
  log :updating, "cert for node '#{node.name}' because ip_address has changed (from #{ips.first} to #{node.ip_address})"
213
226
  return true
214
227
  elsif dns_names != dns_names_for_node(node)
215
- log :updating, "cert for node '#{node.name}' because domain name aliases have changed (from #{dns_names.inspect} to #{dns_names_for_node(node).inspect})"
228
+ log :updating, "cert for node '#{node.name}' because domain name aliases have changed\n from: #{dns_names.inspect}\n to: #{dns_names_for_node(node).inspect})"
216
229
  return true
217
230
  end
218
231
  end
@@ -221,6 +234,22 @@ module LeapCli; module Commands
221
234
  return false
222
235
  end
223
236
 
237
+ def warn_if_commercial_cert_will_soon_expire(node)
238
+ dns_names_for_node(node).each do |domain|
239
+ if file_exists?([:commercial_cert, domain])
240
+ cert = load_certificate_file([:commercial_cert, domain])
241
+ path = Path.relative_path([:commercial_cert, domain])
242
+ if cert.not_after < Time.now.utc
243
+ log :error, "the commercial certificate '#{path}' has EXPIRED! " +
244
+ "You should renew it with `leap cert csr --domain #{domain}`."
245
+ elsif cert.not_after < months_from_yesterday(2)
246
+ log :warning, "the commercial certificate '#{path}' will expire soon. "+
247
+ "You should renew it with `leap cert csr --domain #{domain}`."
248
+ end
249
+ end
250
+ end
251
+ end
252
+
224
253
  def generate_cert_for_node(node)
225
254
  return if node.x509.use == false
226
255
 
@@ -261,6 +290,43 @@ module LeapCli; module Commands
261
290
  yield cert.key_material.private_key.to_pem, cert.to_pem
262
291
  end
263
292
 
293
+ #
294
+ # creates a CSR and returns it.
295
+ # with the correct extReq attribute so that the CA
296
+ # doens't generate certs with extensions we don't want.
297
+ #
298
+ def create_csr(dn, keypair, digest)
299
+ csr = CertificateAuthority::SigningRequest.new
300
+ csr.distinguished_name = dn
301
+ csr.key_material = keypair
302
+ csr.digest = digest
303
+
304
+ # define extensions manually (library doesn't support setting these on CSRs)
305
+ extensions = []
306
+ extensions << CertificateAuthority::Extensions::BasicConstraints.new.tap {|basic|
307
+ basic.ca = false
308
+ }
309
+ extensions << CertificateAuthority::Extensions::KeyUsage.new.tap {|keyusage|
310
+ keyusage.usage = ["digitalSignature", "keyEncipherment"]
311
+ }
312
+ extensions << CertificateAuthority::Extensions::ExtendedKeyUsage.new.tap {|extkeyusage|
313
+ extkeyusage.usage = [ "serverAuth"]
314
+ }
315
+
316
+ # convert extensions to attribute 'extReq'
317
+ # aka "Requested Extensions"
318
+ factory = OpenSSL::X509::ExtensionFactory.new
319
+ attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence(
320
+ extensions.map{|e| factory.create_ext(e.openssl_identifier, e.to_s, e.critical)}
321
+ )])
322
+ attrs = [
323
+ OpenSSL::X509::Attribute.new("extReq", attrval),
324
+ ]
325
+ csr.attributes = attrs
326
+
327
+ return csr
328
+ end
329
+
264
330
  def ca_root
265
331
  @ca_root ||= begin
266
332
  load_certificate_file(:ca_cert, :ca_key)
@@ -381,8 +447,10 @@ module LeapCli; module Commands
381
447
  names = [node.domain.internal, node.domain.full]
382
448
  if node['dns'] && node.dns['aliases'] && node.dns.aliases.any?
383
449
  names += node.dns.aliases
384
- names.compact!
385
450
  end
451
+ names.compact!
452
+ names.sort!
453
+ names.uniq!
386
454
  return names
387
455
  end
388
456
 
@@ -403,6 +471,15 @@ module LeapCli; module Commands
403
471
  cert_serial_number(domain_name).to_s(36)
404
472
  end
405
473
 
474
+ # prints CertificateAuthority::DistinguishedName fields
475
+ def print_dn(dn)
476
+ fields = {}
477
+ [:common_name, :locality, :state, :country, :organization, :organizational_unit, :email_address].each do |attr|
478
+ fields[attr] = dn.send(attr) if dn.send(attr)
479
+ end
480
+ fields.inspect
481
+ end
482
+
406
483
  ##
407
484
  ## TIME HELPERS
408
485
  ##
@@ -3,11 +3,20 @@ module LeapCli
3
3
  module Commands
4
4
 
5
5
  desc "Compile generated files."
6
- command :compile do |c|
6
+ command [:compile, :c] do |c|
7
7
  c.desc 'Compiles node configuration files into hiera files used for deployment.'
8
+ c.arg_name 'ENVIRONMENT', :optional => true
8
9
  c.command :all do |all|
9
10
  all.action do |global_options,options,args|
10
- compile_hiera_files
11
+ environment = args.first
12
+ if !LeapCli.leapfile.environment.nil? && !environment.nil? && environment != LeapCli.leapfile.environment
13
+ bail! "You cannot specify an ENVIRONMENT argument while the environment is pinned."
14
+ end
15
+ if environment && manager.environment_names.include?(environment)
16
+ compile_hiera_files(manager.filter([environment]))
17
+ else
18
+ compile_hiera_files(manager.filter)
19
+ end
11
20
  end
12
21
  end
13
22
 
@@ -29,7 +38,10 @@ module LeapCli
29
38
 
30
39
  # export generated files
31
40
  manager.export_nodes(nodes)
32
- manager.export_secrets(nodes.nil?) # only do a "clean" export if we are examining all the nodes
41
+ # a "clean" export of secrets will also remove keys that are no longer used,
42
+ # but this should not be done if we are not examining all possible nodes.
43
+ clean_export = nodes.nil?
44
+ manager.export_secrets(clean_export)
33
45
  end
34
46
 
35
47
  def update_compiled_ssh_configs
@@ -50,14 +62,16 @@ module LeapCli
50
62
  # keys, and every monitor node has a copy of the private monitor key.
51
63
  #
52
64
  def generate_monitor_ssh_keys
53
- priv_key_file = :monitor_priv_key
54
- pub_key_file = :monitor_pub_key
65
+ priv_key_file = path(:monitor_priv_key)
66
+ pub_key_file = path(:monitor_pub_key)
55
67
  unless file_exists?(priv_key_file, pub_key_file)
56
- cmd = %(ssh-keygen -N '' -C 'monitor' -t ecdsa -b 521 -f '%s') % path(priv_key_file)
68
+ ensure_dir(File.dirname(priv_key_file))
69
+ ensure_dir(File.dirname(pub_key_file))
70
+ cmd = %(ssh-keygen -N '' -C 'monitor' -t rsa -b 4096 -f '%s') % priv_key_file
57
71
  assert_run! cmd
58
72
  if file_exists?(priv_key_file, pub_key_file)
59
- log :created, path(priv_key_file)
60
- log :created, path(pub_key_file)
73
+ log :created, priv_key_file
74
+ log :created, pub_key_file
61
75
  else
62
76
  log :failed, 'to create monitor ssh keys'
63
77
  end
@@ -75,6 +89,9 @@ module LeapCli
75
89
  if keys.empty?
76
90
  bail! "You must have at least one public SSH user key configured in order to proceed. See `leap help add-user`."
77
91
  end
92
+ if file_exists?(path(:monitor_pub_key))
93
+ keys << path(:monitor_pub_key)
94
+ end
78
95
  keys.sort.each do |keyfile|
79
96
  ssh_type, ssh_key = File.read(keyfile).strip.split(" ")
80
97
  buffer << ssh_type
@@ -87,6 +104,30 @@ module LeapCli
87
104
  write_file!(:authorized_keys, buffer.string)
88
105
  end
89
106
 
107
+ #
108
+ # generates the known_hosts file.
109
+ #
110
+ # we do a 'late' binding on the hostnames and ip part of the ssh pub key record in order to allow
111
+ # for the possibility that the hostnames or ip has changed in the node configuration.
112
+ #
113
+ def update_known_hosts
114
+ buffer = StringIO.new
115
+ buffer << "#\n"
116
+ buffer << "# This file is automatically generated by the command `leap`. You should NOT modify this file.\n"
117
+ buffer << "# Instead, rerun `leap node init` on whatever node is causing SSH problems.\n"
118
+ buffer << "#\n"
119
+ manager.nodes.keys.sort.each do |node_name|
120
+ node = manager.nodes[node_name]
121
+ hostnames = [node.name, node.domain.internal, node.domain.full, node.ip_address].join(',')
122
+ pub_key = read_file([:node_ssh_pub_key,node.name])
123
+ if pub_key
124
+ buffer << [hostnames, pub_key].join(' ')
125
+ buffer << "\n"
126
+ end
127
+ end
128
+ write_file!(:known_hosts, buffer.string)
129
+ end
130
+
90
131
  ##
91
132
  ## ZONE FILE
92
133
  ##
@@ -2,14 +2,23 @@ module LeapCli; module Commands
2
2
 
3
3
  desc 'Database commands.'
4
4
  command :db do |db|
5
- db.desc 'Destroy all the databases.'
5
+ db.desc 'Destroy all the databases. If present, limit to FILTER nodes.'
6
+ db.arg_name 'FILTER', :optional => true
6
7
  db.command :destroy do |destroy|
7
8
  destroy.action do |global_options,options,args|
8
9
  say 'You are about to permanently destroy all database data.'
9
10
  return unless agree("Continue? ")
10
- nodes = manager.nodes[:services => 'couchdb']
11
- ssh_connect(nodes, connect_options(options)) do |ssh|
12
- ssh.run('/etc/init.d/bigcouch stop && test ! -z "$(ls /opt/bigcouch/var/lib/ 2> /dev/null)" && rm -r /opt/bigcouch/var/lib/* && echo "db destroyed" || echo "db already destroyed"')
11
+ nodes = manager.filter(args)
12
+ if nodes.any?
13
+ nodes = nodes[:services => 'couchdb']
14
+ end
15
+ if nodes.any?
16
+ ssh_connect(nodes, connect_options(options)) do |ssh|
17
+ ssh.run('/etc/init.d/bigcouch stop && test ! -z "$(ls /opt/bigcouch/var/lib/ 2> /dev/null)" && rm -r /opt/bigcouch/var/lib/* && echo "db destroyed" || echo "db already destroyed"')
18
+ ssh.run('grep ^seq_file /etc/leap/tapicero.yaml | cut -f2 -d\" | xargs rm -v')
19
+ end
20
+ else
21
+ say 'No nodes'
13
22
  end
14
23
  end
15
24
  end