cheffish 4.0.0 → 4.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -8
  3. data/Rakefile +24 -12
  4. data/cheffish.gemspec +15 -15
  5. data/lib/chef/resource/chef_acl.rb +63 -63
  6. data/lib/chef/resource/chef_client.rb +9 -9
  7. data/lib/chef/resource/chef_container.rb +9 -9
  8. data/lib/chef/resource/chef_data_bag.rb +9 -9
  9. data/lib/chef/resource/chef_data_bag_item.rb +27 -27
  10. data/lib/chef/resource/chef_environment.rb +21 -22
  11. data/lib/chef/resource/chef_group.rb +19 -19
  12. data/lib/chef/resource/chef_mirror.rb +32 -17
  13. data/lib/chef/resource/chef_node.rb +14 -14
  14. data/lib/chef/resource/chef_organization.rb +29 -30
  15. data/lib/chef/resource/chef_resolved_cookbooks.rb +7 -7
  16. data/lib/chef/resource/chef_role.rb +25 -22
  17. data/lib/chef/resource/chef_user.rb +13 -14
  18. data/lib/chef/resource/private_key.rb +24 -25
  19. data/lib/chef/resource/public_key.rb +6 -7
  20. data/lib/cheffish.rb +17 -17
  21. data/lib/cheffish/array_property.rb +2 -2
  22. data/lib/cheffish/base_properties.rb +3 -3
  23. data/lib/cheffish/base_resource.rb +8 -8
  24. data/lib/cheffish/basic_chef_client.rb +17 -17
  25. data/lib/cheffish/chef_actor_base.rb +8 -8
  26. data/lib/cheffish/chef_run.rb +7 -2
  27. data/lib/cheffish/chef_run_data.rb +2 -2
  28. data/lib/cheffish/chef_run_listener.rb +1 -1
  29. data/lib/cheffish/key_formatter.rb +16 -18
  30. data/lib/cheffish/merged_config.rb +5 -3
  31. data/lib/cheffish/node_properties.rb +11 -7
  32. data/lib/cheffish/recipe_dsl.rb +33 -34
  33. data/lib/cheffish/rspec.rb +3 -3
  34. data/lib/cheffish/rspec/chef_run_support.rb +13 -13
  35. data/lib/cheffish/rspec/matchers.rb +4 -4
  36. data/lib/cheffish/rspec/matchers/be_idempotent.rb +3 -3
  37. data/lib/cheffish/rspec/matchers/emit_no_warnings_or_errors.rb +3 -3
  38. data/lib/cheffish/rspec/matchers/have_updated.rb +3 -3
  39. data/lib/cheffish/rspec/recipe_run_wrapper.rb +8 -7
  40. data/lib/cheffish/rspec/repository_support.rb +6 -6
  41. data/lib/cheffish/server_api.rb +11 -11
  42. data/lib/cheffish/version.rb +1 -1
  43. data/spec/functional/fingerprint_spec.rb +12 -12
  44. data/spec/functional/merged_config_spec.rb +46 -6
  45. data/spec/functional/server_api_spec.rb +3 -3
  46. data/spec/integration/chef_acl_spec.rb +489 -489
  47. data/spec/integration/chef_client_spec.rb +39 -39
  48. data/spec/integration/chef_container_spec.rb +14 -14
  49. data/spec/integration/chef_data_bag_item_spec.rb +9 -9
  50. data/spec/integration/chef_group_spec.rb +219 -219
  51. data/spec/integration/chef_mirror_spec.rb +228 -228
  52. data/spec/integration/chef_node_spec.rb +511 -511
  53. data/spec/integration/chef_organization_spec.rb +126 -126
  54. data/spec/integration/chef_role_spec.rb +33 -33
  55. data/spec/integration/chef_user_spec.rb +37 -37
  56. data/spec/integration/private_key_spec.rb +154 -154
  57. data/spec/integration/recipe_dsl_spec.rb +10 -10
  58. data/spec/integration/rspec/converge_spec.rb +49 -49
  59. data/spec/support/key_support.rb +6 -6
  60. data/spec/support/spec_support.rb +3 -3
  61. data/spec/unit/get_private_key_spec.rb +19 -19
  62. data/spec/unit/recipe_run_wrapper_spec.rb +4 -4
  63. metadata +3 -3
