Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39539
1
###
2
3
** Deprecated: THIS WAS tons of work, but really Kubernetes is the way to go... **
4
5
---
6
7
This uses the official node.js driver, which is pretty good now, and seems an order
8
of magnitude faster than using the gcloud command line!
9
10
https://googlecloudplatform.github.io/gcloud-node/#/
11
https://github.com/GoogleCloudPlatform/gcloud-node
12
13
npm install --save gcloud
14
15
or maybe
16
17
npm install --save google-cloud
18
19
these days...
20
21
TODO:
22
23
- [ ] increase the boot disk size of a vm
24
- [ ] change the machine type of a vm
25
- [ ] switch a vm between being pre-empt or not
26
- [ ] increase the size of a disk image that is attached to a VM
27
- [ ] change the name of a disk
28
29
Rules we care about are:
30
31
1. If a VM is TERMINATED, but the desired state is RUNNING and preempt is true, then:
32
if it was TERMINATED within 5 minutes create it as non-preempt and start it
33
if it was TERMINATED > 5 minutes ago create it as preempt and start it.
34
35
2. If a VM has been RUNNING for 12 hours and is not preempt, but the desired state
36
is RUNNING and preempt is true, then:
37
stop the VM and recreate and start it as prempt.
38
39
###
40
fs = require('fs')
41
42
43
winston = require('winston')
44
45
winston.remove(winston.transports.Console)
46
winston.add(winston.transports.Console, {level: 'debug', timestamp:true, colorize:true})
47
48
async = require('async')
49
50
temp = require('temp')
51
52
misc = require('smc-util/misc')
53
{defaults, required} = misc
54
55
filename = (path) -> misc.path_split(path).tail
56
57
misc_node = require('smc-util-node/misc_node')
58
59
PROJECT = process.env.SMC_PROJECT ? 'sage-math-inc'
60
DEFAULT_ZONE = 'us-central1-c'
61
62
exports.gcloud = (opts) ->
63
return new GoogleCloud(opts)
64
65
# how long ago a time was, in hours
66
age_h = (time) -> (new Date() - time)/(3600*1000)
67
age_s = (time) -> (new Date() - time)/1000
68
69
onCompleteOpts =
70
maxAttempts : 1200 # 3s * 1200 = 3600s = 1h
71
72
handle_operation = (err, operation, done, cb) ->
73
if err
74
done()
75
cb?(err)
76
else
77
operation.onComplete onCompleteOpts, (err, metadata) ->
78
done()
79
#console.log("onComplete #{misc.to_json(err)}, #{misc.to_json(metadata)}")
80
if err
81
cb?(err)
82
else if metadata.error
83
cb?(metadata.error)
84
else
85
cb?()
86
87
class VM
88
constructor: (@gcloud, @name, @zone=DEFAULT_ZONE) ->
89
@_vm = @gcloud._gce.zone(@zone).vm(@name)
90
91
dbg: (f) -> @gcloud.dbg("vm(name='#{@name}').#{f}")
92
93
show: =>
94
@get_metadata(cb:console.log)
95
96
_action: (cmd, cb) =>
97
dbg = @dbg(cmd)
98
dbg('calling api...')
99
start = misc.walltime()
100
@_vm[cmd] (err, operation, apiResponse) ->
101
handle_operation(err, operation, (->dbg("done -- took #{misc.walltime(start)}s")), cb)
102
103
stop: (opts={}) =>
104
@_action('stop', opts.cb)
105
106
start: (opts={}) =>
107
@_action('start', opts.cb)
108
109
reset: (opts={}) =>
110
@_action('reset', opts.cb)
111
112
delete: (opts={}) =>
113
opts = defaults opts,
114
keep_disks : undefined
115
cb : undefined
116
if opts.keep_disks
117
# this option doesn't seem supported by the Node.js API so we have to use the command line!
118
misc_node.execute_code
119
command : 'gcloud'
120
timeout : 3600
121
args : ['--quiet', 'compute', 'instances', 'delete', '--keep-disks', 'all', '--zone', @zone, @name]
122
cb : (err) => opts.cb?(err)
123
else
124
@_action('delete', opts.cb)
125
126
get_metadata: (opts) =>
127
opts = defaults opts,
128
cb : required
129
dbg = @dbg("metadata")
130
dbg("starting")
131
@_vm.getMetadata (err, metadata, apiResponse) =>
132
dbg("done")
133
opts.cb(err, metadata)
134
135
disks: (opts) =>
136
opts = defaults opts,
137
cb : required
138
@get_metadata
139
cb : (err, data) =>
140
if err
141
opts.cb(err)
142
else
143
disks = (@gcloud.disk(zone:@zone, name:filename(x.source)) for x in data.disks)
144
opts.cb(undefined, disks)
145
146
status: (opts) =>
147
opts = defaults opts,
148
cb : required
149
@get_metadata
150
cb : (err, x) =>
151
opts.cb(err, x?.status)
152
153
# create disk and attach to this instance
154
create_disk: (opts) =>
155
opts = defaults opts,
156
name : required
157
size_GB : undefined
158
type : 'pd-standard' # 'pd-standard' or 'pd-ssd'
159
snapshot : undefined # if given, base on snapshot
160
cb : undefined
161
dbg = @dbg("create_disk(#{misc.to_json(misc.copy_without(opts, ['cb']))})")
162
async.series([
163
(cb) =>
164
dbg("creating disk...")
165
@gcloud.create_disk
166
name : opts.name
167
size_GB : opts.size_GB
168
type : opts.type
169
snapshot : opts.snapshot
170
zone : @zone
171
cb : cb
172
(cb) =>
173
dbg("attaching to this instance")
174
@gcloud.disk(name:opts.name, zone:@zone).attach_to
175
vm : @
176
cb : cb
177
], (err) => opts.cb?(err))
178
179
attach_disk: (opts) =>
180
opts = defaults opts,
181
disk : required
182
read_only : false
183
cb : undefined
184
dbg = @dbg("attach_disk")
185
if not (opts.disk instanceof Disk)
186
dbg("not Disk")
187
if typeof(opts.disk) == 'string'
188
dbg("is string so make disk")
189
opts.disk = @gcloud.disk(name:opts.disk, zone:@zone)
190
else
191
opts.cb?("disk must be an instance of Disk")
192
return
193
dbg("starting...")
194
options =
195
readOnly : opts.read_only
196
deviceName : opts.disk.name # critical to specify -- gcloud api default is BROKEN
197
@_vm.attachDisk opts.disk._disk, options, (err, operation, apiResponse) =>
198
handle_operation(err, operation, (->dbg("done")), opts.cb)
199
200
detach_disk: (opts) =>
201
opts = defaults opts,
202
disk : required
203
cb : undefined
204
dbg = @dbg("detach_disk")
205
if not (opts.disk instanceof Disk)
206
dbg("not Disk")
207
if typeof(opts.disk) == 'string'
208
dbg("is string so make disk")
209
opts.disk = @gcloud.disk(name:opts.disk, zone:@zone)
210
else
211
opts.cb?("disk must be an instance of Disk")
212
return
213
dbg("starting...")
214
vm_data = disk_data = undefined
215
async.series([
216
(cb) =>
217
dbg("getting disk and vm metadata in parallel")
218
async.parallel([
219
(cb) =>
220
@get_metadata
221
cb : (err, x) =>
222
vm_data = x; cb(err)
223
(cb) =>
224
opts.disk.get_metadata
225
cb : (err, x) =>
226
disk_data = x; cb(err)
227
], cb)
228
(cb) =>
229
deviceName = undefined
230
for x in vm_data.disks
231
if x.source == disk_data.selfLink
232
deviceName = x.deviceName
233
break
234
dbg("determined that local deviceName is '#{deviceName}'")
235
if not deviceName
236
dbg("already done -- disk not connected to this machine")
237
cb()
238
return
239
# weird hack around what might be a bug in GCE code (NOT SURE YET)
240
# It's strange we have to make the disk from the deviceName rather than
241
# the actual disk name. It seems like the node.js api authors got confused.
242
disk = @gcloud._gce.zone(@zone).disk(deviceName)
243
dbg("doing the detachDisk operation")
244
@_vm.detachDisk disk, (err, operation, apiResponse) =>
245
handle_operation(err, operation, (->dbg("done")), cb)
246
], (err) => opts.cb?(err))
247
248
get_serial_console: (opts) =>
249
opts = defaults opts,
250
cb : required
251
@_vm.getSerialPortOutput (err, output) => opts.cb(err, output)
252
253
show_console: =>
254
@get_serial_console
255
cb : (err, output) =>
256
if err
257
console.log("ERROR -- ", err)
258
else
259
n = output.length
260
if n > 15000
261
output = output.slice(n - 15000)
262
console.log(output)
263
264
# DIFFICULT change configuration of this VM
265
# WARNING: this may be a possibly dangerous multi-step process that
266
# could involve deleting and recreating the VM.
267
###
268
1. [x] Determine if any changes need to be made.
269
2. [x] Get configuration of machine so know how to recreate it; including if machine is on.
270
3. [x] If so, ensure machine is off.
271
4. [x] Delete machine (not deleting any attached disks)
272
5. [ ] Move disks to new zone if zone changed
273
6. [x] Create machine with new params, disks, starting if it was running initially (but not otherwise).
274
###
275
change: (opts) =>
276
opts = defaults opts,
277
preemptible : undefined # whether or not VM is preemptible
278
type : undefined # the VM machine type
279
zone : undefined # which zone VM is located in
280
storage : undefined # string; set to 'read_write' to provide access to google cloud storage; '' for no access
281
boot_size_GB : undefined # size in GB of boot disk
282
boot_type : undefined # type of boot disk: 'pd-standard' or 'pd-ssd'; if not given, don't change
283
start : undefined # leave machine started after change, even if it was off
284
cb : undefined
285
dbg = @dbg("change(#{misc.to_json(misc.map_without_undefined(misc.copy_with(opts, ['cb'])))})")
286
dbg()
287
data = undefined
288
changes = {}
289
no_change = false
290
external = undefined
291
boot_disk_name = undefined
292
async.series([
293
(cb) =>
294
dbg('get vm metadata to see what needs to be changed')
295
@get_metadata
296
cb : (err, x) =>
297
data = x; cb(err)
298
(cb) =>
299
external = data.networkInterfaces?[0]?.accessConfigs?[0]?.natIP
300
if not external?
301
cb()
302
else
303
dbg('get all static external ip addresses')
304
@gcloud.get_external_static_addresses
305
cb : (err, v) =>
306
if err
307
cb(err)
308
else
309
# is external address of a reserved static interface?
310
is_reserved = false
311
for x in v
312
if x.metadata?.address == external
313
# yes
314
is_reserved = true
315
break
316
if not is_reserved
317
external = undefined
318
cb()
319
(cb) =>
320
if opts.preemptible? and data.scheduling.preemptible != opts.preemptible
321
changes.preemptible = opts.preemptible
322
if opts.type? and filename(data.machineType) != opts.type
323
changes.type = opts.type
324
if opts.zone? and filename(data.zone) != opts.zone
325
changes.zone = opts.zone
326
if opts.storage? and @_storage(data) != opts.storage
327
changes.storage = opts.storage
328
if not opts.boot_size_GB? and not opts.boot_type?
329
cb(); return
330
boot_disk = undefined
331
for x in data.disks
332
if x.boot
333
boot_disk_name = filename(x.source)
334
boot_disk = @gcloud.disk(name: boot_disk_name)
335
break
336
if not boot_disk?
337
cb(); return # is this possible
338
boot_disk.get_metadata
339
cb : (err, data) =>
340
if err
341
cb(err)
342
else
343
if opts.boot_size_GB? and parseInt(data.sizeGb) != opts.boot_size_GB
344
changes.boot_size_GB = opts.boot_size_GB
345
if opts.boot_type? and filename(data.type) != opts.boot_type
346
changes.boot_type = opts.boot_type
347
cb()
348
(cb) =>
349
dbg("determined changes=#{misc.to_json(changes)}")
350
no_change = misc.len(changes) == 0
351
if no_change
352
cb(); return
353
dbg("data.status = '#{data.status}'")
354
if data.status != 'TERMINATED'
355
dbg("Ensure machine is off.")
356
@stop(cb:cb)
357
else
358
cb()
359
(cb) =>
360
if no_change
361
cb(); return
362
dbg("delete machine (not deleting any attached disks)")
363
@delete
364
keep_disks : true
365
cb : cb
366
(cb) =>
367
if no_change
368
cb(); return
369
if not changes.zone
370
cb(); return
371
dbg("move non-boot disks to new zone")
372
f = (disk, cb) =>
373
dbg("moving disk '#{disk}'")
374
d = @gcloud.disk(name:disk, zone:@zone)
375
async.series([
376
(cb) =>
377
d.copy
378
zone : changes.zone
379
cb : cb
380
(cb) =>
381
d.delete
382
cb : cb
383
], cb)
384
async.map((filename(x.source) for x in data.disks when not x.boot), f, cb)
385
(cb) =>
386
if no_change
387
cb(); return
388
if boot_disk_name? and (changes.boot_size_GB? or changes.boot_type? or changes.zone?)
389
dbg("change the size, type, location of the boot disk")
390
@gcloud.disk(name:boot_disk_name, zone:changes.zone).change
391
size_GB : changes.boot_size_GB
392
type : changes.boot_type
393
zone : changes.zone
394
cb : cb
395
else
396
cb()
397
(cb) =>
398
if no_change
399
cb(); return
400
if changes.zone
401
# Disks are in new zone, so mutate zone in metadata.
402
# WARNING: this has *never* been tested (I'm only using one zone for SMC right now)!
403
for d in data.disks
404
@_mutate_disk_zone(d, changes.zone)
405
dbg("Create machine with new params, disks, starting if it was running initially (but not otherwise).")
406
@gcloud.create_vm
407
name : @name
408
zone : changes.zone ? @zone
409
disks : data.disks
410
type : changes.type ? filename(data.machineType)
411
tags : data.tags.items
412
preemptible : changes.preemptible ? data.scheduling.preemptible
413
storage : changes.storage ? @_storage(data)
414
external : external
415
cb : cb
416
(cb) =>
417
if no_change or data.status == 'RUNNING' or opts.start
418
cb(); return
419
dbg("Stop machine")
420
@stop(cb:cb)
421
], (err) =>
422
opts.cb?(err)
423
)
424
425
# If data sets storage access, then this returns a string, e.g., 'read_write' if the
426
# metadata indicates that google cloud storage is enabled in some way. Otherwise, this
427
# returns undefined.
428
_storage: (data) =>
429
{parse} = require('path')
430
for x in data.serviceAccounts ? []
431
for s in x.scopes
432
p = parse(s)
433
if p.name == 'devstorage'
434
return p.ext.slice(1)
435
return undefined # not currently set
436
437
_mutate_disk_zone: (meta, zone) =>
438
i = meta.source.indexOf('/zones/')
439
j = meta.source.indexOf('/', i+7)
440
meta.source = meta.source.slice(0,i+7) + zone + meta.source.slice(j)
441
return
442
443
_mutate_disk_name: (meta, name) =>
444
i = meta.source.lastIndexOf('/')
445
meta.source = meta.source.slice(0,i+1) + name
446
return
447
448
# Keep this instance running by checking on its status every interval_s seconds, and
449
# if the status is TERMINATED, issue a start command. The only way to stop this check
450
# is to exit this process.
451
keep_running: (opts={}) =>
452
opts = defaults opts,
453
interval_s : 30
454
dbg = @dbg("keep_running(interval_s=#{opts.interval_s})")
455
dbg()
456
check = () =>
457
dbg('check')
458
@status
459
cb: (err, status) =>
460
if status == 'TERMINATED'
461
dbg("attempting to start since status is TERMINATED")
462
@start
463
cb : (err) =>
464
dbg("result of start -- #{err}")
465
setInterval(check, opts.interval_s*1000)
466
467
# Make a copy of this VM, but with a different name.
468
# external static ip addresses won't be copied, nor will mounted disks whose name doesn't start with
469
# the name of the source machine.
470
# WARNING: I've not tested this in anything but the simpler case of the same zone and one single disk.
471
# However, it was written to work in general.
472
copy: (opts) =>
473
opts = defaults opts,
474
name : required # new machine name
475
preemptible : undefined # whether or not copied VM is preemptible
476
type : undefined # the new VM machine's type
477
zone : undefined # which zone to copy VM to
478
boot_size_GB : undefined # size in GB of boot disk of copy (can make larger)
479
start : true # leave machine started after copy, even if it was off
480
cb : undefined
481
dbg = @dbg("copy(name='#{opts.name}')")
482
dbg(misc.to_json(misc.copy_without(opts, ['cb'])))
483
if opts.name == @name
484
opts.cb("must specify a different name")
485
return
486
487
# These options will get passed into create_vm below:
488
create_opts =
489
name : opts.name
490
491
# This gets set to the metadata for the machine below:
492
data = undefined
493
494
async.series([
495
(cb) =>
496
dbg('get vm metadata')
497
@get_metadata
498
cb : (err, x) =>
499
data = x; cb(err)
500
(cb) =>
501
create_opts.preemptible = opts.preemptible ? data.scheduling.preemptible
502
create_opts.type = opts.type ? filename(data.machineType)
503
create_opts.zone = opts.zone ? filename(data.zone)
504
create_opts.storage = opts.storage ? @_storage(data)
505
create_opts.tags = data.tags?.items ? []
506
507
# If there is an external interface, we make the copy have one, though
508
# it will NOT be a static one
509
create_opts.external = data.networkInterfaces?[0]?.accessConfigs?[0]?.natIP?
510
511
# check name constraint on disks
512
for disk in data.disks
513
if disk.mode == 'READ_WRITE' and not misc.startswith(filename(disk.source), @name)
514
cb("all READ_WRITE disks must start with '#{@name}' but '#{filename(disk.source)}' does not")
515
return
516
517
create_opts.disks = []
518
f = (disk, cb) =>
519
if disk.mode != 'READ_WRITE'
520
# make absolutely no change at all
521
if create_opts.zone == filename(data.zone) # same zone
522
create_opts.disks.push(disk)
523
cb()
524
return
525
# read-write disk
526
src_name = filename(disk.source)
527
new_name = opts.name + src_name.slice(@name.length)
528
dbg("copying a disk: '#{src_name}' --> '#{new_name}'")
529
@gcloud.disk(zone:filename(data.zone), name:src_name).copy
530
name : new_name
531
zone : create_opts.zone
532
size_GB : if disk.boot and opts.boot_size_GB then opts.boot_size_GB
533
cb : (err) =>
534
if err
535
cb(err)
536
else
537
if create_opts.zone != filename(data.zone)
538
@_mutate_disk_zone(disk, create_opts.zone)
539
@_mutate_disk_name(disk, new_name)
540
create_opts.disks.push(disk)
541
cb()
542
543
async.map(data.disks, f, cb)
544
(cb) =>
545
dbg("Create copy machine with options #{misc.to_json(create_opts)}")
546
create_opts.cb = cb
547
@gcloud.create_vm(create_opts)
548
(cb) =>
549
if data.status == 'RUNNING' or opts.start
550
cb(); return
551
dbg("Stop machine")
552
@gcloud.vm(name: opts.name).stop(cb:cb)
553
], (err) =>
554
if err
555
opts.cb?(err)
556
else
557
opts.cb?(undefined, @gcloud.vm(name: opts.name))
558
)
559
560
class Disk
561
constructor: (@gcloud, @name, @zone=DEFAULT_ZONE) ->
562
@_disk = @gcloud._gce.zone(@zone).disk(@name)
563
564
dbg: (f) -> @gcloud.dbg("disk.#{f}")
565
566
show: =>
567
@get_metadata(cb:console.log)
568
569
copy: (opts) =>
570
opts = defaults opts,
571
name : @name
572
zone : @zone # zone of target disk
573
size_GB : undefined # if specified must be at least as large as existing disk
574
type : undefined # 'pd-standard' or 'pd-ssd'; if not given same as current
575
cb : required
576
dbg = @dbg("copy(name=#{misc.to_json(misc.copy_without(opts, ['cb']))})")
577
dbg()
578
if @name == opts.name and @zone == opts.zone
579
dbg("nothing to do")
580
opts.cb()
581
return
582
@_utility
583
name : opts.name
584
size_GB : opts.size_GB
585
type : opts.type
586
zone : opts.zone
587
delete : false
588
cb : opts.cb
589
590
# Change size or type of a disk.
591
# Disk maybe attached to an instance.
592
change: (opts) =>
593
opts = defaults opts,
594
size_GB : undefined # if specified must be at least as large as existing disk
595
type : undefined # 'pd-standard' or 'pd-ssd'; if not given same as current
596
zone : undefined
597
cb : required
598
dbg = @dbg("change()")
599
dbg(misc.to_json(misc.copy_without(opts, ['cb'])))
600
if not opts.size_GB? and not opts.type? and not opts.zone?
601
dbg("nothing to do")
602
opts.cb()
603
return
604
@_utility
605
size_GB : opts.size_GB
606
type : opts.type
607
zone : opts.zone
608
delete : true
609
cb : opts.cb
610
611
_utility: (opts) =>
612
opts = defaults opts,
613
name : @name
614
size_GB : undefined # if specified must be at least as large as existing disk
615
type : undefined # 'pd-standard' or 'pd-ssd'; if not given same as current
616
zone : @zone # zone of this disk
617
delete : false # if true: deletes original disk after making snapshot successfully; also remounts if zone same
618
cb : undefined
619
dbg = @dbg("_utility(name=#{misc.to_json(misc.copy_without(opts, ['cb']))})")
620
dbg()
621
vms = undefined # vms that disk was attached to (if any)
622
snapshot_name = undefined
623
async.series([
624
(cb) =>
625
if not opts.size_GB?
626
cb()
627
else
628
dbg("size consistency check")
629
if opts.size_GB < 10
630
cb("size_GB must be at least 10")
631
return
632
@get_size_GB
633
cb : (err, size_GB) =>
634
if err
635
cb(err)
636
else
637
if opts.size_GB < size_GB
638
cb("Requested disk size cannot be smaller than the current size")
639
else
640
cb()
641
(cb) =>
642
dbg("determine new disk type")
643
if opts.type
644
cb()
645
else
646
@get_type
647
cb : (err, type) =>
648
opts.type = type
649
cb(err)
650
(cb) =>
651
snapshot_name = "temp-#{@name}-#{misc.uuid()}"
652
dbg("create snapshot with name #{snapshot_name}")
653
@snapshot
654
name : snapshot_name
655
cb : cb
656
(cb) =>
657
if not opts.delete
658
cb(); return
659
dbg("detach disk from any vms")
660
@detach
661
cb : (err, x) =>
662
vms = x
663
cb(err)
664
(cb) =>
665
if not opts.delete
666
cb(); return
667
dbg("delete disk")
668
@delete(cb : cb)
669
(cb) =>
670
dbg("make new disk from snapshot")
671
@gcloud.snapshot(name:snapshot_name).create_disk
672
name : opts.name
673
size_GB : opts.size_GB
674
type : opts.type
675
zone : opts.zone
676
cb : cb
677
(cb) =>
678
if not vms? or vms.length == 0
679
cb(); return
680
if opts.zone? and @zone != opts.zone # moved zones
681
cb(); return
682
dbg("remount new disk on same vms")
683
f = (vm, cb) =>
684
vm.attach_disk
685
disk : opts.name
686
read_only : vms.length > 1 # if more than 1 must be read only (kind of lame)
687
cb : cb
688
async.map(vms, f, cb)
689
(cb) =>
690
if not snapshot_name?
691
cb(); return
692
dbg("clean up snapshot #{snapshot_name}")
693
@gcloud.snapshot(name:snapshot_name).delete(cb : cb)
694
], (err) =>
695
opts.cb?(err)
696
)
697
698
699
snapshot: (opts) =>
700
opts = defaults opts,
701
name : required
702
cb : undefined
703
dbg = @dbg('snapshot')
704
dbg('calling api')
705
start = misc.walltime()
706
done = -> dbg("done -- took #{misc.walltime(start)}s")
707
@_disk.createSnapshot opts.name, (err, snapshot, operation, apiResponse) =>
708
handle_operation(err, operation, done, opts.cb)
709
710
get_size_GB: (opts) =>
711
opts = defaults opts,
712
cb : required
713
@get_metadata
714
cb : (err, data) =>
715
opts.cb(err, if data? then parseInt(data.sizeGb))
716
717
get_type: (opts) =>
718
opts = defaults opts,
719
cb : required
720
@get_metadata
721
cb : (err, data) =>
722
opts.cb(err, if data? then filename(data.type))
723
724
get_metadata: (opts) =>
725
opts = defaults opts,
726
cb : required
727
dbg = @dbg("metadata")
728
dbg("starting")
729
@_disk.getMetadata (err, metadata, apiResponse) =>
730
dbg("done")
731
opts.cb(err, metadata)
732
733
# return the snapshots of this disk
734
get_snapshots: (opts) =>
735
opts = defaults opts,
736
cb : required
737
dbg = @dbg("get_snapshots")
738
id = undefined
739
s = undefined
740
async.series([
741
(cb) =>
742
dbg("determining id of disk")
743
@get_metadata
744
cb : (err, data) =>
745
id = data?.id; cb(err)
746
(cb) =>
747
dbg("get all snapshots with given id as source")
748
@gcloud.get_snapshots
749
filter : "sourceDiskId eq #{id}"
750
cb : (err, snapshots) =>
751
if err
752
cb(err)
753
else
754
s = (@gcloud.snapshot(name:x.name) for x in snapshots)
755
cb()
756
], (err) =>
757
opts.cb(err, s)
758
)
759
760
delete: (opts) =>
761
opts = defaults opts,
762
keep_disks : undefined
763
cb : undefined
764
dbg = @dbg("delete")
765
dbg("starting")
766
@_disk.delete (err, operation, apiResponse) =>
767
handle_operation(err, operation, (->dbg('done')), opts.cb)
768
769
attach_to: (opts) =>
770
opts = defaults opts,
771
vm : required
772
read_only : false
773
cb : required
774
if not (opts.vm instanceof VM)
775
if typeof(opts.vm) == 'string'
776
opts.vm = @gcloud.vm(name:opts.vm, zone:@zone)
777
else
778
opts.cb("vm must be an instance of VM")
779
return
780
opts.vm.attach_disk
781
disk : @
782
read_only : opts.read_only
783
cb : opts.cb
784
785
detach: (opts) =>
786
opts = defaults opts,
787
vm : undefined # if not given, detach from all users of this disk
788
cb : undefined # (err, list_of_vms_that_we_detached_disk_from)
789
dbg = @dbg("detach")
790
vms = undefined
791
async.series([
792
(cb) =>
793
if opts.vm?
794
vms = [opts.vm]
795
cb()
796
else
797
dbg("determine vm that disk is attached to")
798
@get_metadata
799
cb : (err, data) =>
800
if err
801
cb(err)
802
else
803
# all the users must be in the same zone as this disk
804
vms = (@gcloud.vm(name:filename(u), zone:@zone) for u in (data.users ? []))
805
cb()
806
(cb) =>
807
dbg('actually detach disk from that vm')
808
f = (vm, cb) =>
809
vm.detach_disk
810
disk : @
811
cb : cb
812
async.map(vms, f, cb)
813
814
], (err) => opts.cb?(err, vms))
815
816
class Snapshot
817
constructor: (@gcloud, @name) ->
818
@_snapshot = @gcloud._gce.snapshot(@name)
819
820
show: =>
821
@get_metadata(cb:console.log)
822
823
dbg: (f) -> @gcloud.dbg("snapshot.#{f}")
824
825
delete: (opts) =>
826
opts = defaults opts,
827
cb : undefined
828
dbg = @dbg("delete")
829
dbg("starting")
830
@_snapshot.delete (err, operation, apiResponse) =>
831
handle_operation(err, operation, (->dbg('done')), opts.cb)
832
833
get_metadata: (opts) =>
834
opts = defaults opts,
835
cb : required
836
dbg = @dbg("metadata")
837
dbg("starting")
838
@_snapshot.getMetadata (err, metadata, apiResponse) =>
839
dbg("done")
840
opts.cb(err, metadata)
841
842
get_size_GB: (opts) =>
843
opts = defaults opts,
844
cb : required
845
if @_snapshot.metadata.storageBytes
846
opts.cb(undefined, @_snapshot.metadata.storageBytes / 1000 / 1000 / 1000)
847
else
848
@get_metadata
849
cb : (err, data) =>
850
if err
851
opts.cb(err)
852
else
853
opts.cb(undefined, data.storageBytes / 1000 / 1000 / 1000)
854
855
# create disk based on this snapshot
856
create_disk: (opts) =>
857
opts = defaults opts,
858
name : required
859
size_GB : undefined
860
type : 'pd-standard' # 'pd-standard' or 'pd-ssd'
861
zone : DEFAULT_ZONE
862
cb : undefined
863
dbg = @dbg("create_disk(#{misc.to_json(misc.copy_without(opts, ['cb']))})")
864
if opts.size_GB? and opts.size_GB < 10
865
opts.cb?("size_GB must be at least 10")
866
return
867
opts.snapshot = @name
868
@gcloud.create_disk(opts)
869
870
class GoogleCloud
871
constructor: (opts={}) ->
872
opts = defaults opts,
873
debug : true
874
db : undefined
875
@db = opts.db
876
@_debug = opts.debug
877
if @_debug
878
@dbg = (f) -> ((m) -> winston.debug("gcloud.#{f}: #{m}"))
879
else
880
@dbg = (f) -> (->)
881
882
@_gcloud = require('gcloud')(projectId: PROJECT)
883
@_gce = @_gcloud.compute()
884
885
get_external_static_addresses: (opts) =>
886
opts = defaults opts,
887
cb : required
888
@_gce.getAddresses(opts.cb)
889
890
create_vm: (opts) =>
891
opts = defaults opts,
892
name : required
893
zone : DEFAULT_ZONE
894
disks : undefined # see disks[] at https://cloud.google.com/compute/docs/reference/latest/instances
895
# can also pass in Disk objects in the array; or a single string which
896
# will refer to the disk with that name in same zone.
897
http : undefined # allow http
898
https : undefined # allow https
899
type : undefined # the instance type, e.g., 'n1-standard-1'
900
os : undefined # see https://github.com/stephenplusplus/gce-images#accepted-os-names
901
tags : undefined # array of strings
902
preemptible : false
903
storage : undefined # string: e.g., 'read_write' provides read/write access to Google cloud storage; 'read_only' for read only; '' for no access
904
external : true # true for ephemeral external address; name for a specific named external address (which must already exist for now) or actual reserved ip
905
cb : required
906
dbg = @dbg("create_vm(name=#{opts.name})")
907
config = {}
908
config.http = opts.http if opts.http?
909
config.https = opts.https if opts.https?
910
config.machineType = opts.type if opts.type?
911
config.os = opts.os if opts.os?
912
config.tags = opts.tags if opts.tags?
913
config.networkInterfaces = [{network: 'global/networks/default', accessConfigs:[]}]
914
915
if opts.external
916
# WARNING: code below recursively calls create_vm in one case
917
# Also grant external network access (ephemeral by default)
918
net =
919
name: "External NAT"
920
type: "ONE_TO_ONE_NAT"
921
if typeof(opts.external) == 'string'
922
if opts.external.indexOf('.') != -1
923
# It's an ip address
924
net.natIP = opts.external
925
else
926
# name of a network interface
927
@get_external_static_addresses
928
cb : (err, v) =>
929
if err
930
opts.cb(err)
931
else
932
for x in v
933
if x.name == opts.external
934
opts.external = x.metadata.address # the ip address
935
@create_vm(opts)
936
return
937
opts.cb("unknown static external interface '#{opts.external}'")
938
return
939
config.networkInterfaces[0].accessConfigs.push(net)
940
941
if opts.storage? and opts.storage != ''
942
if typeof(opts.storage) != 'string'
943
opts.cb("opts.storage=#{opts.storage}, typeof=#{typeof(opts.storage)}, must be a string")
944
return
945
config.serviceAccounts = [{email:'default', scopes:[]}]
946
config.serviceAccounts[0].scopes.push("https://www.googleapis.com/auth/devstorage.#{opts.storage}")
947
948
if opts.preemptible
949
config.scheduling = {preemptible : true}
950
else
951
config.scheduling =
952
onHostMaintenance: "MIGRATE"
953
automaticRestart: true
954
955
if opts.disks?
956
config.disks = []
957
for disk in opts.disks
958
if typeof(disk) == 'string'
959
disk = @disk(name:disk, zone:opts.zone) # gets used immediately below!
960
961
if disk instanceof Disk
962
# use existing disk read/write
963
config.disks.push({source:disk._disk.formattedName})
964
else
965
# use object as specified at https://cloud.google.com/compute/docs/reference/latest/instances
966
config.disks.push(disk)
967
# ensure at least one disk is a boot disk
968
if config.disks.length > 0 and (x for x in config.disks when x.boot).length == 0
969
config.disks[0].boot = true
970
dbg("config=#{misc.to_json(config)}")
971
@_gce.zone(opts.zone).createVM opts.name, config, (err, vm, operation, apiResponse) =>
972
handle_operation(err, operation, (->dbg('done')), opts.cb)
973
974
vm: (opts) =>
975
opts = defaults opts,
976
name : required
977
zone : DEFAULT_ZONE
978
key = "#{opts.name}-#{opts.zone}"
979
# create cache if not already created
980
@_vm_cache ?= {}
981
# set value for key if not already set; return it
982
return (@_vm_cache[key] ?= new VM(@, opts.name, opts.zone))
983
984
disk: (opts) =>
985
opts = defaults opts,
986
name : required
987
zone : DEFAULT_ZONE
988
key = "#{opts.name}-#{opts.zone}"
989
@_disk_cache ?= {}
990
return (@_disk_cache[key] ?= new Disk(@, opts.name, opts.zone))
991
992
create_disk: (opts) =>
993
opts = defaults opts,
994
name : required
995
size_GB : undefined
996
type : 'pd-standard' # 'pd-standard' or 'pd-ssd'
997
zone : DEFAULT_ZONE
998
snapshot : undefined
999
cb : undefined
1000
dbg = @dbg("create_disk(#{misc.to_json(misc.copy_without(opts, ['cb']))})")
1001
if opts.size_GB? and opts.size_GB < 10
1002
opts.cb?("size_GB must be at least 10")
1003
return
1004
1005
dbg("starting...")
1006
config = {}
1007
if opts.snapshot?
1008
config.sourceSnapshot = "global/snapshots/#{opts.snapshot}"
1009
config.sizeGb = opts.size_GB if opts.size_GB?
1010
config.type = "zones/#{opts.zone}/diskTypes/#{opts.type}"
1011
@_gce.zone(opts.zone).createDisk opts.name, config, (err, disk, operation, apiResponse) =>
1012
handle_operation(err, operation, (->dbg('done')), (err) => opts.cb?(err))
1013
1014
snapshot: (opts) =>
1015
opts = defaults opts,
1016
name : required
1017
key = opts.name
1018
@_snapshot_cache ?= {}
1019
return (@_snapshot_cache[key] ?= new Snapshot(@, opts.name))
1020
1021
# return list of names of all snapshots
1022
get_snapshots: (opts) =>
1023
opts = defaults opts,
1024
filter : undefined
1025
match : undefined # only return results whose name contains match
1026
cb : required
1027
options = {maxResults:500} # deal with pagers next year
1028
options.filter = opts.filter if opts.filter?
1029
dbg = @dbg("get_snapshots")
1030
dbg("options=#{misc.to_json(options)}")
1031
if opts.match?
1032
opts.match = opts.match.toLowerCase()
1033
@_gce.getSnapshots options, (err, snapshots) =>
1034
dbg("done")
1035
if err
1036
opts.cb(err)
1037
else
1038
s = []
1039
for x in snapshots
1040
i = x.metadata.sourceDisk.indexOf('/zones/')
1041
if opts.match? and x.name.toLowerCase().indexOf(opts.match) == -1
1042
continue
1043
s.push
1044
name : x.name
1045
timestamp : new Date(x.metadata.creationTimestamp)
1046
size_GB : x.metadata.storageBytes / 1000 / 1000 / 1000
1047
source : x.metadata.sourceDisk.slice(i+7)
1048
opts.cb(undefined, s)
1049
1050
# return list of names of all snapshots
1051
get_disks: (opts) =>
1052
opts = defaults opts,
1053
filter : undefined
1054
match : undefined # only return results whose name contains match
1055
cb : required
1056
options = {maxResults:500} # deal with pagers next year
1057
options.filter = opts.filter if opts.filter?
1058
dbg = @dbg("get_disks")
1059
dbg("options=#{misc.to_json(options)}")
1060
if opts.match?
1061
opts.match = opts.match.toLowerCase()
1062
@_gce.getDisks options, (err, disks) =>
1063
dbg("done")
1064
if err
1065
opts.cb(err)
1066
else
1067
s = []
1068
for x in disks
1069
if opts.match? and x.name.toLowerCase().indexOf(opts.match) == -1
1070
continue
1071
size_GB = parseInt(x.metadata.sizeGb)
1072
type = filename(x.metadata.type)
1073
switch type
1074
when 'pd-standard'
1075
cost = size_GB * 0.04
1076
when 'pd-ssd'
1077
cost = size_GB * 0.17
1078
else
1079
cost = size_GB * 0.21
1080
s.push
1081
name : x.name
1082
zone : x.zone.name
1083
size_GB : size_GB
1084
type : type
1085
cost_month : cost
1086
opts.cb(undefined, s)
1087
1088
get_vms: (opts) =>
1089
opts = defaults opts,
1090
cb : required
1091
dbg = @dbg("get_vms")
1092
dbg('starting...')
1093
@_gce.getVMs (err, vms) =>
1094
dbg('done')
1095
if err
1096
opts.cb(err)
1097
else
1098
for x in vms
1099
if x.zone?
1100
delete x.zone
1101
opts.cb(undefined, vms)
1102
1103
# Get all outstanding global not-completed operations
1104
get_operations: (opts) =>
1105
opts = defaults opts,
1106
cb : required
1107
@dbg("get_operations")()
1108
@_gce.getOperations {filter:"status ne 'DONE'", maxResults:500}, (err, operations) => opts.cb(err, operations)
1109
1110
_check_db: (cb) =>
1111
if not @db
1112
cb?("database not defined")
1113
return true
1114
1115
vm_manager: (opts) =>
1116
opts = defaults opts,
1117
interval_s : 15 # queries gce api for full current state of vm's every interval_s seconds
1118
all_m : 10 # run all rules on all vm's every this many minutes
1119
manage : true
1120
if not @db?
1121
throw "database not defined!"
1122
opts.gcloud = @
1123
return new VM_Manager(opts)
1124
1125
###
1126
Storage
1127
###
1128
bucket: (opts) =>
1129
opts = defaults opts,
1130
name : required
1131
return new Bucket(@, opts.name)
1132
1133
gcloud_bucket_cache = {}
1134
1135
class Bucket
1136
constructor: (@gcloud, @name) ->
1137
@_bucket = gcloud_bucket_cache[@name]
1138
# if not defined, define it:
1139
@_bucket ?= gcloud_bucket_cache[@name] = @gcloud._gcloud.storage().bucket(@name)
1140
1141
dbg: (f) -> @gcloud.dbg("Bucket.#{f}")
1142
1143
delete: (opts) =>
1144
opts = defaults opts,
1145
name : required
1146
cb : undefined
1147
dbg = @dbg("delete(name='#{opts.name}')")
1148
dbg()
1149
@_bucket.file(opts.name).delete (err) => opts.cb?(err)
1150
1151
write: (opts) =>
1152
opts = defaults opts,
1153
name : required
1154
content : required
1155
cb : undefined
1156
dbg = @dbg("write(name='#{opts.name}')")
1157
dbg()
1158
stream = @_bucket.file(opts.name).createWriteStream()
1159
stream.write(opts.content)
1160
stream.end()
1161
stream.on 'finish', =>
1162
dbg('finish')
1163
opts.cb?()
1164
delete opts.cb
1165
stream.on 'error', (err) =>
1166
dbg("err = '#{JSON.stringify(err)}'")
1167
if err
1168
@_write_using_gsutil(opts)
1169
return
1170
1171
# The write above **should** always work. However, there is a bizarre bug in the gcloud api, so
1172
# some filenames don't work, e.g., '0283f8a0-5b6d-4b44-93ec-92df20615e99'.
1173
_write_using_gsutil: (opts) =>
1174
opts = defaults opts,
1175
name : required
1176
content : required
1177
cb : undefined
1178
dbg = @dbg("_write_using_gsutil(name='#{opts.name}')")
1179
dbg()
1180
info = undefined
1181
async.series([
1182
(cb) =>
1183
dbg("write content to a file")
1184
temp.open '', (err, _info) ->
1185
if err
1186
cb(err)
1187
else
1188
info = _info
1189
dbg("temp file = '#{info.path}'")
1190
fs.writeFile(info.fd, opts.content, cb)
1191
(cb) =>
1192
dbg("close")
1193
fs.close(info.fd, cb)
1194
(cb) =>
1195
dbg("call gsutil via shell")
1196
misc_node.execute_code
1197
command : 'gsutil'
1198
args : ['cp', info.path, "gs://#{@name}/#{opts.name}"]
1199
timeout : 30
1200
cb : cb
1201
], (err) =>
1202
if info?
1203
try
1204
fs.unlink(info.path)
1205
catch e
1206
dbg("error unlinking")
1207
opts.cb?(err)
1208
)
1209
1210
1211
read: (opts) =>
1212
opts = defaults opts,
1213
name : required
1214
cb : required
1215
dbg = @dbg("read(name='#{opts.name}')")
1216
dbg()
1217
stream = @_bucket.file(opts.name).download (err, content) =>
1218
if err
1219
dbg("error = '#{err}")
1220
else
1221
dbg('done')
1222
opts.cb(err, content)
1223
return
1224
1225
class VM_Manager
1226
constructor: (opts) ->
1227
opts = defaults opts,
1228
gcloud : required
1229
interval_s : required
1230
all_m : required
1231
manage : required
1232
@_manage = opts.manage
1233
@_action_timeout_m = 15 # assume actions that took this long failed
1234
@_switch_back_to_preemptible_m = 120 # minutes until we try to switch something that should be pre-empt back
1235
@gcloud = opts.gcloud
1236
dbg = @_dbg("start(interval_s:#{opts.interval_s}, all_m:#{opts.all_m})")
1237
@_init_instances_table()
1238
if @_manage
1239
dbg('starting vm manager monitoring')
1240
@_init_timers(opts)
1241
return
1242
1243
close: () =>
1244
@_dbg('close')()
1245
if @_update_interval?
1246
clearInterval(@_update_interval)
1247
delete @_update_interval
1248
if @_update_all?
1249
clearInterval(@_update_all)
1250
delete @_update_all
1251
if @_instances_table?
1252
@_instances_table.close()
1253
delete @_instances_table
1254
1255
###
1256
require 'c'; vms()
1257
vms.request(name:'compute8-us', status:'RUNNING', preemtible:true, cb:done())
1258
vms.request(name:'compute8-us',status:'TERMINATED',cb:done())
1259
###
1260
request: (opts) =>
1261
opts = defaults opts,
1262
name : required
1263
status : undefined # 'RUNNING', 'TERMINATED'
1264
preemptible : undefined # true or false
1265
cb : undefined
1266
obj = {}
1267
if opts.status?
1268
if opts.status not in ['TERMINATED', 'RUNNING']
1269
err = "status must be 'TERMINATED' or 'RUNNING'"
1270
winston.debug(err)
1271
opts.cb?(err)
1272
return
1273
obj.requested_status = opts.status
1274
if opts.preemptible?
1275
obj.requested_preemptible = !! opts.preemptible
1276
if misc.len(obj) == 0
1277
opts.cb()
1278
else
1279
@gcloud.db.table('instances').get(opts.name).update(obj).run(opts.cb)
1280
1281
get_data: (name) =>
1282
obj = @_instances_table?.get(name)?.toJS()
1283
if obj?
1284
return @_data(obj)
1285
1286
# WARNING: stupid non-indexed query below; make fast when log gets big...
1287
get_log: (opts) =>
1288
opts = defaults opts,
1289
name : undefined
1290
age_m : undefined
1291
cb : required
1292
db = @gcloud.db
1293
query = db.table('instance_actions_log')
1294
if opts.name?
1295
query = query.filter(name:opts.name)
1296
if opts.age_m?
1297
query = query.filter(db.r.row('action')('finished').ge(misc.minutes_ago(opts.age_m)))
1298
query.run(opts.cb)
1299
1300
show_log: (opts) =>
1301
opts = defaults opts,
1302
name : undefined
1303
age_m : undefined
1304
cb : undefined
1305
@get_log
1306
name : opts.name
1307
age_m : opts.age_m
1308
cb : (err, log) =>
1309
if err
1310
console.log("ERROR: ", err)
1311
else
1312
log.sort (x,y) => misc.cmp(x.action?.started ? new Date(), y.action?.started ? new Date())
1313
pad = (s) -> misc.pad_left(s ? '', 10)
1314
for x in log
1315
console.log "#{pad(x.name)} #{pad(x.action?.type)} #{pad(x.action?.action)} #{x.action?.started?.toLocaleString()} #{x.action?.finished?.toLocaleString()} #{pad(misc.round1((x.action?.finished - x.action?.started)/1000/60))} minutes '#{misc.to_json(x.action?.error ? '')}'"
1316
opts.cb?(err)
1317
1318
# periodically display the log for the last 24 hours
1319
monitor: () =>
1320
f = () =>
1321
console.log("\n\n-----------------------------\n\n\n")
1322
@show_log(age_m : 60*24)
1323
f()
1324
setInterval(f, 60*1000)
1325
1326
_init_timers: (opts) =>
1327
@_dbg("_init_timers")()
1328
@_update_interval = setInterval(@_update_db, opts.interval_s * 1000)
1329
@_udpate_all = setInterval(@_update_all, opts.all_m * 1000 * 60)
1330
async.series([((cb)=>@_update_db(cb:cb)), @_update_all])
1331
return
1332
1333
_dbg: (f) ->
1334
return (m) -> winston.debug("VM_Manager.#{f}: #{m}")
1335
1336
_update_all: () =>
1337
dbg = @_dbg("update_all")
1338
dbg()
1339
@_instances_table?.get().map (vm, key) =>
1340
if vm.get('requested_status')
1341
@_apply_rules(vm.toJS())
1342
return
1343
1344
_update_db: (opts) =>
1345
opts = defaults opts,
1346
cb : undefined
1347
dbg = @_dbg("update_db")
1348
dbg()
1349
db_data = {}
1350
gce_data = {}
1351
table = @gcloud.db.table('instances')
1352
async.series([
1353
(cb) =>
1354
async.parallel([
1355
(cb) =>
1356
dbg("get info from Google Compute engine api about all VMs")
1357
@gcloud.get_vms
1358
cb : (err, data) =>
1359
if err
1360
cb(err)
1361
else
1362
for x in data
1363
gce_data[x.name] = x
1364
dbg("got gce api data about #{data.length} VMs")
1365
cb()
1366
(cb) =>
1367
dbg("get info from our database about all VMs")
1368
table.pluck('name', 'gce_sha1').run (err, data) =>
1369
if err
1370
cb(err)
1371
else
1372
for x in data
1373
db_data[x.name] = x.gce_sha1
1374
dbg("got database data about #{misc.len(db_data)} VMs")
1375
cb()
1376
], cb)
1377
(cb) =>
1378
objects = []
1379
for name, x of gce_data
1380
new_sha1 = misc_node.sha1(JSON.stringify(x))
1381
sha1 = db_data[name]
1382
if new_sha1 != sha1
1383
objects.push(name:name, gce:x, gce_sha1:new_sha1)
1384
if objects.length == 0
1385
dbg("nothing changed")
1386
cb()
1387
else
1388
dbg("#{objects.length} vms changed")
1389
global.objects = objects
1390
table.insert(objects, conflict:'update').run(cb)
1391
], (err) =>
1392
opts.cb?(err)
1393
)
1394
1395
_init_instances_table: () =>
1396
dbg = @_dbg("_init_instances_table")
1397
dbg()
1398
@gcloud.db.synctable
1399
query : @gcloud.db.table('instances')
1400
cb : (err, t) =>
1401
if err
1402
# this shouldn't happen...
1403
dbg("ERROR: #{err}")
1404
else
1405
dbg("initialized instances synctable")
1406
@_instances_table = t
1407
if @_manage
1408
t.on 'change', (name) =>
1409
@_apply_rules(t.get(name).toJS())
1410
1411
_is_in_progress: (vm) =>
1412
if not vm.action?
1413
return false
1414
if vm.action.finished?
1415
return false
1416
if vm.action.started? and not vm.action.finished? and vm.action.started <= misc.minutes_ago(@_action_timeout_m)
1417
return false
1418
# at this point finished is not set and started was set recently
1419
return true
1420
1421
_data: (vm) =>
1422
data =
1423
name : vm.name
1424
gce_status : vm.gce.metadata.status
1425
gce_preemptible : vm.gce.metadata.scheduling.preemptible
1426
gce_created : new Date(vm.gce.metadata.creationTimestamp)
1427
requested_status : vm.requested_status
1428
requested_preemptible : vm.requested_preemptible
1429
last_action : vm.action
1430
return data
1431
1432
_apply_rules: (vm) =>
1433
if not vm.requested_status # only manage vm's with desired status set
1434
return
1435
if @_is_in_progress(vm)
1436
return
1437
if not vm.gce?.metadata?.scheduling?
1438
# nothing to be done
1439
return
1440
dbg = @_dbg("_apply_rules")
1441
1442
data = @_data(vm)
1443
dbg(misc.to_json(data))
1444
1445
# Enable this in case something goes wrong with changing properties of VM's; this
1446
# will make it so we only attempt restart, rather than anything more subtle.
1447
#if @_rule0(data)
1448
# return
1449
#return
1450
1451
if @_rule1(data)
1452
return
1453
if @_rule2(data)
1454
return
1455
if @_rule3(data)
1456
return
1457
if @_rule4(data)
1458
return
1459
1460
_rule0: (data) =>
1461
if data.gce_status == 'TERMINATED' and data.requested_status == 'RUNNING'
1462
# Just start the VM
1463
@_action(data, 'start', 'rule1')
1464
return true
1465
1466
1467
_rule1: (data) =>
1468
if data.gce_status == 'TERMINATED' and data.requested_status == 'RUNNING'
1469
dbg = @_dbg("rule1('#{data.name}')")
1470
dbg("terminated VM should be running")
1471
if data.gce_preemptible and data.last_action?.started >= misc.minutes_ago(5) and data.last_action?.action == 'start'
1472
dbg("Pre-emptible right now and there was an attempt to start it recently, so switch to non-pre-empt.")
1473
@_action(data, 'non-preemptible', 'rule1')
1474
else
1475
# Just start the VM
1476
@_action(data, 'start', 'rule1')
1477
return true
1478
1479
_rule2: (data) =>
1480
if data.gce_status == 'RUNNING' and data.requested_status == 'TERMINATED'
1481
dbg = @_dbg("rule2('#{data.name}')")
1482
dbg("running VM should be stopped")
1483
@_action(data, 'stop', 'rule2')
1484
return true
1485
1486
_rule3: (data) =>
1487
if data.gce_status == 'RUNNING' and data.requested_status == 'RUNNING' and \
1488
data.requested_preemptible and not data.gce_preemptible and data.gce_created <= misc.minutes_ago(@_switch_back_to_preemptible_m)
1489
dbg = @_dbg("rule3('#{data.name}')")
1490
dbg("switch running instance from non-preempt to preempt")
1491
@_action(data, 'preemptible', 'rule3')
1492
return true
1493
1494
_rule4: (data) =>
1495
if data.gce_status == 'RUNNING' and data.requested_status == 'RUNNING' and \
1496
not data.requested_preemptible and data.gce_preemptible
1497
dbg = @_dbg("rule4('#{data.name}')")
1498
dbg("switch running instance from preempt to non-preempt")
1499
@_action(data, 'non-preemptible', 'rule4')
1500
return true
1501
1502
_action: (data, action, type, cb) =>
1503
db = @gcloud.db
1504
query = db.table('instances').get(data.name)
1505
dbg = @_dbg("_action(action='#{action}',host='#{data.name}')")
1506
dbg(misc.to_json(data))
1507
action_obj =
1508
action : action
1509
started : new Date()
1510
type : type
1511
log =
1512
id : misc.uuid()
1513
name : data.name
1514
action : action_obj
1515
async.series([
1516
(cb) =>
1517
dbg('set fact that action started in the database')
1518
query.update(action:db.r.literal(action_obj)).run(cb)
1519
(cb) =>
1520
dbg("set log entry to #{misc.to_json(log)}")
1521
db.table('instance_actions_log').insert(log).run(cb)
1522
(cb) =>
1523
vm = @gcloud.vm(name:data.name)
1524
start = data.requested_status == 'RUNNING'
1525
switch action
1526
when 'start', 'stop'
1527
vm[action](cb:cb)
1528
when 'preemptible'
1529
vm.change(preemptible:true, start:start, cb:cb)
1530
when 'non-preemptible'
1531
vm.change(preemptible:false, start:start, cb:cb)
1532
else
1533
cb("invalid action '#{action}'")
1534
(cb) =>
1535
dbg("update db view of GCE machine state, so we don't try to do action again right after finishing")
1536
@_update_db(cb:cb)
1537
], (err) =>
1538
change = {finished:new Date()}
1539
if err
1540
change.error = err
1541
db.table('instances').get(data.name).update(action:change).run(cb)
1542
# update entry in the log
1543
db.table('instance_actions_log').get(log.id).update(action : misc.merge(action_obj, change)).run (err) =>
1544
if err
1545
dbg("ERROR inserting log -- #{err}")
1546
)
1547
1548
###
1549
One off code
1550
###
1551
exports.copy_projects_disks = (v) ->
1552
g = exports.gcloud()
1553
f = (n, cb) ->
1554
async.parallel([
1555
(cb) ->
1556
d = g.disk(name:"projects#{n}-base")
1557
d.copy(name:"storage#{n}",cb:cb)
1558
(cb) ->
1559
d = g.disk(name:"projects#{n}")
1560
d.copy(name:"storage#{n}-projects",cb:cb)
1561
(cb) ->
1562
d = g.disk(name:"projects#{n}-bup")
1563
d.copy(name:"storage#{n}-bups", size_GB:200, cb:cb)
1564
], (err) ->
1565
if not err
1566
g.create_vm(name:"storage#{n}", disks:["storage#{n}", "storage#{n}-projects", "storage#{n}-bups"], tags:['storage','http'], preemptible:false, storage:'read_write', cb:cb)
1567
else
1568
cb(err)
1569
)
1570
1571
async.map v, f, (err)->
1572
console.log("TOTOTALY DONE! -- #{err}")
1573
1574
1575