knife-cloud 1.0.0.rc.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 (68) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +33 -0
  3. data/.travis.yml +7 -0
  4. data/CHANGELOG.md +11 -0
  5. data/CONTRIBUTING.md +5 -0
  6. data/Gemfile +9 -0
  7. data/LICENSE +201 -0
  8. data/README.md +420 -0
  9. data/Rakefile +35 -0
  10. data/knife-cloud.gemspec +27 -0
  11. data/lib/chef/knife/cloud/chefbootstrap/bootstrap_distribution.rb +31 -0
  12. data/lib/chef/knife/cloud/chefbootstrap/bootstrap_options.rb +191 -0
  13. data/lib/chef/knife/cloud/chefbootstrap/bootstrap_protocol.rb +69 -0
  14. data/lib/chef/knife/cloud/chefbootstrap/bootstrapper.rb +78 -0
  15. data/lib/chef/knife/cloud/chefbootstrap/ssh_bootstrap_protocol.rb +179 -0
  16. data/lib/chef/knife/cloud/chefbootstrap/unix_distribution.rb +31 -0
  17. data/lib/chef/knife/cloud/chefbootstrap/windows_distribution.rb +32 -0
  18. data/lib/chef/knife/cloud/chefbootstrap/winrm_bootstrap_protocol.rb +85 -0
  19. data/lib/chef/knife/cloud/command.rb +101 -0
  20. data/lib/chef/knife/cloud/exceptions.rb +38 -0
  21. data/lib/chef/knife/cloud/fog/options.rb +29 -0
  22. data/lib/chef/knife/cloud/fog/service.rb +200 -0
  23. data/lib/chef/knife/cloud/helpers.rb +39 -0
  24. data/lib/chef/knife/cloud/list_resource_command.rb +97 -0
  25. data/lib/chef/knife/cloud/list_resource_options.rb +21 -0
  26. data/lib/chef/knife/cloud/server/create_command.rb +165 -0
  27. data/lib/chef/knife/cloud/server/create_options.rb +80 -0
  28. data/lib/chef/knife/cloud/server/delete_command.rb +68 -0
  29. data/lib/chef/knife/cloud/server/delete_options.rb +42 -0
  30. data/lib/chef/knife/cloud/server/list_command.rb +84 -0
  31. data/lib/chef/knife/cloud/server/list_options.rb +43 -0
  32. data/lib/chef/knife/cloud/server/options.rb +39 -0
  33. data/lib/chef/knife/cloud/server/show_command.rb +55 -0
  34. data/lib/chef/knife/cloud/server/show_options.rb +36 -0
  35. data/lib/chef/knife/cloud/service.rb +91 -0
  36. data/lib/knife-cloud/version.rb +6 -0
  37. data/lib/test/fixtures/knife.rb +9 -0
  38. data/lib/test/fixtures/validation.pem +27 -0
  39. data/lib/test/knife-utils/helper.rb +39 -0
  40. data/lib/test/knife-utils/knife_test_utils.rb +40 -0
  41. data/lib/test/knife-utils/matchers.rb +29 -0
  42. data/lib/test/knife-utils/test_bed.rb +56 -0
  43. data/lib/test/templates/chef-full-chef-zero.erb +67 -0
  44. data/lib/test/templates/windows-chef-client-msi.erb +231 -0
  45. data/lib/test/templates/windows-shell.erb +77 -0
  46. data/spec/resource_spec_helper.rb +49 -0
  47. data/spec/server_command_common_spec_helper.rb +48 -0
  48. data/spec/spec_helper.rb +25 -0
  49. data/spec/support/shared_examples_for_command.rb +35 -0
  50. data/spec/support/shared_examples_for_servercreatecommand.rb +144 -0
  51. data/spec/support/shared_examples_for_serverdeletecommand.rb +77 -0
  52. data/spec/support/shared_examples_for_service.rb +85 -0
  53. data/spec/unit/bootstrap_protocol_spec.rb +70 -0
  54. data/spec/unit/bootstrapper_spec.rb +171 -0
  55. data/spec/unit/cloud_command_spec.rb +35 -0
  56. data/spec/unit/command_spec.rb +49 -0
  57. data/spec/unit/fog_service_spec.rb +138 -0
  58. data/spec/unit/list_resource_command_spec.rb +136 -0
  59. data/spec/unit/server_create_command_spec.rb +198 -0
  60. data/spec/unit/server_delete_command_spec.rb +25 -0
  61. data/spec/unit/server_list_command_spec.rb +119 -0
  62. data/spec/unit/server_show_command_spec.rb +64 -0
  63. data/spec/unit/service_spec.rb +46 -0
  64. data/spec/unit/ssh_bootstrap_protocol_spec.rb +116 -0
  65. data/spec/unit/unix_distribution_spec.rb +37 -0
  66. data/spec/unit/windows_distribution_spec.rb +37 -0
  67. data/spec/unit/winrm_bootstrap_protocol_spec.rb +106 -0
  68. metadata +248 -0