@@ -1,7 +1,7 @@
1
- require 'openssl/cipher'
2
- require 'cheffish/base_resource'
3
- require 'openssl'
4
- require 'cheffish/key_formatter'
1
+ require "openssl/cipher"
2
+ require "cheffish/base_resource"
3
+ require "openssl"
4
+ require "cheffish/key_formatter"
5
5
 
6
6
  class Chef
7
7
  class Resource
@@ -23,7 +23,6 @@ class Chef
23
23
  Chef::Log.debug("Overloading #{resource_name}.load_prior_resource with NOOP")
24
24
  end
25
25
 
26
-
27
26
  action :create do
28
27
  if !new_source_key
29
28
  raise "No source key specified"
@@ -68,10 +67,10 @@ class Chef
68
67
  end
69
68
 
70
69
  if source_key.private?
71
- @new_source_key_publicity = 'private'
70
+ @new_source_key_publicity = "private"
72
71
  source_key.public_key
73
72
  else
74
- @new_source_key_publicity = 'public'
73
+ @new_source_key_publicity = "public"
75
74
  source_key
76
75
  end
77
76
  end
data/lib/cheffish.rb CHANGED
@@ -10,8 +10,8 @@ module Cheffish
10
10
  :chef_server_url => config[:chef_server_url],
11
11
  :options => {
12
12
  :client_name => config[:node_name],
13
- :signing_key_filename => config[:client_key]
14
- }
13
+ :signing_key_filename => config[:client_key],
14
+ },
15
15
  }
16
16
  end
17
17
 
@@ -19,7 +19,7 @@ module Cheffish
19
19
  # Pin the server api version to 0 until https://github.com/chef/cheffish/issues/56
20
20
  # gets the correct compatibility fix.
21
21
  chef_server[:options] ||= {}
22
- chef_server[:options].merge!(api_version: "0")
22
+ chef_server[:options][:api_version] = "0"
23
23
  Cheffish::ServerAPI.new(chef_server[:chef_server_url], chef_server[:options])
24
24
  end
25
25
 
@@ -32,7 +32,7 @@ module Cheffish
32
32
  end
33
33
 
34
34
  def self.load_chef_config(chef_config = Chef::Config)
35
- if ::Gem::Version.new(::Chef::VERSION) >= ::Gem::Version.new('12.0.0')
35
+ if ::Gem::Version.new(::Chef::VERSION) >= ::Gem::Version.new("12.0.0")
36
36
  chef_config.config_file = ::Chef::Knife.chef_config_dir
37
37
  else
38
38
  chef_config.config_file = ::Chef::Knife.locate_config_file
@@ -50,7 +50,7 @@ module Cheffish
50
50
  rescue Exception => error
51
51
  Chef::Log.fatal("Configuration error #{error.class}: #{error.message}")
