ncc-api 1.0.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.
@@ -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