realityforge-knife-windows 0.5.14

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.
@@ -0,0 +1,268 @@
1
+ #
2
+ # Author:: Seth Chisamore (<schisamo@opscode.com>)
3
+ # Copyright:: Copyright (c) 2011 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/knife'
20
+ require 'chef/knife/winrm_base'
21
+
22
+ class Chef
23
+ class Knife
24
+ class Winrm < Knife
25
+
26
+ include Chef::Knife::WinrmBase
27
+
28
+ deps do
29
+ require 'readline'
30
+ require 'chef/search/query'
31
+ require 'em-winrm'
32
+ end
33
+
34
+ attr_writer :password
35
+
36
+ banner "knife winrm QUERY COMMAND (options)"
37
+
38
+ option :address_attribute,
39
+ :short => "-a ATTR",
40
+ :long => "--attribute ATTR",
41
+ :description => "The attribute to use for opening the connection - default is fqdn",
42
+ :default => "fqdn"
43
+
44
+ option :returns,
45
+ :long => "--returns CODES",
46
+ :description => "A comma delimited list of return codes which indicate success",
47
+ :default => nil,
48
+ :proc => Proc.new { |codes|
49
+ Chef::Config[:knife][:returns] = codes.split(',').collect {|item| item.to_i} }
50
+
51
+ option :manual,
52
+ :short => "-m",
53
+ :long => "--manual-list",
54
+ :boolean => true,
55
+ :description => "QUERY is a space separated list of servers",
56
+ :default => false
57
+
58
+ def session
59
+ session_opts = {}
60
+ session_opts[:logger] = Chef::Log.logger if Chef::Log.level == :debug
61
+ @session ||= begin
62
+ s = EventMachine::WinRM::Session.new(session_opts)
63
+ s.on_output do |host, data|
64
+ print_data(host, data)
65
+ end
66
+ s.on_error do |host, err|
67
+ print_data(host, err, :red)
68
+ end
69
+ s.on_command_complete do |host|
70
+ host = host == :all ? 'All Servers' : host
71
+ Chef::Log.debug("command complete on #{host}")
72
+ end
73
+ s
74
+ end
75
+
76
+ end
77
+
78
+ def configure_session
79
+ list = case config[:manual]
80
+ when true
81
+ @name_args[0].split(" ")
82
+ when false
83
+ r = Array.new
84
+ q = Chef::Search::Query.new
85
+ @action_nodes = q.search(:node, @name_args[0])[0]
86
+ @action_nodes.each do |item|
87
+ i = format_for_display(item)[config[:address_attribute]]
88
+ r.push(i) unless i.nil?
89
+ end
90
+ r
91
+ end
92
+ if list.length == 0
93
+ if @action_nodes.length == 0
94
+ ui.fatal("No nodes returned from search!")
95
+ else
96
+ p format_for_display(@action_nodes[0])#['fqdn']
97
+ p @action_nodes[0]
98
+ ui.fatal("#{@action_nodes.length} #{@action_nodes.length > 1 ? "nodes":"node"} found, " +
99
+ "but does not have the required attribute (#{config[:address_attribute]}) to establish the connection. " +
100
+ "Try setting another attribute to open the connection using --address_attribute.")
101
+ end
102
+ exit 10
103
+ end
104
+ session_from_list(list)
105
+ end
106
+
107
+ def session_from_list(list)
108
+ list.each do |item|
109
+ Chef::Log.debug("Adding #{item}")
110
+ session_opts = {}
111
+ session_opts[:user] = config[:winrm_user] = Chef::Config[:knife][:winrm_user] || config[:winrm_user]
112
+ session_opts[:password] = config[:winrm_password] = Chef::Config[:knife][:winrm_password] || config[:winrm_password]
113
+ session_opts[:port] = Chef::Config[:knife][:winrm_port] || config[:winrm_port]
114
+ session_opts[:keytab] = Chef::Config[:knife][:kerberos_keytab_file] if Chef::Config[:knife][:kerberos_keytab_file]
115
+ session_opts[:realm] = Chef::Config[:knife][:kerberos_realm] if Chef::Config[:knife][:kerberos_realm]
116
+ session_opts[:service] = Chef::Config[:knife][:kerberos_service] if Chef::Config[:knife][:kerberos_service]
117
+ session_opts[:ca_trust_path] = Chef::Config[:knife][:ca_trust_file] if Chef::Config[:knife][:ca_trust_file]
118
+ session_opts[:operation_timeout] = 1800 # 30 min OperationTimeout for long bootstraps fix for KNIFE_WINDOWS-8
119
+
120
+ ## If you have a \\ in your name you need to use NTLM domain authentication
121
+ if session_opts[:user].split("\\").length.eql?(2)
122
+ session_opts[:basic_auth_only] = false
123
+ else
124
+ session_opts[:basic_auth_only] = true
125
+ end
126
+
127
+ if config.keys.any? {|k| k.to_s =~ /kerberos/ }
128
+ session_opts[:transport] = :kerberos
129
+ session_opts[:basic_auth_only] = false
130
+ else
131
+ session_opts[:transport] = (Chef::Config[:knife][:winrm_transport] || config[:winrm_transport]).to_sym
132
+ session_opts[:disable_sspi] = true
133
+ if session_opts[:user] and
134
+ (not session_opts[:password])
135
+ session_opts[:password] = Chef::Config[:knife][:winrm_password] = config[:winrm_password] = get_password
136
+
137
+ end
138
+ end
139
+
140
+ session.use(item, session_opts)
141
+
142
+ @longest = item.length if item.length > @longest
143
+ end
144
+ session
145
+ end
146
+
147
+ def print_data(host, data, color = :cyan)
148
+ if data =~ /\n/
149
+ data.split(/\n/).each { |d| print_data(host, d, color) }
150
+ else
151
+ padding = @longest - host.length
152
+ print ui.color(host, color)
153
+ padding.downto(0) { print " " }
154
+ puts data.chomp
155
+ end
156
+ end
157
+
158
+ def winrm_command(command, subsession=nil)
159
+ subsession ||= session
160
+ subsession.relay_command(command)
161
+ end
162
+
163
+ def get_password
164
+ @password ||= ui.ask("Enter your password: ") { |q| q.echo = false }
165
+ end
166
+
167
+ # Present the prompt and read a single line from the console. It also
168
+ # detects ^D and returns "exit" in that case. Adds the input to the
169
+ # history, unless the input is empty. Loops repeatedly until a non-empty
170
+ # line is input.
171
+ def read_line
172
+ loop do
173
+ command = reader.readline("#{ui.color('knife-winrm>', :bold)} ", true)
174
+
175
+ if command.nil?
176
+ command = "exit"
177
+ puts(command)
178
+ else
179
+ command.strip!
180
+ end
181
+
182
+ unless command.empty?
183
+ return command
184
+ end
185
+ end
186
+ end
187
+
188
+ def reader
189
+ Readline
190
+ end
191
+
192
+ def interactive
193
+ puts "Connected to #{ui.list(session.servers.collect { |s| ui.color(s.host, :cyan) }, :inline, " and ")}"
194
+ puts
195
+ puts "To run a command on a list of servers, do:"
196
+ puts " on SERVER1 SERVER2 SERVER3; COMMAND"
197
+ puts " Example: on latte foamy; echo foobar"
198
+ puts
199
+ puts "To exit interactive mode, use 'quit!'"
200
+ puts
201
+ while 1
202
+ command = read_line
203
+ case command
204
+ when 'quit!'
205
+ puts 'Bye!'
206
+ session.close
207
+ break
208
+ when /^on (.+?); (.+)$/
209
+ raw_list = $1.split(" ")
210
+ server_list = Array.new
211
+ session.servers.each do |session_server|
212
+ server_list << session_server if raw_list.include?(session_server.host)
213
+ end
214
+ command = $2
215
+ winrm_command(command, session.on(*server_list))
216
+ else
217
+ winrm_command(command)
218
+ end
219
+ end
220
+ end
221
+
222
+ def check_for_errors!(exit_codes)
223
+
224
+ exit_codes.each do |host, value|
225
+ unless Chef::Config[:knife][:returns].include? value.to_i
226
+ @exit_code = 1
227
+ ui.error "Failed to execute command on #{host} return code #{value}"
228
+ end
229
+ end
230
+
231
+ end
232
+
233
+ def run
234
+ STDOUT.sync = STDERR.sync = true
235
+
236
+ begin
237
+ @longest = 0
238
+
239
+ configure_session
240
+
241
+ case @name_args[1]
242
+ when "interactive"
243
+ interactive
244
+ else
245
+ winrm_command(@name_args[1..-1].join(" "))
246
+
247
+ if config[:returns]
248
+ check_for_errors! session.exit_codes
249
+ end
250
+
251
+ session.close
252
+ @exit_code || 0
253
+ end
254
+ rescue WinRM::WinRMHTTPTransportError => e
255
+ case e.message
256
+ when /401/
257
+ ui.error "Failed to authenticate to #{@name_args[0].split(" ")} as #{config[:winrm_user]}"
258
+ ui.info "Response: #{e.message}"
259
+ else
260
+ raise e
261
+ end
262
+ end
263
+ end
264
+
265
+ end
266
+ end
267
+ end
268
+
@@ -0,0 +1,98 @@
1
+ #
2
+ # Author:: Seth Chisamore (<schisamo@opscode.com>)
3
+ # Copyright:: Copyright (c) 2011 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/knife'
20
+ require 'chef/encrypted_data_bag_item'
21
+
22
+ class Chef
23
+ class Knife
24
+ module WinrmBase
25
+
26
+ # :nodoc:
27
+ # Would prefer to do this in a rational way, but can't be done b/c of
28
+ # Mixlib::CLI's design :(
29
+ def self.included(includer)
30
+ includer.class_eval do
31
+
32
+ deps do
33
+ require 'readline'
34
+ require 'chef/json_compat'
35
+ end
36
+
37
+ option :winrm_user,
38
+ :short => "-x USERNAME",
39
+ :long => "--winrm-user USERNAME",
40
+ :description => "The WinRM username",
41
+ :default => "Administrator",
42
+ :proc => Proc.new { |key| Chef::Config[:knife][:winrm_user] = key }
43
+
44
+ option :winrm_password,
45
+ :short => "-P PASSWORD",
46
+ :long => "--winrm-password PASSWORD",
47
+ :description => "The WinRM password",
48
+ :proc => Proc.new { |key| Chef::Config[:knife][:winrm_password] = key }
49
+
50
+ option :winrm_port,
51
+ :short => "-p PORT",
52
+ :long => "--winrm-port PORT",
53
+ :description => "The WinRM port, by default this is 5985",
54
+ :default => "5985",
55
+ :proc => Proc.new { |key| Chef::Config[:knife][:winrm_port] = key }
56
+
57
+ option :identity_file,
58
+ :short => "-i IDENTITY_FILE",
59
+ :long => "--identity-file IDENTITY_FILE",
60
+ :description => "The SSH identity file used for authentication"
61
+
62
+ option :winrm_transport,
63
+ :short => "-t TRANSPORT",
64
+ :long => "--winrm-transport TRANSPORT",
65
+ :description => "The WinRM transport type. valid choices are [ssl, plaintext]",
66
+ :default => 'plaintext',
67
+ :proc => Proc.new { |transport| Chef::Config[:knife][:winrm_transport] = transport }
68
+
69
+ option :kerberos_keytab_file,
70
+ :short => "-i KEYTAB_FILE",
71
+ :long => "--keytab-file KEYTAB_FILE",
72
+ :description => "The Kerberos keytab file used for authentication",
73
+ :proc => Proc.new { |keytab| Chef::Config[:knife][:kerberos_keytab_file] = keytab }
74
+
75
+ option :kerberos_realm,
76
+ :short => "-R KERBEROS_REALM",
77
+ :long => "--kerberos-realm KERBEROS_REALM",
78
+ :description => "The Kerberos realm used for authentication",
79
+ :proc => Proc.new { |realm| Chef::Config[:knife][:kerberos_realm] = realm }
80
+
81
+ option :kerberos_service,
82
+ :short => "-S KERBEROS_SERVICE",
83
+ :long => "--kerberos-service KERBEROS_SERVICE",
84
+ :description => "The Kerberos service used for authentication",
85
+ :proc => Proc.new { |service| Chef::Config[:knife][:kerberos_service] = service }
86
+
87
+ option :ca_trust_file,
88
+ :short => "-f CA_TRUST_FILE",
89
+ :long => "--ca-trust-file CA_TRUST_FILE",
90
+ :description => "The Certificate Authority (CA) trust file used for SSL transport",
91
+ :proc => Proc.new { |trust| Chef::Config[:knife][:ca_trust_file] = trust }
92
+
93
+ end
94
+ end
95
+
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,6 @@
1
+ module Knife
2
+ module Windows
3
+ VERSION = "0.5.14"
4
+ MAJOR, MINOR, TINY = VERSION.split('.')
5
+ end
6
+ end
@@ -0,0 +1,115 @@
1
+ #
2
+ # Author:: Adam Edwards (<adamed@opscode.com>)
3
+ # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
15
+ # implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'spec_helper'
21
+ require 'tmpdir'
22
+
23
+ # These test cases exercise the Knife::Windows knife plugin's ability
24
+ # to download a bootstrap msi as part of the bootstrap process on
25
+ # Windows nodes. The test modifies the Windows batch file generated
26
+ # from an erb template in the plugin source in order to enable execution
27
+ # of only the download functionality contained in the bootstrap template.
28
+ # The test relies on knowledge of the fields of the template itself and
29
+ # also on knowledge of the contents and structure of the Windows batch
30
+ # file generated by the template.
31
+ #
32
+ # Note that if the bootstrap template changes substantially, the tests
33
+ # should fail and will require re-implementation. If such changes
34
+ # occur, the bootstrap code should be refactored to explicitly expose
35
+ # the download funcitonality separately from other tasks to make the
36
+ # test more robust.
37
+ describe 'Knife::Windows::Core msi download functionality for knife Windows winrm bootstrap template' do
38
+
39
+ before(:all) do
40
+ # Since we're always running 32-bit Ruby, fix the
41
+ # PROCESSOR_ARCHITECTURE environment variable.
42
+
43
+ if ENV["PROCESSOR_ARCHITEW6432"]
44
+ ENV["PROCESSOR_ARCHITECTURE"] = ENV["PROCESSOR_ARCHITEW6432"]
45
+ end
46
+
47
+ # All file artifacts from this test will be written into this directory
48
+ @temp_directory = Dir.mktmpdir("bootstrap_test")
49
+
50
+ # Location to which the download script will be modified to write
51
+ # the downloaded msi
52
+ @local_file_download_destination = "#{@temp_directory}/chef-client-latest.msi"
53
+ end
54
+
55
+ after(:all) do
56
+ # Clear the temp directory upon exit
57
+ if Dir.exists?(@temp_directory)
58
+ FileUtils::remove_dir(@temp_directory)
59
+ end
60
+ end
61
+
62
+ describe "running on any version of the Windows OS", :windows_only do
63
+ before do
64
+ @mock_bootstrap_context = Chef::Knife::Core::WindowsBootstrapContext.new({ }, nil, { })
65
+
66
+ # Stub the bootstrap context and prevent config related sections
67
+ # to be populated, chef installation and first chef run
68
+ @mock_bootstrap_context.stub(:validation_key).and_return("echo.validation_key")
69
+ @mock_bootstrap_context.stub(:encrypted_data_bag_secret).and_return("echo.encrypted_data_bag_secret")
70
+ @mock_bootstrap_context.stub(:config_content).and_return("echo.config_content")
71
+ @mock_bootstrap_context.stub(:start_chef).and_return("echo.echo start_chef_command")
72
+ @mock_bootstrap_context.stub(:run_list).and_return("echo.run_list")
73
+ @mock_bootstrap_context.stub(:install_chef).and_return("echo.echo install_chef_command")
74
+
75
+ # Change the directorires where bootstrap files will be created
76
+ @mock_bootstrap_context.stub(:bootstrap_directory).and_return(@temp_directory.gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR))
77
+ @mock_bootstrap_context.stub(:local_download_path).and_return(@local_file_download_destination.gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR))
78
+
79
+ # Prevent password prompt during bootstrap process
80
+ @mock_winrm = Chef::Knife::Winrm.new
81
+ @mock_winrm.stub(:get_password).and_return(nil)
82
+ Chef::Knife::Winrm.stub(:new).and_return(@mock_winrm)
83
+
84
+ Chef::Knife::Core::WindowsBootstrapContext.stub(:new).and_return(@mock_bootstrap_context)
85
+ end
86
+
87
+ it "downloads the chef-client MSI during winrm bootstrap" do
88
+ clean_test_case
89
+
90
+ bootstrap_context = Chef::Knife::BootstrapWindowsWinrm.new([ "127.0.0.1" ])
91
+
92
+ # Execute the commands locally that would normally be executed via WinRM
93
+ bootstrap_context.stub(:run_command) do |command|
94
+ system(command)
95
+ end
96
+
97
+ bootstrap_context.run
98
+
99
+ # Download should succeed
100
+ download_succeeded?.should == true
101
+ end
102
+ end
103
+
104
+ def download_succeeded?
105
+ File.exists?(@local_file_download_destination) && ! File.zero?(@local_file_download_destination)
106
+ end
107
+
108
+ # Remove file artifiacts generated by individual test cases
109
+ def clean_test_case
110
+ if File.exists?(@local_file_download_destination)
111
+ File.delete(@local_file_download_destination)
112
+ end
113
+ end
114
+
115
+ end
@@ -0,0 +1,63 @@
1
+
2
+ # Author:: Adam Edwards (<adamed@opscode.com>)
3
+ # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
15
+ # implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ def windows?
21
+ !!(RUBY_PLATFORM =~ /mswin|mingw|windows/)
22
+ end
23
+
24
+ require_relative '../lib/chef/knife/core/windows_bootstrap_context'
25
+ require_relative '../lib/chef/knife/bootstrap_windows_winrm'
26
+
27
+ if windows?
28
+ require 'ruby-wmi'
29
+ end
30
+
31
+ def windows2012?
32
+ is_win2k12 = false
33
+
34
+ if windows?
35
+ this_operating_system = WMI::Win32_OperatingSystem.find(:first)
36
+ os_version = this_operating_system.send('Version')
37
+
38
+ # The operating system version is a string in the following form
39
+ # that can be split into components based on the '.' delimiter:
40
+ # MajorVersionNumber.MinorVersionNumber.BuildNumber
41
+ os_version_components = os_version.split('.')
42
+
43
+ if os_version_components.length < 2
44
+ raise 'WMI returned a Windows version from Win32_OperatingSystem.Version ' +
45
+ 'with an unexpected format. The Windows version could not be determined.'
46
+ end
47
+
48
+ # Windows 6.2 is Windows Server 2012, so test the major and
49
+ # minor version components
50
+ is_win2k12 = os_version_components[0] == '6' && os_version_components[1] == '2'
51
+ end
52
+
53
+ is_win2k12
54
+ end
55
+
56
+
57
+ RSpec.configure do |config|
58
+ config.treat_symbols_as_metadata_keys_with_true_values = true
59
+
60
+ config.filter_run_excluding :windows_only => true unless windows?
61
+ config.filter_run_excluding :windows_2012_only => true unless windows2012?
62
+ end
63
+
@@ -0,0 +1,65 @@
1
+ #
2
+ # Author:: Bryan McLellan <btm@opscode.com>
3
+ # Copyright:: Copyright (c) 2013 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'spec_helper'
20
+
21
+ Chef::Knife::Winrm.load_deps
22
+
23
+ describe Chef::Knife::Winrm do
24
+ before(:all) do
25
+ @original_config = Chef::Config.hash_dup
26
+ @original_knife_config = Chef::Config[:knife].dup
27
+ end
28
+
29
+ after(:all) do
30
+ Chef::Config.configuration = @original_config
31
+ Chef::Config[:knife] = @original_knife_config
32
+ end
33
+
34
+ before do
35
+ @knife = Chef::Knife::Winrm.new
36
+ @knife.config[:address_attribute] = "fqdn"
37
+ @node_foo = Chef::Node.new
38
+ @node_foo.automatic_attrs[:fqdn] = "foo.example.org"
39
+ @node_foo.automatic_attrs[:ipaddress] = "10.0.0.1"
40
+ @node_bar = Chef::Node.new
41
+ @node_bar.automatic_attrs[:fqdn] = "bar.example.org"
42
+ @node_bar.automatic_attrs[:ipaddress] = "10.0.0.2"
43
+ end
44
+
45
+ describe "#configure_session" do
46
+ before do
47
+ @query = mock("Chef::Search::Query")
48
+ end
49
+
50
+ context "when there are some hosts found but they do not have an attribute to connect with" do
51
+ before do
52
+ @query.stub!(:search).and_return([[@node_foo, @node_bar]])
53
+ @node_foo.automatic_attrs[:fqdn] = nil
54
+ @node_bar.automatic_attrs[:fqdn] = nil
55
+ Chef::Search::Query.stub!(:new).and_return(@query)
56
+ end
57
+
58
+ it "should raise a specific error (KNIFE-222)" do
59
+ @knife.ui.should_receive(:fatal).with(/does not have the required attribute/)
60
+ @knife.should_receive(:exit).with(10)
61
+ @knife.configure_session
62
+ end
63
+ end
64
+ end
65
+ end