ncc-api 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,590 @@
1
+ #!ruby
2
+ # /* Copyright 2013 Proofpoint, Inc. All rights reserved.
3
+ # Copyright 2014 Evernote Corporation. All rights reserved.
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
+ require 'rubygems'
19
+ require 'fog'
20
+ require 'uuidtools'
21
+
22
+ class NCC
23
+
24
+ end
25
+
26
+ class NCC::Connection
27
+
28
+ attr_reader :fog
29
+
30
+ def self.connect(ncc, cloud=:default, opt={})
31
+ cfg = ncc.config
32
+ provider = cfg[:clouds][cloud]['provider']
33
+ case provider
34
+ when 'aws'
35
+ require 'ncc/connection/aws'
36
+ NCC::Connection::AWS.new(ncc, cloud, opt)
37
+ when 'openstack'
38
+ require 'ncc/connection/openstack'
39
+ NCC::Connection::OpenStack.new(ncc, cloud, opt)
40
+ end
41
+ end
42
+
43
+ def initialize(ncc, cloud, opt={})
44
+ @ncc = ncc
45
+ @cache = { }
46
+ @cfg = ncc.config
47
+ @cfg_mtime = @cfg.mtime
48
+ @cloud = cloud
49
+ @logger = opt[:logger] if opt.has_key? :logger
50
+ @cache_timeout = opt[:cache_timeout] || 600
51
+ @auth_retry_limit = opt[:auth_retry_limit] || 1
52
+ do_connect
53
+ end
54
+
55
+ def current?
56
+ provider == @cfg[:clouds][@cloud]['provider'] and
57
+ @cfg_mtime == @cfg[:clouds][@cloud].mtime
58
+ end
59
+
60
+ def maybe_invalidate(type)
61
+ if @cache.has_key? type
62
+ @cache.delete(type) if
63
+ (Time.now - @cache[type][:timestamp]) > @cache_timeout
64
+ end
65
+ end
66
+
67
+ def me
68
+ [self.class, provider, @cloud].join(' ')
69
+ end
70
+
71
+ def to_s
72
+ '#<' + me + '>'
73
+ end
74
+
75
+ def warn(msg)
76
+ if @logger and @logger.respond_to? :warn
77
+ @logger.warn "#{self} #{msg}"
78
+ end
79
+ end
80
+
81
+ def debug(msg=nil)
82
+ msg ||= yield if block_given?
83
+ if @logger and @logger.respond_to? :debug
84
+ @logger.debug "#{self} #{msg}"
85
+ end
86
+ end
87
+
88
+ def notice(msg)
89
+ if @logger and @logger.respond_to? :notice
90
+ @logger.notice "#{self} #{msg}"
91
+ end
92
+ end
93
+
94
+ def info(msg)
95
+ if @logger and @logger.respond_to? :info
96
+ @logger.info "#{self} #{msg}"
97
+ end
98
+ end
99
+
100
+ def connection_params
101
+ []
102
+ end
103
+
104
+ def do_connect
105
+ cloud_config = @cfg[:clouds][@cloud]
106
+ pnames = connection_params + ['provider']
107
+ params = Hash[cloud_config.to_hash(*pnames).map do |k, v|
108
+ [k.intern, v]
109
+ end ]
110
+ @fog = Fog::Compute.new(params)
111
+ end
112
+
113
+ def do_fog
114
+ do_connect unless @fog
115
+ remaining_tries = @auth_retry_limit
116
+ begin
117
+ yield @fog
118
+ rescue Excon::Errors::Unauthorized => err
119
+ # might be an expired auth token, retry
120
+ if remaining_tries > 0
121
+ do_connect
122
+ remaining_tries -= 1
123
+ retry
124
+ else
125
+ @fog = nil
126
+ raise NCC::Error::Cloud, "Error communicating with #{provider} " +
127
+ "cloud #{@cloud} (#{e.class}): #{e.message}"
128
+ end
129
+ rescue NCC::Error::Cloud => err
130
+ @fog = nil
131
+ raise err
132
+ rescue NCC::Error => err
133
+ raise err
134
+ rescue StandardError => err
135
+ @fog = nil
136
+ raise NCC::Error::Cloud, "Error communicating with #{provider} " +
137
+ "cloud #{@cloud} [#{err.class}]: #{err.message}"
138
+ end
139
+ end
140
+
141
+ # Warning. use_only_mapped is ignored for types with
142
+ # alternate id fields
143
+ def use_only_mapped(type)
144
+ false
145
+ end
146
+
147
+ def fields(obj, *flist)
148
+ Hash[flist.map { |f| [f, obj.send(f)] }]
149
+ end
150
+
151
+ def ids(a)
152
+ Hash[a.map { |e| [e['id'], e] }]
153
+ end
154
+
155
+ def provider
156
+ generic
157
+ end
158
+
159
+ def keyname_for(type)
160
+ case type
161
+ when :size
162
+ 'sizes'
163
+ when :image
164
+ 'images'
165
+ end
166
+ end
167
+
168
+ # Something like "images" => { "$abstract_id" => x }
169
+ # can have an x where it's the provider id, or an x
170
+ # which is a hash. If the hash has an id key, than that
171
+ # is used as a mapped id, if it doesn't, it's ignored
172
+ # (for the purposes of id mapping) -jbrinkley/20130410
173
+ def id_map_hash(h)
174
+ Hash[
175
+ h.map do |k, v|
176
+ if v.respond_to? :has_key?
177
+ if v.has_key? 'id'
178
+ [k, v['id']]
179
+ end
180
+ else
181
+ [k, v]
182
+ end
183
+ end.reject { |p| p.nil? }
184
+ ]
185
+ end
186
+
187
+ def id_map(type)
188
+ debug "making id_map for #{type.inspect}"
189
+ keyname = keyname_for(type)
190
+ r = { }
191
+ debug "keyname = #{keyname} (for #{type.inspect})"
192
+ debug "provider map: #{@cfg[:providers][provider].to_hash.inspect}"
193
+ r.update(id_map_hash(@cfg[:providers][provider][keyname].to_hash)) if
194
+ @cfg[:providers][provider].has_key? keyname
195
+ #debug "cloud map: #{@cfg[:clouds][@cloud][keyname].to_hash}"
196
+ r.update(id_map_hash(@cfg[:clouds][@cloud][keyname].to_hash)) if
197
+ @cfg[:clouds][@cloud].has_key? keyname
198
+ debug " >> #{r.inspect}"
199
+ r
200
+ end
201
+
202
+ def provider_id_map(type)
203
+ debug "making provider_id_map for #{type.inspect}"
204
+ keyname = keyname_for(type)
205
+ r = { }
206
+ r.update(id_map_hash(@cfg[:providers][provider][keyname].
207
+ to_hash).invert) if
208
+ @cfg[:providers][provider].has_key? keyname
209
+ r.update(id_map_hash(@cfg[:clouds][@cloud][keyname].
210
+ to_hash).invert) if
211
+ @cfg[:clouds][@cloud].has_key? keyname
212
+ debug " >>> #{r.inspect}"
213
+ r
214
+ end
215
+
216
+ def map_to_id(type, provider_id)
217
+ debug "provider_id_map = #{provider_id_map(type).inspect}"
218
+ provider_id_map(type)[provider_id] || provider_id
219
+ end
220
+
221
+ def map_to_provider_id(type, abstract_id)
222
+ id_map(type)[abstract_id] || abstract_id
223
+ end
224
+
225
+ # raw meaning not using "our" notion of id_field
226
+ def map_from_raw_provider_id(type, raw_provider_id)
227
+ # Using collection here for cache
228
+ provider_object = collection(type).find do |item|
229
+ item.id == raw_provider_id
230
+ end
231
+ id_of(type, provider_object)
232
+ end
233
+
234
+ def map_to_status(provider_status)
235
+ provider_status
236
+ end
237
+
238
+ def map_to_provider_status(abstract_status)
239
+ abstract_status
240
+ end
241
+
242
+ def sizes(size_id=nil)
243
+ debug "sizes(#{size_id.inspect})"
244
+ if size_id.nil?
245
+ collection(:size).find_all do |flavor|
246
+ @cfg[:sizes].has_key? id_of(:size, flavor)
247
+ end.map { |f| object_of(:size, f) }
248
+ else
249
+ object_of(:size,
250
+ get_provider_object(:size => size_id))
251
+ end
252
+ end
253
+
254
+ def images(image_id=nil)
255
+ if image_id.nil?
256
+ collection(:image).find_all do |provider_image|
257
+ @cfg[:images].has_key? id_of(:image, provider_image)
258
+ end.map { |i| object_of(:image, i) }
259
+ else
260
+ object_of(:image,
261
+ get_provider_object(:image => image_id))
262
+ end
263
+ end
264
+
265
+ def id_field(abstract_object_type)
266
+ methodname = abstract_object_type.to_s + '_id_field'
267
+ if self.respond_to? methodname
268
+ self.send methodname
269
+ else
270
+ :id
271
+ end
272
+ end
273
+
274
+ def id_of(abstract_object_type, obj)
275
+ map_to_id(abstract_object_type,
276
+ translated_id_of(abstract_object_type, obj))
277
+ end
278
+
279
+ def translated_id_of(abstract_object_type, obj)
280
+ id_fielder = id_field abstract_object_type
281
+ if id_fielder.kind_of? Proc
282
+ id_fielder.call obj
283
+ elsif obj.nil?
284
+ nil
285
+ else
286
+ obj.send id_fielder
287
+ end
288
+ end
289
+
290
+ def collection(type, key=nil)
291
+ maybe_invalidate type
292
+ @cache[type] ||= {
293
+ :timestamp => Time.now,
294
+ :data => Hash[get_provider_objects(type).map { |o| [o.id, o] }]
295
+ }
296
+ if key.nil?
297
+ @cache[type][:data].values
298
+ else
299
+ @cache[type][:data][key]
300
+ end
301
+ end
302
+
303
+ def get_provider_objects(type)
304
+ enum = case type
305
+ when :size
306
+ :flavors
307
+ when :image
308
+ :images
309
+ end
310
+ if use_only_mapped(type) and id_field(type) == :id
311
+ id_map(type).values.map do |id|
312
+ do_fog { |fog| fog.send(enum).get(id) }
313
+ end
314
+ else
315
+ do_fog { |fog| fog.send(enum) }
316
+ end
317
+ end
318
+
319
+ def get_provider_object(provider_object_spec)
320
+ abstract_object_type, abstract_object_id = provider_object_spec.first
321
+ if id_field(abstract_object_type) != :id
322
+ collection(abstract_object_type).find do |provider_object|
323
+ id_of(abstract_object_type, provider_object) ==
324
+ abstract_object_id
325
+ end
326
+ else
327
+ provider_object_id = map_to_provider_id(abstract_object_type,
328
+ abstract_object_id)
329
+ collection(abstract_object_type, provider_object_id)
330
+ end
331
+ end
332
+
333
+ def translate(abstract_object_type, provider_object)
334
+ debug "translate(#{abstract_object_type.inspect}, #{provider_object})"
335
+ methodname = 'translate_' + abstract_object_type.to_s
336
+ if self.respond_to? methodname
337
+ self.send(methodname, provider_object)
338
+ else
339
+ generic_translate(abstract_object_type, provider_object)
340
+ end
341
+ end
342
+
343
+ def merge_configured(abstract_object, data)
344
+ if data.respond_to? :each_pair
345
+ data.each_pair { |k, v| abstract_object[k] ||= v }
346
+ else
347
+ abstract_object['provider_id'] ||= data
348
+ end
349
+ end
350
+
351
+ def object_of(abstract_object_type, provider_object)
352
+ debug "object_of(#{abstract_object_type.inspect}, #{provider_object})"
353
+ abstract_object = translate(abstract_object_type, provider_object)
354
+ keyname = keyname_for abstract_object_type
355
+ id = abstract_object['id']
356
+ [@cfg[:clouds][@cloud], @cfg[:providers][provider]].each do |cfg|
357
+ if cfg.has_key? keyname and cfg[keyname].has_key? id
358
+ debug "merging cloud/provider data: cfg[#{keyname}][#{id}]"
359
+ merge_configured(abstract_object, cfg[keyname][id])
360
+ end
361
+ end
362
+ if @cfg.has_key? keyname.intern and @cfg[keyname.intern].has_key? id
363
+ debug "merging abstract data: " +
364
+ "@cfg[#{keyname.intern.inspect}][#{id}]"
365
+ merge_configured(abstract_object, @cfg[keyname.intern][id].to_hash)
366
+ end
367
+ debug "object_of returns: #{abstract_object.inspect}"
368
+ abstract_object
369
+ end
370
+
371
+ def generic_translate(abstract_object_type, provider_object)
372
+ abstract_object = {
373
+ 'id' => id_of(abstract_object_type, provider_object),
374
+ 'provider_id' => provider_object.id
375
+ }
376
+ [:name, :description].each do |field|
377
+ abstract_object[field.to_s] = provider_object.send(field) if
378
+ provider_object.respond_to? field
379
+ end
380
+ abstract_object
381
+ end
382
+
383
+ def instance_for(server)
384
+ instance = NCC::Instance.new(@cfg, :logger => @logger)
385
+ instance.set_without_validation(:id => server.id)
386
+ instance.set_without_validation(:name => instance_name(server))
387
+ instance.set_without_validation(:size => instance_size(server))
388
+ instance.set_without_validation(:image => instance_image(server))
389
+ instance.set_without_validation(:ip_address =>
390
+ instance_ip_address(server))
391
+ instance.set_without_validation(:host => instance_host(server))
392
+ instance.set_without_validation(:status => instance_status(server))
393
+ instance
394
+ end
395
+
396
+ def instance_name(server)
397
+ server.name
398
+ end
399
+
400
+ def instance_size(server)
401
+ map_to_id(:size, server.size)
402
+ end
403
+
404
+ def instance_image(server)
405
+ map_to_id(:image, server.image)
406
+ end
407
+
408
+ def instance_status(server)
409
+ map_to_status(server.status)
410
+ end
411
+
412
+ def instance_host(server)
413
+ nil
414
+ end
415
+
416
+ def instance_ip_address(server)
417
+ server.ip_address
418
+ end
419
+
420
+ def instances(instance_id=nil)
421
+ debug "instances(#{instance_id.inspect})"
422
+ if instance_id.nil?
423
+ do_fog { |fog| fog.servers }.map { |server| instance_for server }
424
+ else
425
+ server = do_fog { |fog| fog.servers.get instance_id }
426
+ if server.nil?
427
+ instance_not_found instance_id
428
+ end
429
+ instance_for server
430
+ end
431
+ end
432
+
433
+ def provider_request(instance)
434
+ keyintern(
435
+ provider_request_of(instance.
436
+ with_defaults(@cfg[:clouds][@cloud]['defaults'],
437
+ @cfg[:providers][provider]['defaults']))
438
+ )
439
+ end
440
+
441
+ def provider_request_of(instance)
442
+ generic_provider_request(instance)
443
+ end
444
+
445
+ def generic_provider_request(instance)
446
+ {
447
+ :name => instance.name,
448
+ :flavor => map_to_provider_id(:size, instance.size),
449
+ :size => map_to_provider_id(:image, instance.image),
450
+ }.merge(instance.extra provider)
451
+ end
452
+
453
+ def keyintern(h)
454
+ Hash[h.map do |k, v|
455
+ [k.to_sym,
456
+ ((v.is_a? Hash and (k.to_sym != :tags)) ?
457
+ keyintern(v) : v)]
458
+ end]
459
+ end
460
+
461
+ def inventory_request(instance)
462
+ i = instance.with_defaults(@cfg[:clouds][@cloud]['defaults'],
463
+ @cfg[:providers][provider]['defaults'])
464
+ {
465
+ 'fqdn' => i.name,
466
+ 'size' => i.size,
467
+ 'image' => i.image,
468
+ 'serial_number' => i.id,
469
+ 'roles' => i.role.sort.join(','),
470
+ 'ip_address' => i.ip_address,
471
+ 'host_fqdn' => host_fqdn(i.host),
472
+ 'environment_name' => i.environment
473
+ }.merge(instance.extra 'inventory')
474
+ end
475
+
476
+ def host_fqdn(host)
477
+ if @cfg[:clouds][@cloud].has_key? 'host_domain'
478
+ host + '.' + @cfg[:clouds][@cloud]['host_domain']
479
+ else
480
+ host
481
+ end
482
+ end
483
+
484
+ def modify_instance_request(instance)
485
+ instance
486
+ end
487
+
488
+ def create_instance(instance_spec, wait_for_ip=60)
489
+ if ! instance_spec.kind_of? NCC::Instance
490
+ instance_spec = NCC::Instance.new(@cfg, instance_spec)
491
+ end
492
+ req_id = ['ncc',
493
+ @cloud,
494
+ UUIDTools::UUID.random_create.to_s].join('-')
495
+ begin
496
+ info "#{@cloud} requesting name from inventory using #{req_id}"
497
+ fqdn = @ncc.inventory.get_or_assign_system_name(req_id)['fqdn']
498
+ rescue StandardError => e
499
+ raise NCC::Error::Cloud, "Error [#{e.class}] " +
500
+ "communicating with inventory to " +
501
+ "assign name using (ncc_req_id=#{req_id}): #{e.message}"
502
+ end
503
+ instance_spec.name = fqdn
504
+ begin
505
+ req = provider_request(instance_spec)
506
+ t0 = Time.now
507
+ info "#{@cloud} sending request: #{req.inspect}"
508
+ server = do_fog { |fog| fog.servers.create(req) }
509
+ # The reason we wait for an IP is so we can add it to
510
+ # inventory, which then calculates the datacenter
511
+ if wait_for_ip > 0 and instance_ip_address(server).nil?
512
+ info "#{@cloud} waiting for ip on #{server.id}"
513
+ this = self
514
+ server.wait_for(wait_for_ip) { this.instance_ip_address(server) }
515
+ end
516
+ rescue StandardError => err
517
+ inv_update = { 'fqdn' => fqdn, 'status' => 'decommissioned' }
518
+ if ! server.nil? and server.id
519
+ inv_update['serial_number'] = server.id
520
+ end
521
+ @ncc.inventory.update('system', inv_update, fqdn)
522
+ server_id = (server.nil? ? 'nil' : server.id)
523
+ communication_error "[#{err.class}] (ncc_req_id=#{req_id} " +
524
+ "instance_id=#{server_id}): #{err.message}"
525
+ end
526
+ elapsed = Time.now - t0
527
+ info "Created instance instance_id=#{server.id} at #{provider} cloud #{@cloud} in #{elapsed}s"
528
+ instance = instance_for server
529
+ instance_spec.id = instance.id
530
+ instance_spec.ip_address = instance.ip_address
531
+ instance_spec.host = instance.host
532
+ inv_req = inventory_request(instance_spec)
533
+ inv_req['cloud'] = @cloud
534
+ inv_req['status'] = 'building'
535
+ begin
536
+ info "#{@cloud} updating inventory #{fqdn}/#{req_id} -> #{inv_req.inspect}"
537
+ @ncc.inventory.update('system', inv_req, fqdn)
538
+ rescue StandardError => e
539
+ raise NCC::Error::Cloud, "Error [#{e.class}] updating inventory " +
540
+ "system #{fqdn} " +
541
+ "(cloud=#{@cloud} ncc_req_id=#{req_id} " +
542
+ "instance_id=#{server.id}): " + e.message
543
+ end
544
+ instance
545
+ end
546
+
547
+ def instance_not_found(instance_id)
548
+ raise NCC::Error::NotFound, "Instance #{instance_id.inspect} not " +
549
+ "found in #{provider} cloud #{@cloud}"
550
+ end
551
+
552
+ def communication_error(message)
553
+ message = "Error communicating with #{provider} cloud #{@cloud}: " + message unless
554
+ /Error .*communicating with/.match(message)
555
+ raise NCC::Error::Cloud, message
556
+ end
557
+
558
+ def delete(instance_id)
559
+ server = do_fog { |fog| fog.servers.get(instance_id) }
560
+ if server.nil?
561
+ instance_not_found instance_id
562
+ else
563
+ begin
564
+ instance = instance_for server
565
+ server.destroy
566
+ inv_update = { 'fqdn' => instance.name,
567
+ 'status' => 'decommissioned' }
568
+ @ncc.inventory.update('system', inv_update, instance.name)
569
+ rescue StandardError => e
570
+ communication_error "deleting fqdn=#{instance.name} " +
571
+ "instance_id=#{server.id}: #{e.message}"
572
+ end
573
+ end
574
+ end
575
+
576
+ def console_log(instance_id)
577
+ raise NCC::Error::NotFound, "Cloud #{@cloud} provider " +
578
+ "#{provider} does not support console logs"
579
+ end
580
+
581
+ def reboot(instance_id)
582
+ server = do_fog { |fog| fog.servers.get(instance_id) }
583
+ if server.nil?
584
+ instance_not_found instance_id
585
+ else
586
+ server.reboot
587
+ end
588
+ end
589
+
590
+ end
data/lib/ncc/error.rb ADDED
@@ -0,0 +1,41 @@
1
+ #!ruby
2
+ # /* Copyright 2013 Proofpoint, Inc. All rights reserved.
3
+ # Copyright 2014 Evernote Corporation. All rights reserved.
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
+
19
+ class NCC
20
+
21
+ end
22
+
23
+ class NCC::Error < StandardError
24
+
25
+ end
26
+
27
+ class NCC::Error::NotFound < NCC::Error
28
+
29
+ end
30
+
31
+ class NCC::Error::Cloud < NCC::Error
32
+
33
+ end
34
+
35
+ class NCC::Error::Internal < NCC::Error
36
+
37
+ end
38
+
39
+ class NCC::Error::Client < NCC::Error
40
+
41
+ end