ucloudstack 0.0.1

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,70 @@
1
+ #
2
+ # Author::
3
+ # Copyright:: Copyright (c) 2011 Edmunds, 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
+
21
+ module KnifeCloudstack
22
+ class CsProductList < Chef::Knife
23
+
24
+ deps do
25
+ require 'knife-cloudstack/connection'
26
+ end
27
+
28
+ banner "knife cs product list (options)"
29
+
30
+ def run
31
+
32
+ connection = CloudstackClient::Connection.new(
33
+ locate_config_value(:cloudstack_url),
34
+ locate_config_value(:cloudstack_api_key),
35
+ locate_config_value(:cloudstack_secret_key)
36
+ )
37
+
38
+ product_list = [
39
+ ui.color('id', :bold),
40
+ ui.color('serviceid', :bold),
41
+ ui.color('desc', :bold),
42
+ ui.color('diskid', :bold),
43
+ ui.color('desc', :bold),
44
+ ui.color('templateid', :bold),
45
+ ui.color('desc', :bold),
46
+ ui.color('zoneid', :bold),
47
+ ui.color('desc', :bold)
48
+ ]
49
+ products = connection.list_products
50
+ products.each do |product|
51
+ product_list << product['productid']
52
+ product_list << product['serviceofferingid']
53
+ product_list << product['serviceofferingdesc']
54
+ product_list << product['diskofferingid']
55
+ product_list << product['diskofferingdesc']
56
+ product_list << product['templateid']
57
+ product_list << product['templatedesc']
58
+ product_list << product['zoneid']
59
+ product_list << product['zonedesc']
60
+ end
61
+ puts ui.list(product_list, :columns_across, 9)
62
+ end
63
+
64
+ def locate_config_value(key)
65
+ key = key.to_sym
66
+ Chef::Config[:knife][key] || config[key]
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,321 @@
1
+ #
2
+ # Author:: Ryan Holmes (<rholmes@edmunds.com>)
3
+ # Copyright:: Copyright (c) 2011 Edmunds, 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 'json'
21
+
22
+ module KnifeCloudstack
23
+ class CsServerCreate < Chef::Knife
24
+
25
+ # Seconds to delay between detecting ssh and initiating the bootstrap
26
+ BOOTSTRAP_DELAY = 3
27
+
28
+ # Seconds to wait between ssh pings
29
+ SSH_POLL_INTERVAL = 2
30
+
31
+ deps do
32
+ require 'chef/knife/bootstrap'
33
+ Chef::Knife::Bootstrap.load_deps
34
+ require 'socket'
35
+ require 'net/ssh/multi'
36
+ require 'chef/json_compat'
37
+ require 'knife-cloudstack/connection'
38
+ end
39
+
40
+ banner "knife cs server create [SERVER_NAME] (options)"
41
+
42
+ option :cloudstack_service,
43
+ :short => "-S SERVICE",
44
+ :long => "--service SERVICE",
45
+ :description => "The CloudStack service offering name",
46
+ :proc => Proc.new { |o| Chef::Config[:knife][:cloudstack_service] = o },
47
+ :default => "M"
48
+
49
+ option :cloudstack_template,
50
+ :short => "-T TEMPLATE",
51
+ :long => "--template TEMPLATE",
52
+ :description => "The CloudStack template for the server",
53
+ :proc => Proc.new { |t| Chef::Config[:knife][:cloudstack_template] = t }
54
+
55
+ option :cloudstack_zone,
56
+ :short => "-Z ZONE",
57
+ :long => "--zone ZONE",
58
+ :description => "The CloudStack zone for the server",
59
+ :proc => Proc.new { |z| Chef::Config[:knife][:cloudstack_zone] = z }
60
+
61
+ option :cloudstack_networks,
62
+ :short => "-W NETWORKS",
63
+ :long => "--networks NETWORK",
64
+ :description => "Comma separated list of CloudStack network names",
65
+ :proc => lambda { |n| n.split(',').map {|sn| sn.strip}} ,
66
+ :default => []
67
+
68
+ option :public_ip,
69
+ :long => "--[no-]public-ip",
70
+ :description => "Allocate a public IP for this server",
71
+ :boolean => true,
72
+ :default => true
73
+
74
+ option :chef_node_name,
75
+ :short => "-N NAME",
76
+ :long => "--node-name NAME",
77
+ :description => "The Chef node name for your new node"
78
+
79
+ option :ssh_user,
80
+ :short => "-x USERNAME",
81
+ :long => "--ssh-user USERNAME",
82
+ :description => "The ssh username"
83
+
84
+ option :ssh_password,
85
+ :short => "-P PASSWORD",
86
+ :long => "--ssh-password PASSWORD",
87
+ :description => "The ssh password"
88
+
89
+ option :identity_file,
90
+ :short => "-i IDENTITY_FILE",
91
+ :long => "--identity-file IDENTITY_FILE",
92
+ :description => "The SSH identity file used for authentication"
93
+
94
+ option :cloudstack_url,
95
+ :short => "-U URL",
96
+ :long => "--cloudstack-url URL",
97
+ :description => "The CloudStack API endpoint URL",
98
+ :proc => Proc.new { |u| Chef::Config[:knife][:cloudstack_url] = u }
99
+
100
+ option :cloudstack_api_key,
101
+ :short => "-A KEY",
102
+ :long => "--cloudstack-api-key KEY",
103
+ :description => "Your CloudStack API key",
104
+ :proc => Proc.new { |k| Chef::Config[:knife][:cloudstack_api_key] = k }
105
+
106
+ option :cloudstack_secret_key,
107
+ :short => "-K SECRET",
108
+ :long => "--cloudstack-secret-key SECRET",
109
+ :description => "Your CloudStack secret key",
110
+ :proc => Proc.new { |s| Chef::Config[:knife][:cloudstack_secret_key] = s }
111
+
112
+ option :prerelease,
113
+ :long => "--prerelease",
114
+ :description => "Install the pre-release chef gems"
115
+
116
+ option :bootstrap_version,
117
+ :long => "--bootstrap-version VERSION",
118
+ :description => "The version of Chef to install",
119
+ :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v }
120
+
121
+ option :distro,
122
+ :short => "-d DISTRO",
123
+ :long => "--distro DISTRO",
124
+ :description => "Bootstrap a distro using a template",
125
+ :proc => Proc.new { |d| Chef::Config[:knife][:distro] = d },
126
+ :default => "ubuntu10.04-gems"
127
+
128
+ option :template_file,
129
+ :long => "--template-file TEMPLATE",
130
+ :description => "Full path to location of template to use",
131
+ :proc => Proc.new { |t| Chef::Config[:knife][:template_file] = t },
132
+ :default => false
133
+
134
+ option :run_list,
135
+ :short => "-r RUN_LIST",
136
+ :long => "--run-list RUN_LIST",
137
+ :description => "Comma separated list of roles/recipes to apply",
138
+ :proc => lambda { |o| o.split(/[\s,]+/) },
139
+ :default => []
140
+
141
+ option :no_host_key_verify,
142
+ :long => "--no-host-key-verify",
143
+ :description => "Disable host key verification",
144
+ :boolean => true,
145
+ :default => false
146
+
147
+ option :no_bootstrap,
148
+ :long => "--no-bootstrap",
149
+ :description => "Disable Chef bootstrap",
150
+ :boolean => true,
151
+ :default => false
152
+
153
+ option :port_rules,
154
+ :short => "-p PORT_RULES",
155
+ :long => "--port-rules PORT_RULES",
156
+ :description => "Comma separated list of port forwarding rules, e.g. '25,53:4053,80:8080:TCP'",
157
+ :proc => lambda { |o| o.split(/[\s,]+/) },
158
+ :default => []
159
+
160
+
161
+ def run
162
+
163
+ # validate hostname and options
164
+ hostname = @name_args.first
165
+ unless /^[a-zA-Z0-9][a-zA-Z0-9-]*$/.match hostname then
166
+ ui.error "Invalid hostname. Please specify a short hostname, not an fqdn (e.g. 'myhost' instead of 'myhost.domain.com')."
167
+ exit 1
168
+ end
169
+ validate_options
170
+
171
+ $stdout.sync = true
172
+
173
+ connection = CloudstackClient::Connection.new(
174
+ locate_config_value(:cloudstack_url),
175
+ locate_config_value(:cloudstack_api_key),
176
+ locate_config_value(:cloudstack_secret_key)
177
+ )
178
+
179
+ print "#{ui.color("Waiting for server", :magenta)}"
180
+ server = connection.create_server(
181
+ hostname,
182
+ locate_config_value(:cloudstack_service),
183
+ locate_config_value(:cloudstack_template),
184
+ locate_config_value(:cloudstack_zone),
185
+ locate_config_value(:cloudstack_networks)
186
+ )
187
+
188
+ public_ip = find_or_create_public_ip(server, connection)
189
+
190
+ puts "\n\n"
191
+ puts "#{ui.color("Name", :cyan)}: #{server['name']}"
192
+ puts "#{ui.color("Public IP", :cyan)}: #{public_ip}"
193
+
194
+ return if config[:no_bootstrap]
195
+
196
+ print "\n#{ui.color("Waiting for sshd", :magenta)}"
197
+
198
+ print(".") until is_ssh_open?(public_ip) {
199
+ sleep BOOTSTRAP_DELAY
200
+ puts "\n"
201
+ }
202
+
203
+ config[:ssh_password] = server['password']
204
+
205
+ bootstrap_for_node(public_ip).run
206
+
207
+ puts "\n"
208
+ puts "#{ui.color("Name", :cyan)}: #{server['name']}"
209
+ puts "#{ui.color("Public IP", :cyan)}: #{public_ip}"
210
+ puts "#{ui.color("Environment", :cyan)}: #{config[:environment] || '_default'}"
211
+ puts "#{ui.color("Run List", :cyan)}: #{config[:run_list].join(', ')}"
212
+
213
+ end
214
+
215
+ def validate_options
216
+
217
+ unless locate_config_value :cloudstack_template
218
+ ui.error "Cloudstack template not specified"
219
+ exit 1
220
+ end
221
+
222
+ unless locate_config_value :cloudstack_service
223
+ ui.error "Cloudstack service offering not specified"
224
+ exit 1
225
+ end
226
+
227
+ identity_file = locate_config_value :identity_file
228
+ ssh_user = locate_config_value :ssh_user
229
+ ssh_password = locate_config_value :ssh_password
230
+ unless identity_file || (ssh_user && ssh_password)
231
+ ui.error("You must specify either an ssh identity file or an ssh user and password")
232
+ #exit 1
233
+ end
234
+ end
235
+
236
+
237
+ def find_or_create_public_ip(server, connection)
238
+ nic = connection.get_server_default_nic(server) || {}
239
+ puts "#{ui.color("Not allocating public IP for server", :red)}" unless config[:public_ip]
240
+
241
+ if (config[:public_ip] == false) || (nic['type'] != 'Virtual') then
242
+ nic['ipaddress']
243
+ else
244
+ # create ip address, ssh forwarding rule and optional forwarding rules
245
+ ip_address = connection.associate_ip_address(server['zoneid'])
246
+ ssh_rule = connection.create_port_forwarding_rule(ip_address['id'], "22", "TCP", "22", server['id'])
247
+ create_port_forwarding_rules(ip_address['id'], server['id'], connection)
248
+ ssh_rule['ipaddress']
249
+ end
250
+ end
251
+
252
+ def create_port_forwarding_rules(ip_address_id, server_id, connection)
253
+ rules = locate_config_value(:port_rules)
254
+ return unless rules
255
+
256
+ rules.each do |rule|
257
+ args = rule.split(':')
258
+ public_port = args[0]
259
+ private_port = args[1] || args[0]
260
+ protocol = args[2] || "TCP"
261
+ connection.create_port_forwarding_rule(ip_address_id, private_port, protocol, public_port, server_id)
262
+ end
263
+
264
+ end
265
+
266
+ #noinspection RubyArgCount,RubyResolve
267
+ def is_ssh_open?(ip)
268
+ s = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
269
+ sa = Socket.sockaddr_in(22, ip)
270
+
271
+ begin
272
+ s.connect_nonblock(sa)
273
+ rescue Errno::EINPROGRESS
274
+ resp = IO.select(nil, [s], nil, 1)
275
+ if resp.nil?
276
+ sleep SSH_POLL_INTERVAL
277
+ return false
278
+ end
279
+
280
+ begin
281
+ s.connect_nonblock(sa)
282
+ rescue Errno::EISCONN
283
+ Chef::Log.debug("sshd accepting connections on #{ip}")
284
+ yield
285
+ return true
286
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
287
+ sleep SSH_POLL_INTERVAL
288
+ return false
289
+ end
290
+ ensure
291
+ s && s.close
292
+ end
293
+ end
294
+
295
+
296
+ def bootstrap_for_node(host)
297
+ bootstrap = Chef::Knife::Bootstrap.new
298
+ bootstrap.name_args = [host]
299
+ bootstrap.config[:run_list] = config[:run_list]
300
+ bootstrap.config[:ssh_user] = config[:ssh_user]
301
+ bootstrap.config[:ssh_password] = config[:ssh_password]
302
+ bootstrap.config[:identity_file] = config[:identity_file]
303
+ bootstrap.config[:chef_node_name] = config[:chef_node_name] if config[:chef_node_name]
304
+ bootstrap.config[:prerelease] = config[:prerelease]
305
+ bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
306
+ bootstrap.config[:distro] = locate_config_value(:distro)
307
+ bootstrap.config[:use_sudo] = true
308
+ bootstrap.config[:template_file] = locate_config_value(:template_file)
309
+ bootstrap.config[:environment] = config[:environment]
310
+ # may be needed for vpc_mode
311
+ bootstrap.config[:no_host_key_verify] = config[:no_host_key_verify]
312
+ bootstrap
313
+ end
314
+
315
+ def locate_config_value(key)
316
+ key = key.to_sym
317
+ Chef::Config[:knife][key] || config[key]
318
+ end
319
+
320
+ end # class
321
+ end
@@ -0,0 +1,157 @@
1
+ #
2
+ # Author:: Ryan Holmes (<rholmes@edmunds.com>)
3
+ # Copyright:: Copyright (c) 2011 Edmunds, 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
+
21
+ module KnifeCloudstack
22
+ class CsServerDelete < Chef::Knife
23
+
24
+ deps do
25
+ require 'knife-cloudstack/connection'
26
+ require 'chef/api_client'
27
+ end
28
+
29
+ banner "knife cs server delete SERVER_NAME [SERVER_NAME ...] (options)"
30
+
31
+ option :cloudstack_url,
32
+ :short => "-U URL",
33
+ :long => "--cloudstack-url URL",
34
+ :description => "The CloudStack endpoint URL",
35
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
36
+
37
+ option :cloudstack_api_key,
38
+ :short => "-A KEY",
39
+ :long => "--cloudstack-api-key KEY",
40
+ :description => "Your CloudStack API key",
41
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
42
+
43
+ option :cloudstack_secret_key,
44
+ :short => "-K SECRET",
45
+ :long => "--cloudstack-secret-key SECRET",
46
+ :description => "Your CloudStack secret key",
47
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
48
+
49
+ def run
50
+
51
+ @name_args.each do |hostname|
52
+ server = connection.get_server(hostname)
53
+
54
+ if !server then
55
+ ui.error("Server '#{hostname}' not found")
56
+ next
57
+ end
58
+
59
+ if server['state'] == 'Destroyed' then
60
+ ui.warn("Server '#{hostname}' already destroyed")
61
+ next
62
+ end
63
+
64
+ puts "\n"
65
+ msg("Name", server['name'])
66
+ msg("Public IP", connection.get_server_public_ip(server) || '?')
67
+ msg("Service", server['serviceofferingname'])
68
+ msg("Template", server['templatename'])
69
+ msg("Domain", server['domain'])
70
+ msg("Zone", server['zonename'])
71
+ msg("State", server['state'])
72
+
73
+ puts "\n"
74
+ ui.confirm("Do you really want to delete this server")
75
+
76
+ print "#{ui.color("Waiting for deletion", :magenta)}"
77
+ disassociate_virtual_ip_address server
78
+ connection.delete_server hostname
79
+ puts "\n"
80
+ ui.msg("Deleted server #{hostname}")
81
+
82
+ # delete chef client and node
83
+ node_name = connection.get_server_fqdn server
84
+ ui.confirm("Do you want to delete the chef node and client '#{node_name}")
85
+ delete_node node_name
86
+ delete_client node_name
87
+ end
88
+
89
+ end
90
+
91
+ def disassociate_virtual_ip_address(server)
92
+ nic = server['nic'].first || {}
93
+ return unless nic['type'] == 'Virtual'
94
+
95
+ # get the ssh rule for this server
96
+ ssh_rule = connection.get_ssh_port_forwarding_rule(server)
97
+ return unless ssh_rule
98
+
99
+ # get all rules for the same ip address
100
+ rules = connection.list_port_forwarding_rules(ssh_rule['ipaddressid'])
101
+ return unless rules
102
+
103
+ # ensure ip address has rules only for this server
104
+ rules.each { |r|
105
+ return if r['virtualmachineid'] != server['id']
106
+ }
107
+
108
+ # dissassociate the ip address if all tests passed
109
+ connection.disassociate_ip_address(ssh_rule['ipaddressid'])
110
+ end
111
+
112
+ def delete_client(name)
113
+ begin
114
+ client = Chef::ApiClient.load(name)
115
+ rescue Net::HTTPServerException
116
+ return
117
+ end
118
+
119
+ client.destroy
120
+ ui.msg "Deleted client #{name}"
121
+ end
122
+
123
+ def delete_node(name)
124
+ begin
125
+ node = Chef::Node.load(name)
126
+ rescue Net::HTTPServerException
127
+ return
128
+ end
129
+
130
+ node.destroy
131
+ ui.msg "Deleted node #{name}"
132
+ end
133
+
134
+ def connection
135
+ unless @connection
136
+ @connection = CloudstackClient::Connection.new(
137
+ locate_config_value(:cloudstack_url),
138
+ locate_config_value(:cloudstack_api_key),
139
+ locate_config_value(:cloudstack_secret_key)
140
+ )
141
+ end
142
+ @connection
143
+ end
144
+
145
+ def msg(label, value)
146
+ if value && !value.empty?
147
+ puts "#{ui.color(label, :cyan)}: #{value}"
148
+ end
149
+ end
150
+
151
+ def locate_config_value(key)
152
+ key = key.to_sym
153
+ Chef::Config[:knife][key] || config[key]
154
+ end
155
+
156
+ end
157
+ end
@@ -0,0 +1,92 @@
1
+ #
2
+ # Author:: Ryan Holmes (<rholmes@edmunds.com>)
3
+ # Copyright:: Copyright (c) 2011 Edmunds, 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
+
21
+ module KnifeCloudstack
22
+ class CsServerList < Chef::Knife
23
+
24
+ deps do
25
+ require 'knife-cloudstack/connection'
26
+ end
27
+
28
+ banner "knife cs server list (options)"
29
+
30
+ option :cloudstack_url,
31
+ :short => "-U URL",
32
+ :long => "--cloudstack-url URL",
33
+ :description => "The CloudStack endpoint URL",
34
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
35
+
36
+ option :cloudstack_api_key,
37
+ :short => "-A KEY",
38
+ :long => "--cloudstack-api-key KEY",
39
+ :description => "Your CloudStack API key",
40
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
41
+
42
+ option :cloudstack_secret_key,
43
+ :short => "-K SECRET",
44
+ :long => "--cloudstack-secret-key SECRET",
45
+ :description => "Your CloudStack secret key",
46
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
47
+
48
+ def run
49
+
50
+ $stdout.sync = true
51
+
52
+ connection = CloudstackClient::Connection.new(
53
+ locate_config_value(:cloudstack_url),
54
+ locate_config_value(:cloudstack_api_key),
55
+ locate_config_value(:cloudstack_secret_key)
56
+ )
57
+
58
+ server_list = [
59
+ ui.color('Name', :bold),
60
+ ui.color('Public IP', :bold),
61
+ ui.color('Service', :bold),
62
+ ui.color('Template', :bold),
63
+ ui.color('State', :bold)
64
+ ]
65
+
66
+ servers = connection.list_servers
67
+ rules = connection.list_port_forwarding_rules
68
+
69
+ servers.each do |server|
70
+
71
+ name = server['name']
72
+ display_name = server['displayname']
73
+ if display_name && !display_name.empty? && display_name != name
74
+ name << " (#{display_name})"
75
+ end
76
+ server_list << server['name']
77
+ server_list << (connection.get_server_public_ip(server, rules) || '')
78
+ server_list << server['serviceofferingname']
79
+ server_list << server['templatename']
80
+ server_list << server['state']
81
+ end
82
+ puts ui.list(server_list, :columns_across, 5)
83
+
84
+ end
85
+
86
+ def locate_config_value(key)
87
+ key = key.to_sym
88
+ Chef::Config[:knife][key] || config[key]
89
+ end
90
+
91
+ end
92
+ end