knife-scaleway 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+ require 'rubygems'
3
+ require 'bundler'
4
+ Bundler.setup
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ require 'rspec/core/rake_task'
8
+
9
+ RSpec::Core::RakeTask.new(:spec)
10
+
11
+ require 'rubocop/rake_task'
12
+ desc 'Run RuboCop on the lib directory'
13
+ RuboCop::RakeTask.new(:rubocop) do |task|
14
+ task.patterns = ['lib/**/*.rb']
15
+ end
16
+
17
+ desc 'Display LOC stats'
18
+ task :loc do
19
+ puts "\n## LOC Stats"
20
+ sh 'countloc -r lib/chef/knife'
21
+ end
22
+
23
+ task default: :spec
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'knife-scaleway/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'knife-scaleway'
8
+ gem.version = Knife::Scaleway::VERSION
9
+ gem.authors = ['Lukas Diener']
10
+ gem.email = ['lukas.diener@hotmail.com']
11
+ gem.description = "A plugin for chef's knife to manage instances of Scaleway servers"
12
+ gem.summary = "A plugin for chef's knife to manage instances of Scaleway servers"
13
+ gem.homepage = 'https://github.com/LukasSkywalker/knife-scaleway'
14
+ gem.license = 'Apache 2.0'
15
+
16
+ gem.add_dependency 'chef', '>= 10.18'
17
+
18
+ gem.add_development_dependency 'rspec', '~> 3.1'
19
+ gem.add_development_dependency 'rubocop', '~> 0.27'
20
+ gem.add_development_dependency 'rake'
21
+ gem.add_development_dependency 'knife-solo'
22
+ gem.add_development_dependency 'knife-zero'
23
+ gem.add_development_dependency 'webmock', '~> 1.20'
24
+ gem.add_development_dependency 'vcr', '~> 2.9'
25
+ gem.add_development_dependency 'guard', '~> 2.8'
26
+ gem.add_development_dependency 'guard-rspec', '~> 4.3'
27
+ gem.add_development_dependency 'coveralls'
28
+ gem.add_development_dependency 'countloc'
29
+ gem.add_development_dependency 'simplecov'
30
+ gem.add_development_dependency 'simplecov-console'
31
+
32
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
33
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
34
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
35
+ gem.require_paths = ['lib']
36
+ end
@@ -0,0 +1,110 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+
4
+ module Scaleway
5
+ class Client
6
+ attr_accessor :access_key, :token
7
+
8
+ def initialize(access_key, token)
9
+ @host1 = 'https://api.scaleway.com'
10
+ @host2 = 'https://account.scaleway.com'
11
+ @access_key = access_key
12
+ @token = token
13
+ @instance = self
14
+ end
15
+
16
+ def self.instance
17
+ #raise StandardError, 'Create client before accessing methods' if @instance.nil?
18
+ #@instance
19
+ Scaleway::Client.new(Chef::Config[:knife][:scaleway_access_key], Chef::Config[:knife][:scaleway_token])
20
+ end
21
+
22
+ def request(path, method, payload = nil)
23
+ host = @host1 if path.index('/servers') || path.index('/images') || path.index('/volumes') || path.index('/ips')
24
+ host = @host2 if path.index('/organizations')
25
+ raise StandardError, "Add /#{path.split('/')[1]} to host map" if host.nil?
26
+
27
+ headers = {:'X-Auth-Token' => @token, :'Content-Type' => 'application/json'}
28
+ url = host + path
29
+
30
+ options = { url: url, method: method, verify_ssl: false, headers: headers }
31
+ if method == :post
32
+ options.merge!(payload: payload)
33
+ puts payload if ENV['DEBUG']
34
+ end
35
+
36
+ begin
37
+ puts "### Req #{method.upcase} #{host + path}" if ENV['DEBUG']
38
+ JSON.parse(RestClient::Request.execute(options).body, object_class: OpenStruct, array_class: Array)
39
+ rescue => e
40
+ data = JSON.parse(e.response, object_class: OpenStruct)
41
+ puts "#{data.type}: #{data.message}"
42
+ end
43
+ end
44
+
45
+ def get(path)
46
+ request(path, :get)
47
+ end
48
+
49
+ def post(path, data)
50
+ request(path, :post, data)
51
+ end
52
+
53
+ def put(path, data)
54
+ request(path, :put, data)
55
+ end
56
+ end
57
+
58
+ class Organization
59
+ def self.all
60
+ Client.instance.get('/organizations')
61
+ end
62
+ end
63
+
64
+ class Ip
65
+ def self.all
66
+ Client.instance.get('/ips').ips
67
+ end
68
+ end
69
+
70
+ class Image
71
+ def self.all
72
+ Client.instance.get('/images').images
73
+ end
74
+
75
+ def self.find(query)
76
+ response = Client.instance.get('/images')
77
+ response.images.select do |image|
78
+ image.name.downcase.index(query.downcase)
79
+ end
80
+ end
81
+ end
82
+
83
+ class Server
84
+ def self.all
85
+ Client.instance.get('/servers').servers
86
+ end
87
+
88
+ def self.find(id)
89
+ Client.instance.get("/servers/#{id}").server
90
+ end
91
+
92
+ def self.create(name, image, commercial_type)
93
+ Client.instance.post('/servers', { name: name, organization: Client.instance.access_key, image: image, commercial_type: commercial_type}.to_json).server
94
+ end
95
+
96
+ def self.actions(id)
97
+ Client.instance.get("/servers/#{id}/action")
98
+ end
99
+
100
+ def self.action(id, act)
101
+ Client.instance.post("/servers/#{id}/action", { action: act }.to_json)
102
+ end
103
+ end
104
+
105
+ class Volume
106
+ def self.all
107
+ Client.instance.get('/volumes').volumes
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,45 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+ #
13
+ require 'chef/knife/scaleway_base'
14
+
15
+ class Chef
16
+ class Knife
17
+ class ScalewayAccountInfo < Knife
18
+ include Knife::ScalewayBase
19
+
20
+ banner 'knife scaleway account info (options)'
21
+
22
+ def run
23
+ $stdout.sync = true
24
+
25
+ validate!
26
+
27
+ account_info = [
28
+ ui.color('UUID', :bold),
29
+ ui.color('Email', :bold),
30
+ ui.color('Droplet Limit', :bold),
31
+ ui.color('Email Verified', :bold)
32
+ ]
33
+
34
+ account = client.account.info
35
+
36
+ account_info << account.uuid.to_s
37
+ account_info << account.email.to_s
38
+ account_info << account.server_limit.to_s
39
+ account_info << account.email_verified.to_s
40
+
41
+ puts ui.list(account_info, :uneven_columns_across, 4)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,89 @@
1
+ # lots of awesome stoff stolen from opscode/knife-azure ;-)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ class Chef
16
+ class Knife
17
+ module ScalewayBase
18
+ def self.load_deps
19
+ require_relative 'scaleway'
20
+ require 'json'
21
+ require 'chef/mixin/shell_out'
22
+ end
23
+
24
+ def self.included(includer)
25
+ includer.class_eval do
26
+ category 'scaleway'
27
+
28
+ # Lazy load our dependencies. Later calls to `Knife#deps` override
29
+ # previous ones, so if the including class calls it, it needs to also
30
+ # call our #load_deps, i.e:
31
+ #
32
+ # Include Chef::Knife::ScalewayBase
33
+ #
34
+ # deps do
35
+ # require 'foo'
36
+ # require 'bar'
37
+ # Chef::Knife::ScalewayBase.load_deps
38
+ # end
39
+ #
40
+ deps { Chef::Knife::ScalewayBase.load_deps }
41
+
42
+ option :scaleway_access_token,
43
+ short: '-A ACCESS_TOKEN',
44
+ long: '--scaleway_access_token ACCESS_TOKEN',
45
+ description: 'Your Scaleway ACCESS_TOKEN',
46
+ proc: proc { |access_token| Chef::Config[:knife][:scaleway_access_token] = access_token }
47
+ end
48
+ end
49
+
50
+ def client
51
+ Scaleway::Client.new(Chef::Config[:knife][:scaleway_access_key], Chef::Config[:knife][:scaleway_token])
52
+ end
53
+
54
+ def validate!(keys = [:scaleway_access_key, :scaleway_token])
55
+ errors = []
56
+
57
+ keys.each do |k|
58
+ if locate_config_value(k).nil?
59
+ errors << "You did not provide a valid '#{k}' value. " \
60
+ "Please set knife[:#{k}] in your knife.rb or pass as an option."
61
+ end
62
+ end
63
+
64
+ exit 1 if errors.each { |e| ui.error(e) }.any?
65
+ end
66
+
67
+ def locate_config_value(key)
68
+ key = key.to_sym
69
+ config[key] || Chef::Config[:knife][key]
70
+ end
71
+
72
+ def wait_for_status(result, status: 'in-progress', sleep: 3)
73
+ print "Waiting for state #{status}"
74
+ result = Scaleway::Server.find(locate_config_value(:id))
75
+ while result.state != status
76
+ sleep sleep
77
+ print('.')
78
+
79
+ #if status == 'starting' || status == 'stopping'
80
+ #break if client.servers.find(id: locate_config_value(:id)).status != 'in-progress'
81
+ #else
82
+ break if Scaleway::Server.find(locate_config_value(:id)).state == status
83
+ #end
84
+ end
85
+ ui.info 'OK'
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,338 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+ #
13
+ require 'chef/knife/scaleway_base'
14
+
15
+ class Chef
16
+ class Knife
17
+ class ScalewayServerCreate < Knife
18
+ include Knife::ScalewayBase
19
+
20
+ deps do
21
+ require 'socket'
22
+ require 'chef/knife/bootstrap'
23
+ Chef::Knife::Bootstrap.load_deps
24
+ Chef::Knife::ScalewayBase.load_deps
25
+ # Knife loads subcommands automatically, so we can just check if the
26
+ # class exists.
27
+ Chef::Knife::SoloBootstrap.load_deps if defined? Chef::Knife::SoloBootstrap
28
+ if defined? Chef::Knife::ZeroBootstrap
29
+ Chef::Knife::ZeroBootstrap.load_deps
30
+ self.options = Chef::Knife::ZeroBootstrap.options.merge(options)
31
+ end
32
+ end
33
+
34
+ banner 'knife scaleway server create (options)'
35
+
36
+ option :server_name,
37
+ short: '-N NAME',
38
+ long: '--server-name NAME',
39
+ description: 'The server name',
40
+ proc: proc { |server_name| Chef::Config[:knife][:server_name] = server_name }
41
+
42
+ option :image,
43
+ short: '-I IMAGE',
44
+ long: '--image IMAGE',
45
+ description: 'Your Scaleway Image',
46
+ proc: proc { |image| Chef::Config[:knife][:image] = image }
47
+
48
+ option :size,
49
+ short: '-S SIZE',
50
+ long: '--size SIZE',
51
+ description: 'Your Scaleway Size',
52
+ proc: proc { |size| Chef::Config[:knife][:size] = size }
53
+
54
+ option :location,
55
+ short: '-L REGION',
56
+ long: '--location REGION',
57
+ description: 'Scaleway Location (Region)',
58
+ proc: proc { |location| Chef::Config[:knife][:location] = location }
59
+
60
+ option :ssh_key_ids,
61
+ short: '-K KEYID',
62
+ long: '--ssh-keys KEY_ID',
63
+ description: 'Comma spearated list of your SSH key ids',
64
+ proc: ->(o) { o.split(/[\s,]+/) }
65
+
66
+ option :identity_file,
67
+ short: '-i IDENTITY_FILE',
68
+ long: '--identity-file IDENTITY_FILE',
69
+ description: 'The SSH identity file used for authentication',
70
+ proc: proc { |identity| Chef::Config[:knife][:identity_file] = identity }
71
+
72
+ option :bootstrap,
73
+ short: '-B',
74
+ long: '--bootstrap',
75
+ description: 'Do a chef-client bootstrap on the created server (for use with chef-server)'
76
+
77
+ option :solo,
78
+ long: '--[no-]solo',
79
+ description: 'Do a chef-solo bootstrap on the server using knife-solo',
80
+ proc: proc { |s| Chef::Config[:knife][:solo] = s }
81
+
82
+ option :zero,
83
+ long: '--[no-]zero',
84
+ description: 'Do a chef-zero bootstrap on the server using knife-zero',
85
+ proc: proc { |z| Chef::Config[:knife][:zero] = z }
86
+
87
+ option :ssh_user,
88
+ short: '-x USERNAME',
89
+ long: '--ssh-user USERNAME',
90
+ description: 'The ssh username; default is "root"',
91
+ default: 'root'
92
+
93
+ option :distro,
94
+ short: '-d DISTRO',
95
+ long: '--distro DISTRO',
96
+ description: 'Chef-Bootstrap a distro using a template; default is "chef-full"',
97
+ proc: proc { |d| Chef::Config[:knife][:distro] = d },
98
+ default: 'chef-full'
99
+
100
+ option :run_list,
101
+ short: '-r RUN_LIST',
102
+ long: '--run-list RUN_LIST',
103
+ description: 'Comma separated list of roles/recipes to apply',
104
+ proc: ->(o) { o.split(/[\s,]+/) },
105
+ default: []
106
+
107
+ option :template_file,
108
+ long: '--template-file TEMPLATE',
109
+ description: 'Full path to location of template to use',
110
+ proc: proc { |t| Chef::Config[:knife][:template_file] = t },
111
+ default: false
112
+
113
+ option :host_key_verify,
114
+ long: '--[no-]host-key-verify',
115
+ description: 'Verify host key, enabled by default',
116
+ default: true
117
+
118
+ option :prerelease,
119
+ long: '--prerelease',
120
+ description: 'Install the pre-release chef gems'
121
+
122
+ option :bootstrap_version,
123
+ long: '--bootstrap-version VERSION',
124
+ description: 'The version of Chef to install',
125
+ proc: proc { |v| Chef::Config[:knife][:bootstrap_version] = v }
126
+
127
+ option :environment,
128
+ short: '-E ENVIRONMENT',
129
+ long: '--environment ENVIRONMENT',
130
+ description: 'The name of the chef environment to use',
131
+ proc: proc { |e| Chef::Config[:knife][:environment] = e },
132
+ default: '_default'
133
+
134
+ option :json_attributes,
135
+ short: '-j JSON',
136
+ long: '--json-attributes JSON',
137
+ description: 'A JSON string to be added to the first run of chef-client',
138
+ proc: ->(o) { JSON.parse(o) }
139
+
140
+ option :private_networking,
141
+ long: '--private_networking',
142
+ description: 'Enables private networking if the selected region supports it',
143
+ default: false
144
+
145
+ option :secret_file,
146
+ long: '--secret-file SECRET_FILE',
147
+ description: 'A file containing the secret key to use to encrypt data bag item values',
148
+ proc: proc { |sf| Chef::Config[:knife][:secret_file] = sf }
149
+
150
+ option :ssh_port,
151
+ short: '-p PORT',
152
+ long: '--ssh-port PORT',
153
+ description: 'The ssh port',
154
+ default: '22',
155
+ proc: proc { |port| Chef::Config[:knife][:ssh_port] = port }
156
+
157
+ option :backups,
158
+ short: '-b',
159
+ long: '--backups-enabled',
160
+ description: 'Enables backups for the created server',
161
+ default: false
162
+
163
+ option :ipv6,
164
+ short: '-6',
165
+ long: '--ipv6-enabled',
166
+ description: 'Enables ipv6 for the created server',
167
+ default: false
168
+
169
+ def run
170
+ $stdout.sync = true
171
+
172
+ validate!
173
+
174
+ unless locate_config_value(:server_name)
175
+ ui.error('Server Name cannot be empty: -N <servername>')
176
+ exit 1
177
+ end
178
+
179
+ unless locate_config_value(:image)
180
+ ui.error('Image cannot be empty: -I <image>')
181
+ exit 1
182
+ end
183
+ =begin
184
+ unless locate_config_value(:size)
185
+ ui.error('Size cannot be empty: -S <size>')
186
+ exit 1
187
+ end
188
+
189
+ unless locate_config_value(:location)
190
+ ui.error('Location cannot be empty: -L <region>')
191
+ exit 1
192
+ end
193
+
194
+ unless locate_config_value(:ssh_key_ids)
195
+ ui.error('One or more Scaleway SSH key ids missing: -K <KEY1>, <KEY2> ...')
196
+ exit 1
197
+ end
198
+ =end
199
+ if solo_bootstrap? && !defined?(Chef::Knife::SoloBootstrap)
200
+ ui.error [
201
+ 'Knife plugin knife-solo was not found.',
202
+ 'Please add the knife-solo gem to your Gemfile or',
203
+ 'install it manually with `gem install knife-solo`.'
204
+ ].join(' ')
205
+ exit 1
206
+ end
207
+
208
+ if zero_bootstrap? && !defined?(Chef::Knife::ZeroBootstrap)
209
+ ui.error [
210
+ 'Knife plugin knife-zero was not found.',
211
+ 'Please add the knife-zero gem to your Gemfile or',
212
+ 'install it manually with `gem install knife-zero`.'
213
+ ].join(' ')
214
+ exit 1
215
+ end
216
+ =begin
217
+ server = DropletKit::Droplet.new(name: locate_config_value(:server_name),
218
+ size: locate_config_value(:size),
219
+ image: locate_config_value(:image),
220
+ region: locate_config_value(:location),
221
+ ssh_keys: locate_config_value(:ssh_key_ids),
222
+ private_networking: locate_config_value(:private_networking),
223
+ backups: locate_config_value(:backups),
224
+ ipv6: locate_config_value(:ipv6)
225
+ )
226
+ =end
227
+ server = Scaleway::Server.create(locate_config_value(:server_name), locate_config_value(:image), 'VC1S')
228
+
229
+ #server = client.servers.create(server)
230
+
231
+ if Scaleway::Server.find(server.id).state != 'stopped'
232
+ ui.error("Droplet could not be started #{server.inspect}")
233
+ exit 1
234
+ end
235
+
236
+ puts "Droplet creation for #{locate_config_value(:server_name)} started. Droplet-ID is #{server.id}"
237
+
238
+ unless !config.key?(:json_attributes) || config[:json_attributes].empty?
239
+ puts ui.color("JSON Attributes: #{config[:json_attributes]}", :magenta)
240
+ end
241
+
242
+ puts "Starting server #{server.id}"
243
+
244
+ Scaleway::Server.action(server.id, 'poweron')
245
+
246
+ print ui.color('Waiting for IPv4-Address', :magenta)
247
+ print('.') until ip_address = ip_address_available(server.id) do
248
+ puts 'done'
249
+ end
250
+
251
+ puts ui.color("IPv4 address is: #{ip_address.address}", :green)
252
+
253
+ print ui.color('Waiting for sshd:', :magenta)
254
+ print('.') until tcp_test_ssh(ip_address.address) do
255
+ sleep 2
256
+ puts 'done'
257
+ end
258
+
259
+ if locate_config_value(:bootstrap) || solo_bootstrap? || zero_bootstrap?
260
+ bootstrap_for_node(ip_address.address).run
261
+ else
262
+ puts ip_address.address
263
+ exit 0
264
+ end
265
+ end
266
+
267
+ def ip_address_available(server_id)
268
+ server = Scaleway::Server.find(server_id)
269
+ if server.public_ip
270
+ yield
271
+ server.public_ip
272
+ else
273
+ sleep @initial_sleep_delay ||= 10
274
+ false
275
+ end
276
+ end
277
+
278
+ def tcp_test_ssh(hostname)
279
+ port = Chef::Config[:knife][:ssh_port] || config[:ssh_port]
280
+ tcp_socket = TCPSocket.new(hostname, port)
281
+ readable = IO.select([tcp_socket], nil, nil, 5)
282
+ if readable
283
+ Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
284
+ yield
285
+ true
286
+ else
287
+ false
288
+ end
289
+ rescue Errno::ETIMEDOUT
290
+ false
291
+ rescue Errno::EPERM
292
+ false
293
+ rescue Errno::ECONNREFUSED
294
+ sleep 2
295
+ false
296
+ rescue Errno::EHOSTUNREACH
297
+ sleep 2
298
+ false
299
+ ensure
300
+ tcp_socket && tcp_socket.close
301
+ end
302
+
303
+ def bootstrap_for_node(ip_address)
304
+ bootstrap = bootstrap_class.new
305
+ bootstrap.name_args = [ip_address]
306
+ bootstrap.config.merge! config
307
+ bootstrap.config[:chef_node_name] = locate_config_value(:server_name)
308
+ bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
309
+ bootstrap.config[:ssh_port] = Chef::Config[:knife][:ssh_port] || config[:ssh_port]
310
+ bootstrap.config[:distro] = locate_config_value(:distro)
311
+ bootstrap.config[:use_sudo] = true unless config[:ssh_user] == 'root'
312
+ bootstrap.config[:template_file] = locate_config_value(:template_file)
313
+ bootstrap.config[:environment] = locate_config_value(:environment)
314
+ bootstrap.config[:first_boot_attributes] = locate_config_value(:json_attributes) || {}
315
+ bootstrap.config[:secret_file] = locate_config_value(:secret_file) || {}
316
+ bootstrap
317
+ end
318
+
319
+ def bootstrap_class
320
+ if solo_bootstrap?
321
+ Chef::Knife::SoloBootstrap
322
+ elsif zero_bootstrap?
323
+ Chef::Knife::ZeroBootstrap
324
+ else
325
+ Chef::Knife::Bootstrap
326
+ end
327
+ end
328
+
329
+ def solo_bootstrap?
330
+ config[:solo] || (config[:solo].nil? && Chef::Config[:knife][:solo])
331
+ end
332
+
333
+ def zero_bootstrap?
334
+ config[:zero] || (config[:zero].nil? && Chef::Config[:knife][:zero])
335
+ end
336
+ end
337
+ end
338
+ end