52
52
  filtered_trace = error.backtrace.grep(/#{Regexp.escape(config_file_path)}/)
53
- filtered_trace.each {|line| Chef::Log.fatal(" " + line )}
53
+ filtered_trace.each { |line| Chef::Log.fatal(" " + line ) }
54
54
  Chef::Application.fatal!("Aborting due to error in '#{config_file_path}'", 2)
55
55
  end
56
56
  end
@@ -65,7 +65,7 @@ module Cheffish
65
65
  Chef::Config.chef_repo_path = Chef::Config.find_chef_repo_path(Dir.pwd)
66
66
  end
67
67
  begin
68
- require 'chef/local_mode'
68
+ require "chef/local_mode"
69
69
  Chef::LocalMode.with_server_connectivity(&block)
70
70
 
71
71
  rescue LoadError
@@ -100,8 +100,8 @@ module Cheffish
100
100
  next unless File.exist?(private_key_path)
101
101
  Dir.entries(private_key_path).sort.each do |key|
102
102
  ext = File.extname(key)
103
- if key == name || ext == '' || ext == '.pem'
104
- key_name = key[0..-(ext.length+1)]
103
+ if key == name || ext == "" || ext == ".pem"
104
+ key_name = key[0..-(ext.length + 1)]
105
105
  if key_name == name || key == name
106
106
  Chef::Log.info("Reading key #{name} from file #{private_key_path}/#{key}")
107
107
  return [ IO.read("#{private_key_path}/#{key}"), "#{private_key_path}/#{key}" ]
@@ -120,12 +120,12 @@ end
120
120
 
121
121
  # Include all recipe objects so require 'cheffish' brings in the whole recipe DSL
122
122
 
123
- require 'chef/run_list/run_list_item'
124
- require 'cheffish/basic_chef_client'
125
- require 'cheffish/server_api'
126
- require 'chef/knife'
127
- require 'chef/config_fetcher'
128
- require 'chef/log'
129
- require 'chef/application'
130
- require 'cheffish/recipe_dsl'
131
- require 'cheffish/node_properties'
123
+ require "chef/run_list/run_list_item"
124
+ require "cheffish/basic_chef_client"
125
+ require "cheffish/server_api"
126
+ require "chef/knife"
127
+ require "chef/config_fetcher"
128
+ require "chef/log"
129
+ require "chef/application"
130
+ require "cheffish/recipe_dsl"
131
+ require "cheffish/node_properties"
@@ -1,4 +1,4 @@
1
- require 'chef/property'
1
+ require "chef/property"
2
2
 
3
3
  module Cheffish
4
4
  # A typical array property. Defaults to [], accepts multiple args to setter, accumulates values.
@@ -12,7 +12,7 @@ module Cheffish
12
12
 
13
13
  # Support my_property 'a', 'b', 'c'; my_property 'a'; and my_property ['a', 'b']
14
14
  def emit_dsl
15
- declared_in.class_eval(<<-EOM, __FILE__, __LINE__+1)
15
+ declared_in.class_eval(<<-EOM, __FILE__, __LINE__ + 1)
16
16
  def #{name}(*values)
17
17
  property = self.class.properties[#{name.inspect}]
18
18
  if values.empty?
@@ -1,6 +1,6 @@
1
- require 'chef/mixin/properties'
2
- require 'cheffish/array_property'
3
- require 'cheffish'
1
+ require "chef/mixin/properties"
2
+ require "cheffish/array_property"
3
+ require "cheffish"
4
4
 
5
5
  module Cheffish
6
6
  module BaseProperties
@@ -1,5 +1,5 @@
1
- require 'chef/resource'
2
- require 'cheffish/base_properties'
1
+ require "chef/resource"
2
+ require "cheffish/base_properties"
3
3
 
4
4
  module Cheffish
5
5
  class BaseResource < Chef::Resource
@@ -84,7 +84,7 @@ module Cheffish
84
84
  data_handler.normalize(json, fake_entry)
85
85
  end
86
86
 
87
- def json_differences(old_json, new_json, print_values=true, name = '', result = nil)
87
+ def json_differences(old_json, new_json, print_values = true, name = "", result = nil)
88
88
  result ||= []
89
89
  json_differences_internal(old_json, new_json, print_values, name, result)
90
90
  result
@@ -97,7 +97,7 @@ module Cheffish
97
97
  if old_json.has_key?(new_key)
98
98
  removed_keys.delete(new_key)
99
99
  if new_value != old_json[new_key]
100
- json_differences_internal(old_json[new_key], new_value, print_values, name == '' ? new_key : "#{name}.#{new_key}", result)
100
+ json_differences_internal(old_json[new_key], new_value, print_values, name == "" ? new_key : "#{name}.#{new_key}", result)
101
101
  end
102
102
  else
103
103
  if print_values
@@ -136,11 +136,11 @@ module Cheffish
136
136
  modifiers.each do |path, value|
137
137
  path = [path] if !path.kind_of?(Array)
138
138
  path = path.map { |path_part| path_part.to_s }
139
- parent = 0.upto(path.size-2).inject(json) do |hash, index|
139
+ parent = 0.upto(path.size - 2).inject(json) do |hash, index|
140
140
  if hash.nil?
141
141
  nil
142
142
  elsif !hash.is_a?(Hash)
143
- raise "Attempt to set #{path} to #{value} when #{path[0..index-1]} is not a hash"
143
+ raise "Attempt to set #{path} to #{value} when #{path[0..index - 1]} is not a hash"
144
144
  else
145
145
  hash[path[index]]
146
146
  end
@@ -181,7 +181,7 @@ module Cheffish
181
181
  result = []
182
182
  add_to_run_list_index = 0
183
183
  run_list_index = 0
184
- while run_list_index < run_list.run_list_items.size do
184
+ while run_list_index < run_list.run_list_items.size
185
185
  # See if the desired run list has this item
186
186
  found_desired = add_to_run_list.index { |item| same_run_list_item(item, run_list[run_list_index]) }
187
187
  if found_desired
@@ -192,7 +192,7 @@ module Cheffish
192
192
  # be X, A, B, Y, Z.
193
193
  if found_desired >= add_to_run_list_index
194
194
  result += add_to_run_list[add_to_run_list_index..found_desired].map { |item| item.to_s }
195
- add_to_run_list_index = found_desired+1
195
+ add_to_run_list_index = found_desired + 1
196
196
  end
197
197
  else
198
198
  # If not, just copy it in
@@ -1,13 +1,13 @@
1
- require 'cheffish/version'
2
- require 'chef/dsl/recipe'
3
- require 'chef/event_dispatch/base'
4
- require 'chef/event_dispatch/dispatcher'
5
- require 'chef/node'
6
- require 'chef/run_context'
7
- require 'chef/runner'
8
- require 'forwardable'
9
- require 'chef/providers'
10
- require 'chef/resources'
1
+ require "cheffish/version"
2
+ require "chef/dsl/recipe"
3
+ require "chef/event_dispatch/base"
4
+ require "chef/event_dispatch/dispatcher"
5
+ require "chef/node"
6
+ require "chef/run_context"
7
+ require "chef/runner"
8
+ require "forwardable"
9
+ require "chef/providers"
10
+ require "chef/resources"
11
11
 
12
12
  module Cheffish
13
13
  class BasicChefClient
@@ -16,8 +16,8 @@ module Cheffish
16
16
  def initialize(node = nil, events = nil, **chef_config)
17
17
  if !node
18
18
  node = Chef::Node.new
19
- node.name 'basic_chef_client'
20
- node.automatic[:platform] = 'basic_chef_client'
19
+ node.name "basic_chef_client"
20
+ node.automatic[:platform] = "basic_chef_client"
21
21
  node.automatic[:platform_version] = Cheffish::VERSION
22
22
  end
23
23
 
@@ -25,7 +25,7 @@ module Cheffish
25
25
  @chef_config = chef_config
26
26
 
27
27
  with_chef_config do
28
- @cookbook_name = 'basic_chef_client'
28
+ @cookbook_name = "basic_chef_client"
29
29
  @event_catcher = BasicChefClientEvents.new
30
30
  dispatcher = Chef::EventDispatch::Dispatcher.new(@event_catcher)
31
31
  case events
@@ -37,7 +37,7 @@ module Cheffish
37
37
  end
38
38
  @run_context = Chef::RunContext.new(node, {}, dispatcher)
39
39
  @updated = []
40
- @cookbook_name = 'basic_chef_client'
40
+ @cookbook_name = "basic_chef_client"
41
41
  end
42
42
  end
43
43
 
@@ -58,7 +58,7 @@ module Cheffish
58
58
 
59
59
  def load_block(&block)
60
60
  with_chef_config do
61
- @recipe_name = 'block'
61
+ @recipe_name = "block"
62
62
  instance_eval(&block)
63
63
  end
64
64
  end
@@ -79,7 +79,7 @@ module Cheffish
79
79
 
80
80
  # Builds a resource sans context, which can be later used in a new client's
81
81
  # add_resource() method.
82
- def self.build_resource(type, name, created_at=nil, &resource_attrs_block)
82
+ def self.build_resource(type, name, created_at = nil, &resource_attrs_block)
83
83
  created_at ||= caller[0]
84
84
  result = BasicChefClient.new.tap do |client|
85
85
  client.with_chef_config do
@@ -129,7 +129,7 @@ module Cheffish
129
129
  # end
130
130
  begin
131
131
  deep_merge_config(chef_config, Chef::Config)
132
- block.call
132
+ yield
133
133
  ensure
134
134
  # $stdout = old_stdout if chef_config[:stdout]
135
135
  # $stderr = old_stderr if chef_config[:stderr]
@@ -1,5 +1,5 @@
1
- require 'cheffish/key_formatter'
2
- require 'cheffish/base_resource'
1
+ require "cheffish/key_formatter"
2
+ require "cheffish/base_resource"
3
3
 
4
4
  module Cheffish
5
5
  class ChefActorBase < Cheffish::BaseResource
@@ -11,7 +11,7 @@ module Cheffish
11
11
  end
12
12
 
13
13
  # Create or update the client/user
14
- current_public_key = new_json['public_key']
14
+ current_public_key = new_json["public_key"]
15
15
  differences = json_differences(current_json, new_json)
16
16
  if current_resource_exists?
17
17
  # Update the actor if it's different
@@ -19,7 +19,7 @@ module Cheffish
19
19
  description = [ "update #{actor_type} #{new_resource.name} at #{actor_path}" ] + differences
20
20
  converge_by description do
21
21
  result = rest.put("#{actor_path}/#{new_resource.name}", normalize_for_put(new_json))
22
- current_public_key, current_public_key_format = Cheffish::KeyFormatter.decode(result['public_key']) if result['public_key']
22
+ current_public_key, current_public_key_format = Cheffish::KeyFormatter.decode(result["public_key"]) if result["public_key"]
23
23
  end
24
24
  end
25
25
  else
@@ -30,7 +30,7 @@ module Cheffish
30
30
  description = [ "create #{actor_type} #{new_resource.name} at #{actor_path}" ] + differences
31
31
  converge_by description do
32
32
  result = rest.post("#{actor_path}", normalize_for_post(new_json))
33
- current_public_key, current_public_key_format = Cheffish::KeyFormatter.decode(result['public_key']) if result['public_key']
33
+ current_public_key, current_public_key_format = Cheffish::KeyFormatter.decode(result["public_key"]) if result["public_key"]
34
34
  end
35
35
  end
36
36
 
@@ -39,9 +39,9 @@ module Cheffish
39
39
  # TODO use inline_resource
40
40
  key_content = Cheffish::KeyFormatter.encode(current_public_key, { :format => new_resource.output_key_format })
41
41
  if !current_resource.output_key_path
42
- action = 'create'
42
+ action = "create"
43
43
  elsif key_content != IO.read(current_resource.output_key_path)
44
- action = 'overwrite'
44
+ action = "overwrite"
45
45
  else
46
46
  action = nil
47
47
  end
@@ -109,7 +109,7 @@ module Cheffish
109
109
 
110
110
  def augment_new_json(json)
111
111
  if new_public_key
112
- json['public_key'] = new_public_key.to_pem
112
+ json["public_key"] = new_public_key.to_pem
113
113
  end
114
114
  json
115
115
  end
@@ -1,4 +1,4 @@
1
- require 'cheffish/basic_chef_client'
1
+ require "cheffish/basic_chef_client"
2
2
 
3
3
  module Cheffish
4
4
  class ChefRun
@@ -11,7 +11,7 @@ module Cheffish
11
11
  # - log_location: <path|IO object> - where to stream logs to
12
12
  # - verbose_logging: true|false - true if you want verbose logging in :debug
13
13
  #
14
- def initialize(chef_config={})
14
+ def initialize(chef_config = {})
15
15
  @chef_config = chef_config || {}
16
16
  end
17
17
 
@@ -56,18 +56,23 @@ module Cheffish
56
56
  def stdout
57
57
  @client ? client.chef_config[:stdout].string : nil
58
58
  end
59
+
59
60
  def stderr
60
61
  @client ? client.chef_config[:stderr].string : nil
61
62
  end
63
+
62
64
  def logs
63
65
  @client ? client.chef_config[:log_location].string : nil
64
66
  end
67
+
65
68
  def logged_warnings
66
69
  logs.lines.select { |l| l =~ /^\[[^\]]*\] WARN:/ }.join("\n")
67
70
  end
71
+
68
72
  def logged_errors
69
73
  logs.lines.select { |l| l =~ /^\[[^\]]*\] ERROR:/ }.join("\n")