@@ -0,0 +1,101 @@
1
+ #
2
+ # Author:: Kaustubh Deorukhkar (<kaustubh@clogeny.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 'chef/knife'
20
+ require "chef/knife/cloud/helpers"
21
+ require 'chef/knife/cloud/exceptions'
22
+
23
+ class Chef
24
+ class Knife
25
+
26
+ class Cloud
27
+ class Command < Chef::Knife
28
+ include Cloud::Helpers
29
+ attr_accessor :service, :custom_arguments
30
+
31
+ def run
32
+ begin
33
+ # Set dafult config
34
+ set_default_config
35
+
36
+ # validate compulsory params
37
+ validate!
38
+
39
+ # validate command pre-requisites (cli options)
40
+ validate_params!
41
+
42
+ # setup the service
43
+ @service = create_service_instance
44
+
45
+ service.ui = ui # for interactive user prompts/messages
46
+
47
+ # Perform any steps before handling the command
48
+ before_exec_command
49
+
50
+ # exec the actual cmd
51
+ execute_command
52
+
53
+ # Perform any steps after handling the command
54
+ after_exec_command
55
+ rescue CloudExceptions::KnifeCloudError => e
56
+ Chef::Log.debug(e.message)
57
+ exit 1
58
+ end
59
+ end
60
+
61
+ def create_service_instance
62
+ raise Chef::Exceptions::Override, "You must override create_service_instance in #{self.to_s} to create cloud specific service"
63
+ end
64
+
65
+ def execute_command
66
+ raise Chef::Exceptions::Override, "You must override execute_command in #{self.to_s}"
67
+ end
68
+
69
+ # Derived classes can override before_exec_command and after_exec_command
70
+ def before_exec_command
71
+ end
72
+
73
+ def after_exec_command
74
+ end
75
+
76
+ def set_default_config
77
+ end
78
+
79
+ def validate!(*keys)
80
+ # validates necessary options/params to carry out the command.
81
+ # subclasses to implement this.
82
+ errors = []
83
+ keys.each do |k|
84
+ errors << "You did not provide a valid '#{pretty_key(k)}' value." if locate_config_value(k).nil?
85
+ end
86
+ error_message = ""
87
+ raise CloudExceptions::ValidationError, error_message if errors.each{|e| ui.error(e); error_message = "#{error_message} #{e}."}.any?
88
+ end
89
+
90
+ def validate_params!
91
+ end
92
+
93
+ def pretty_key(key)
94
+ key.to_s.gsub(/_/, ' ').gsub(/\w+/){ |w| (w =~ /(ssh)|(aws)/i) ? w.upcase : w.capitalize }
95
+ end
96
+
97
+ end # class Command
98
+ end
99
+ end
100
+ end
101
+
@@ -0,0 +1,38 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2013 Opscode, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ class Chef
19
+ class Knife
20
+ class Cloud
21
+ class CloudExceptions
22
+ class KnifeCloudError < RuntimeError; end
23
+ class CloudAPIException < KnifeCloudError; end
24
+ class ServiceConnectionError < KnifeCloudError; end
25
+ class ValidationError < KnifeCloudError; end
26
+ class ServerCreateError < KnifeCloudError; end
27
+ class ServerSetupError < KnifeCloudError; end
28
+ class ServerDeleteError < KnifeCloudError; end
29
+ class ServerCreateDependenciesError < KnifeCloudError; end
30
+ class BootstrapError < KnifeCloudError; end
31
+ class ServerShowError < KnifeCloudError; end
32
+ class ChefServerError < KnifeCloudError; end
33
+ class NetworkNotFoundError < KnifeCloudError; end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
@@ -0,0 +1,29 @@
1
+ #
2
+ # Author:: Kaustubh Deorukhkar (<kaustubh@clogeny.com>)
3
+ # Copyright:: Copyright (c) 2013 Opscode, Inc.
4
+ #
5
+
6
+ class Chef
7
+ class Knife
8
+ class Cloud
9
+ module FogOptions
10
+
11
+ def self.included(includer)
12
+ includer.instance_eval do
13
+ option :fog_version,
14
+ :long => "--fog-version version",
15
+ :description => "Fog gem version to use. Use the ruby gem version strings",
16
+ :default => "",
17
+ :proc => Proc.new { |v| Chef::Config[:knife][:cloud_fog_version] = v}
18
+
19
+ option :api_endpoint,
20
+ :long => "--api-endpoint ENDPOINT",
21
+ :description => "Your API endpoint. Eg, for Eucalyptus it can be 'http://ecc.eucalyptus.com:8773/services/Eucalyptus'",
22
+ :proc => Proc.new { |endpoint| Chef::Config[:knife][:api_endpoint] = endpoint }
23
+
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,200 @@
1
+ #
2
+ # Author:: Kaustubh Deorukhkar (<kaustubh@clogeny.com>)
3
+ # Author:: Prabhu Das (<prabhu.das@clogeny.com>)
4
+ # Copyright:: Copyright (c) 2013 Opscode, Inc.
5
+ #
6
+
7
+ require 'chef/knife/cloud/service'
8
+ require 'chef/knife/cloud/exceptions'
9
+
10
+ class Chef
11
+ class Knife
12
+ class Cloud
13
+ class FogService < Service
14
+
15
+ def initialize(options = {})
16
+ load_fog_gem
17
+ super
18
+ end
19
+
20
+ def load_fog_gem
21
+ begin
22
+ # Load specific version of fog. Any other classes/modules using fog are loaded after this.
23
+ gem "fog", Chef::Config[:knife][:cloud_fog_version]
24
+ require 'fog'
25
+ Chef::Log.debug("Using fog version: #{Gem.loaded_specs["fog"].version}")
26
+ rescue Exception => e
27
+ Chef::Log.error "Error loading fog gem."
28
+ exit 1
29
+ end
30
+ end
31
+
32
+ def connection
33
+ add_api_endpoint
34
+ @connection ||= begin
35
+ connection = Fog::Compute.new(@auth_params)
36
+ rescue Excon::Errors::Unauthorized => e
37
+ error_message = "Connection failure, please check your username and password."
38
+ ui.fatal(error_message)
39
+ raise CloudExceptions::ServiceConnectionError, "#{e.message}. #{error_message}"
40
+ rescue Excon::Errors::SocketError => e
41
+ error_message = "Connection failure, please check your authentication URL."
42
+ ui.fatal(error_message)
43
+ raise CloudExceptions::ServiceConnectionError, "#{e.message}. #{error_message}"
44
+ end
45
+ end
46
+
47
+ def network
48
+ @network ||= begin
49
+ network = Fog::Network.new(@auth_params)
50
+ rescue Excon::Errors::Unauthorized => e
51
+ error_message = "Connection failure, please check your username and password."
52
+ ui.fatal(error_message)
53
+ raise CloudExceptions::ServiceConnectionError, "#{e.message}. #{error_message}"
54
+ rescue Excon::Errors::SocketError => e
55
+ error_message = "Connection failure, please check your authentication URL."
56
+ ui.fatal(error_message)
57
+ raise CloudExceptions::ServiceConnectionError, "#{e.message}. #{error_message}"
58
+ rescue Fog::Errors::NotFound => e
59
+ error_message = "No Network service found. This command is unavailable with current cloud."
60
+ ui.fatal(error_message)
61
+ raise CloudExceptions::NetworkNotFoundError, "#{e.message}. #{error_message}"
62
+ end
63
+ end
64
+
65
+ # cloud server specific implementation methods for commands.
66
+ def create_server(options = {})
67
+ begin
68
+ add_custom_attributes(options[:server_def])
69
+ server = connection.servers.create(options[:server_def])
70
+ rescue Excon::Errors::BadRequest => e
71
+ response = Chef::JSONCompat.from_json(e.response.body)
72
+ if response['badRequest']['code'] == 400
73
+ message = "Bad request (400): #{response['badRequest']['message']}"
74
+ ui.fatal(message)
75
+ else
76
+ message = "Unknown server error (#{response['badRequest']['code']}): #{response['badRequest']['message']}"
77
+ ui.fatal(message)
78
+ end
79
+ raise CloudExceptions::ServerCreateError, message
80
+ rescue Fog::Errors::Error => e
81
+ raise CloudExceptions::ServerCreateError, e.message
82
+ end
83
+
84
+ print "\n#{ui.color("Waiting for server [wait time = #{options[:server_create_timeout]}]", :magenta)}"
85
+
86
+ # wait for it to be ready to do stuff
87
+ server.wait_for(Integer(options[:server_create_timeout])) { print "."; ready? }
88
+
89
+ puts("\n")
90
+ server
91
+ end
92
+
93
+ def delete_server(server_name)
94
+ begin
95
+ server = get_server(server_name)
96
+ msg_pair("Instance Name", get_server_name(server))
97
+ msg_pair("Instance ID", server.id)
98
+
99
+ puts "\n"
100
+ ui.confirm("Do you really want to delete this server")
101
+
102
+ # delete the server
103
+ server.destroy
104
+ rescue NoMethodError
105
+ error_message = "Could not locate server '#{server_name}'."
106
+ ui.error(error_message)
107
+ raise CloudExceptions::ServerDeleteError, error_message
108
+ rescue Excon::Errors::BadRequest => e
109
+ handle_excon_exception(CloudExceptions::ServerDeleteError, e)
110
+ end
111
+ end
112
+
113
+ ["servers", "images", "networks"].each do |resource_type|
114
+ define_method("list_#{resource_type}") do
115
+ begin
116
+ case resource_type
117
+ when "networks"
118
+ network.method(resource_type).call.all
119
+ else
120
+ connection.method(resource_type).call.all
121
+ end
122
+ rescue Excon::Errors::BadRequest => e
123
+ handle_excon_exception(CloudExceptions::CloudAPIException, e)
124
+ end
125
+ end
126
+ end
127
+
128
+ def handle_excon_exception(exception_class, e)
129
+ error_message = if e.response
130
+ response = Chef::JSONCompat.from_json(e.response.body)
131
+ "Unknown server error (#{response['badRequest']['code']}): #{response['badRequest']['message']}"
132
+ else
133
+ "Unknown server error : #{e.message}"
134
+ end
135
+ ui.fatal(error_message)
136
+ raise exception_class, error_message
137
+ end
138
+
139
+ def list_resource_configurations
140
+ begin
141
+ flavors = connection.flavors.all
142
+ rescue Excon::Errors::BadRequest => e
143
+ handle_excon_exception(CloudExceptions::CloudAPIException, e)
144
+ end
145
+ end
146
+
147
+ def delete_server_on_failure(server = nil)
148
+ server.destroy if ! server.nil?
149
+ end
150
+
151
+ def add_api_endpoint
152
+ raise Chef::Exceptions::Override, "You must override add_api_endpoint in #{self.to_s} to add endpoint in auth_params for connection"
153
+ end
154
+
155
+ def get_server_name(server)
156
+ server.name
157
+ end
158
+
159
+ def get_server(instance_id)
160
+ begin
161
+ server = connection.servers.get(instance_id)
162
+ rescue Excon::Errors::BadRequest => e
163
+ handle_excon_exception(CloudExceptions::KnifeCloudError, e)
164
+ end
165
+ end
166
+
167
+ def get_image(name_or_id)
168
+ connection.images.find{|img| img.name =~ /#{name_or_id}/ || img.id == name_or_id }
169
+ end
170
+
171
+ def get_flavor(name_or_id)
172
+ connection.flavors.find{|f| f.name == name_or_id || f.id == name_or_id }
173
+ end
174
+
175
+ def server_summary(server, columns_with_info = [])
176
+ # columns_with_info is array of hash with label, key and attribute extraction callback, ex [{:label => "Label text", :key => 'key', value => 'the_actual_value', value_callback => callback_method to extract/format the required value}, ...]
177
+ list = []
178
+ columns_with_info.each do |col_info|
179
+ value = if col_info[:value].nil?
180
+ (col_info[:value_callback].nil? ? server.send(col_info[:key]).to_s : col_info[:value_callback].call(server.send(col_info[:key])))
181
+ else
182
+ col_info[:value]
183
+ end
184
+ if !(value.nil? || value.empty?)
185
+ list << ui.color(col_info[:label], :bold)
186
+ list << value
187
+ end
188
+ end
189
+ puts ui.list(list, :uneven_columns_across, 2) if columns_with_info.length > 0
190
+ end
191
+
192
+ def is_image_windows?(image)
193
+ image_info = connection.images.get(image)
194
+ !image_info.nil? ? image_info.platform == 'windows' : false
195
+ end
196
+
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,39 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2013 Opscode, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ class Chef
19
+ class Knife
20
+ class Cloud
21
+ module Helpers
22
+
23
+ # Additional helpers
24
+ def msg_pair(label, value, color=:cyan)
25
+ if value && !value.to_s.empty?
26
+ puts "#{ui.color(label, color)}: #{value}"
27
+ end
28
+ end
29
+
30
+ def locate_config_value(key)
31
+ key = key.to_sym
32
+ config[key] || Chef::Config[:knife][key]
33
+ end
34
+
35
+ end
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,97 @@
1
+ #
2
+ # Author:: Prabhu Das (<prabhu.das@clogeny.com>)
3
+ # Copyright:: Copyright (c) 2013-14 Chef, 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/cloud/command'
20
+
21
+ class Chef
22
+ class Knife
23
+ class Cloud
24
+ class ResourceListCommand < Command
25
+
26
+ def initialize(argv=[])
27
+ super argv
28
+ # columns_with_info is array of hash with label, key and attribute extraction callback, ex [{:label => "Label text", :key => 'key', value_callback => callback_method to extract/format the required value}, ...]
29
+ @columns_with_info = []
30
+ @sort_by_field = "id" # default sort by id
31
+ end
32
+
33
+ def execute_command
34
+ # exec the cmd
35
+ resources = query_resource
36
+
37
+ # handle the response
38
+ list(resources)
39
+ end
40
+
41
+ def query_resource
42
+ # specific resource type must override this.
43
+ raise Chef::Exceptions::Override, "You must override query_resource in #{self.to_s} to return resources."
44
+ end
45
+
46
+ def is_resource_filtered?(attribute, value)
47
+ # resource_filters is array of filters in form {:attribute => attribute-name, :regex => 'filter regex value'}
48
+ return false if @resource_filters.nil?
49
+ @resource_filters.each do |filter|
50
+ if attribute == filter[:attribute] and value =~ filter[:regex]
51
+ return true
52
+ end
53
+ end
54
+ false
55
+ end
56
+
57
+ # Derived class can override this to add more functionality.
58
+ def get_resource_col_val(resource)
59
+ resource_filtered = false
60
+ list = []
61
+ @columns_with_info.each do |col_info|
62
+ value = (col_info[:value_callback].nil? ? resource.send(col_info[:key]).to_s : col_info[:value_callback].call(resource.send(col_info[:key])))
63
+ if !config[:disable_filter]
64
+ resource_filtered = true if is_resource_filtered?(col_info[:key], value)
65
+ end
66
+ list << value
67
+ end
68
+ return list unless resource_filtered
69
+ end
70
+
71
+ # When @columns_with_info is nil display all
72
+ def list(resources)
73
+ # display column wise only if @columns_with_info is specified, else as a json for readable display.
74
+ begin
75
+ resource_list = @columns_with_info.map { |col_info| ui.color(col_info[:label], :bold) } if @columns_with_info.length > 0
76
+ resources.sort_by{|x| x.send(@sort_by_field).downcase }.each do |resource|
77
+ if @columns_with_info.length > 0
78
+ list = get_resource_col_val(resource)
79
+ resource_list.concat(list) unless list.nil?
80
+ else
81
+ puts resource.to_json
82
+ puts "\n"
83
+ end
84
+ end
85
+
86
+ rescue => e
87
+ ui.fatal("Unknown resource error : #{e.message}")
88
+ raise e
89
+ end
90
+ puts ui.list(resource_list, :uneven_columns_across, @columns_with_info.length) if @columns_with_info.length > 0
91
+ end
92
+
93
+ end # class ResourceListCommand
94
+ end
95
+ end
96
+ end
97
+
@@ -0,0 +1,21 @@
1
+
2
+ class Chef
3
+ class Knife
4
+ class Cloud
5
+ module ResourceListOptions
6
+
7
+ def self.included(includer)
8
+ includer.class_eval do
9
+
10
+ option :disable_filter,
11
+ :long => "--disable-filter",
12
+ :description => "Disable filtering of the current resource listing.",
13
+ :boolean => true,
14
+ :default => false
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,165 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2013 Opscode, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ require 'chef/knife/cloud/command'
18
+ require 'chef/knife/cloud/exceptions'
19
+ require 'chef/knife/cloud/chefbootstrap/bootstrapper'
20
+
21
+ class Chef
22
+ class Knife
23
+ class Cloud
24
+ class ServerCreateCommand < Command
25
+ attr_accessor :server, :create_options
26
+
27
+ def initialize(argv=[])
28
+ super argv
29
+ # columns_with_info is array of hash with label, key and attribute extraction callback, ex [{:label => "Label text", :key => 'key', value_callback => callback_method to extract/format the required value}, ...]
30
+ @columns_with_info = []
31
+ end
32
+
33
+ def validate_params!
34
+ # set param vm_name to a random value if the name is not set by the user (plugin)
35
+ config[:chef_node_name] = get_node_name(locate_config_value(:chef_node_name), locate_config_value(:chef_node_name_prefix))
36
+
37
+ # validate ssh_user, ssh_password, identity_file for ssh bootstrap protocol and winrm_password for winrm bootstrap protocol
38
+ errors = []
39
+
40
+ if locate_config_value(:bootstrap_protocol) == 'ssh'
41
+ if locate_config_value(:identity_file).nil? && locate_config_value(:ssh_password).nil?
42
+ errors << "You must provide either Identity file or SSH Password."
43
+ end
44
+ elsif locate_config_value(:bootstrap_protocol) == 'winrm'
45
+ if locate_config_value(:winrm_password).nil?
46
+ errors << "You must provide Winrm Password."
47
+ end
48
+ else
49
+ errors << "You must provide a valid bootstrap protocol. options [ssh/winrm]. For linux type images, options [ssh]"
50
+ end
51
+ error_message = ""
52
+ raise CloudExceptions::ValidationError, error_message if errors.each{|e| ui.error(e); error_message = "#{error_message} #{e}."}.any?
53
+ end
54
+
55
+ def before_exec_command
56
+ begin
57
+ post_connection_validations
58
+ service.create_server_dependencies
59
+ rescue CloudExceptions::ServerCreateDependenciesError => e
60
+ ui.fatal(e.message)
61
+ service.delete_server_dependencies
62
+ raise e
63
+ end
64
+ end
65
+
66
+ def execute_command
67
+ begin
68
+ @server = service.create_server(create_options)
69
+ rescue CloudExceptions::ServerCreateError => e
70
+ ui.fatal(e.message)
71
+ # server creation failed, so we need to rollback only dependencies.
72
+ service.delete_server_dependencies
73
+ raise e
74
+ end
75
+ service.server_summary(@server, @columns_with_info)
76
+ end
77
+
78
+ # Derived classes can override after_exec_command and also call cleanup_on_failure if any exception occured.
79
+ def after_exec_command
80
+ begin
81
+ # bootstrap the server
82
+ bootstrap
83
+ rescue CloudExceptions::BootstrapError => e
84
+ ui.fatal(e.message)
85
+ cleanup_on_failure
86
+ raise e
87
+ rescue => e
88
+ error_message = "Check if --bootstrap-protocol and --image-os-type is correct. #{e.message}"
89
+ ui.fatal(error_message)
90
+ cleanup_on_failure
91
+ raise e, error_message
92
+ end
93
+ end
94
+
95
+ def cleanup_on_failure
96
+ if config[:delete_server_on_failure]
97
+ service.delete_server_dependencies
98
+ service.delete_server_on_failure(@server)
99
+ end
100
+ end
101
+
102
+ # Bootstrap the server
103
+ def bootstrap
104
+ before_bootstrap
105
+ @bootstrapper = Bootstrapper.new(config)
106
+ Chef::Log.debug("Bootstrapping the server...")
107
+ ui.info("Bootstrapping the server by using #{ui.color("bootstrap_protocol", :cyan)}: #{config[:bootstrap_protocol]} and #{ui.color("image_os_type", :cyan)}: #{config[:image_os_type]}")
108
+ @bootstrapper.bootstrap
109
+ after_bootstrap
110
+ end
111
+
112
+ # any cloud specific initializations/cleanup we want to do around bootstrap.
113
+ def before_bootstrap
114
+ ssh_override_winrm if locate_config_value(:bootstrap_protocol) == 'ssh'
115
+ end
116
+
117
+ def after_bootstrap
118
+ service.server_summary(@server, @columns_with_info)
119
+ end
120
+
121
+ # knife-plugin can override set_default_config to set default config by using their own mechanism.
122
+ def set_default_config
123
+ config[:image_os_type] = 'windows' if config[:bootstrap_protocol] == 'winrm'
124
+ end
125
+
126
+ #generate a random name if chef_node_name is empty
127
+ def get_node_name(chef_node_name, prefix)
128
+ return chef_node_name unless chef_node_name.nil?
129
+ #lazy uuids
130
+ chef_node_name = "#{prefix}-"+rand.to_s.split('.')[1]
131
+ end
132
+
133
+ def post_connection_validations
134
+ end
135
+
136
+ private
137
+
138
+ # Here, ssh config override winrm config
139
+ def ssh_override_winrm
140
+ # unchanged ssh_user and changed winrm_user, override ssh_user
141
+ if locate_config_value(:ssh_user).eql?(options[:ssh_user][:default]) &&
142
+ !locate_config_value(:winrm_user).eql?(options[:winrm_user][:default])
143
+ config[:ssh_user] = locate_config_value(:winrm_user)
144
+ end
145
+ # unchanged ssh_port and changed winrm_port, override ssh_port
146
+ if locate_config_value(:ssh_port).eql?(options[:ssh_port][:default]) &&
147
+ !locate_config_value(:winrm_port).eql?(options[:winrm_port][:default])
148
+ config[:ssh_port] = locate_config_value(:winrm_port)
149
+ end
150
+ # unset ssh_password and set winrm_password, override ssh_password
151
+ if locate_config_value(:ssh_password).nil? &&
152
+ !locate_config_value(:winrm_password).nil?
153
+ config[:ssh_password] = locate_config_value(:winrm_password)
154
+ end
155
+ # unset identity_file and set kerberos_keytab_file, override identity_file
156
+ if locate_config_value(:identity_file).nil? &&
157
+ !locate_config_value(:kerberos_keytab_file).nil?
158
+ config[:identity_file] = locate_config_value(:kerberos_keytab_file)
159
+ end
160
+ end
161
+ end # class ServerCreateCommand
162
+ end
163
+ end
164
+ end
165
+