70
74
  end
75
+
71
76
  def logged_info
72
77
  logs.lines.select { |l| l =~ /^\[[^\]]*\] INFO:/ }.join("\n")
73
78
  end
@@ -1,5 +1,5 @@
1
- require 'chef/config'
2
- require 'cheffish/with_pattern'
1
+ require "chef/config"
2
+ require "cheffish/with_pattern"
3
3
 
4
4
  module Cheffish
5
5
  class ChefRunData
@@ -1,4 +1,4 @@
1
- require 'chef/event_dispatch/base'
1
+ require "chef/event_dispatch/base"
2
2
 
3
3
  module Cheffish
4
4
  class ChefRunListener < Chef::EventDispatch::Base
@@ -1,14 +1,14 @@
1
- require 'openssl'
2
- require 'net/ssh'
3
- require 'etc'
4
- require 'socket'
5
- require 'digest/md5'
6
- require 'base64'
1
+ require "openssl"
2
+ require "net/ssh"
3
+ require "etc"
4
+ require "socket"
5
+ require "digest/md5"
6
+ require "base64"
7
7
 
8
8
  module Cheffish
9
9
  class KeyFormatter
10
10
  # Returns nil or key, format
11
- def self.decode(str, pass_phrase=nil, filename='')
11
+ def self.decode(str, pass_phrase = nil, filename = "")
12
12
  key_format = {}
13
13
  key_format[:format] = format_of(str)
14
14
 
@@ -38,7 +38,7 @@ module Cheffish
38
38
  encode_openssh_key(key)
39
39
  when :pem
40
40
  if key_format[:pass_phrase]
41
- cipher = key_format[:cipher] || 'DES-EDE3-CBC'
41
+ cipher = key_format[:cipher] || "DES-EDE3-CBC"
42
42
  key.to_pem(OpenSSL::Cipher.new(cipher), key_format[:pass_phrase])
43
43
  else
44
44
  key.to_pem
@@ -48,44 +48,42 @@ module Cheffish
48
48
  when :fingerprint, :pkcs1md5fingerprint
49
49
  hexes = Digest::MD5.hexdigest(key.to_der)
50
50
  # Put : between every pair of hexes
51
- hexes.scan(/../).join(':')
51
+ hexes.scan(/../).join(":")
52
52
  when :rfc4716md5fingerprint
53
53
  type, base64_data, etc = encode_openssh_key(key).split
54
54
  data = Base64.decode64(base64_data)
55
55
  hexes = Digest::MD5.hexdigest(data)
56
- hexes.scan(/../).join(':')
56
+ hexes.scan(/../).join(":")
57
57
  when :pkcs8sha1fingerprint
58
58
  if RUBY_VERSION.to_f >= 2.0
59
59
  raise "PKCS8 SHA1 not supported in Ruby #{RUBY_VERSION}"
60
60
  end
61
- require 'openssl_pkcs8'
61
+ require "openssl_pkcs8"
62
62
  pkcs8_pem = key.to_pem_pkcs8
63
63
  pkcs8_base64 = pkcs8_pem.split("\n").reject { |l| l =~ /^-----/ }
64
64
  pkcs8_data = Base64.decode64(pkcs8_base64.join)
65
65
  hexes = Digest::SHA1.hexdigest(pkcs8_data)
66
- hexes.scan(/../).join(':')
66
+ hexes.scan(/../).join(":")
67
67
  else
68
68
  raise "Unrecognized key format #{format}"
69
69
  end
70
70
  end
71
71
 
72
- private
73
-
74
72
  def self.encode_openssh_key(key)
75
73
  # TODO there really isn't a method somewhere in net/ssh or openssl that does this??
76
74
  type = key.ssh_type
77
- data = [ key.to_blob ].pack('m0')
75
+ data = [ key.to_blob ].pack("m0")
78
76
  "#{type} #{data} #{Etc.getlogin}@#{Socket.gethostname}"
79
77
  end
80
78
 
81
- def self.decode_openssh_key(str, filename='')
79
+ def self.decode_openssh_key(str, filename = "")
82
80
  Net::SSH::KeyFactory.load_data_public_key(str, filename)
83
81
  end
84
82
 
85
83
  def self.format_of(key_contents)
86
- if key_contents.start_with?('-----BEGIN ')
84
+ if key_contents.start_with?("-----BEGIN ")
87
85
  :pem
88
- elsif key_contents.start_with?('ssh-rsa ') || key_contents.start_with?('ssh-dss ')
86
+ elsif key_contents.start_with?("ssh-rsa ", "ssh-dss ")
89
87
  :openssh
90
88
  else
91
89
  :der