Statistics
| Branch: | Tag: | Revision:

root / ui / static / snf / js / models.js @ 2506f741

History | View | Annotate | Download (49.1 kB)

1
;(function(root){
2
    
3
    // root
4
    var root = root;
5
    
6
    // setup namepsaces
7
    var snf = root.synnefo = root.synnefo || {};
8
    var models = snf.models = snf.models || {}
9
    var storage = snf.storage = snf.storage || {};
10
    var util = snf.util = snf.util || {};
11

    
12
    // shortcuts
13
    var bb = root.Backbone;
14
    var slice = Array.prototype.slice
15

    
16
    // logging
17
    var logger = new snf.logging.logger("SNF-MODELS");
18
    var debug = _.bind(logger.debug, logger);
19
    
20
    // get url helper
21
    var getUrl = function() {
22
        return snf.config.api_url + "/" + this.path;
23
    }
24
    
25
    // i18n
26
    BUILDING_MESSAGES = window.BUILDING_MESSAGES || {'INIT': 'init', 'COPY': '{0}, {1}, {2}', 'FINAL': 'final'};
27

    
28
    // Base object for all our models
29
    models.Model = bb.Model.extend({
30
        sync: snf.api.sync,
31
        api: snf.api,
32
        has_status: false,
33

    
34
        initialize: function() {
35
            if (this.has_status) {
36
                this.bind("change:status", this.handle_remove);
37
                this.handle_remove();
38
            }
39
            models.Model.__super__.initialize.apply(this, arguments)
40
        },
41

    
42
        handle_remove: function() {
43
            if (this.get("status") == 'DELETED') {
44
                if (this.collection) {
45
                    try { this.clear_pending_action();} catch (err) {};
46
                    try { this.reset_pending_actions();} catch (err) {};
47
                    this.collection.remove(this.id);
48
                }
49
            }
50
        },
51
        
52
        // custom set method to allow submodels to use
53
        // set_<attr> methods for handling the value of each
54
        // attribute and overriding the default set method
55
        // for specific parameters
56
        set: function(params, options) {
57
            _.each(params, _.bind(function(value, key){
58
                if (this["set_" + key]) {
59
                    params[key] = this["set_" + key](value);
60
                }
61
            }, this))
62
            var ret = bb.Model.prototype.set.call(this, params, options);
63
            return ret;
64
        },
65

    
66
        url: function(options) {
67
            return getUrl.call(this) + "/" + this.id;
68
        },
69

    
70
        api_path: function(options) {
71
            return this.path + "/" + this.id;
72
        },
73

    
74
        parse: function(resp, xhr) {
75
            return resp.server;
76
        },
77

    
78
        remove: function() {
79
            this.api.call(this.api_path(), "delete");
80
        },
81

    
82
        changedKeys: function() {
83
            return _.keys(this.changedAttributes() || {});
84
        },
85

    
86
        hasOnlyChange: function(keys) {
87
            var ret = false;
88
            _.each(keys, _.bind(function(key) {
89
                if (this.changedKeys().length == 1 && this.changedKeys().indexOf(key) > -1) { ret = true};
90
            }, this));
91
            return ret;
92
        }
93

    
94
    })
95
    
96
    // Base object for all our model collections
97
    models.Collection = bb.Collection.extend({
98
        sync: snf.api.sync,
99
        api: snf.api,
100

    
101
        url: function(options) {
102
            return getUrl.call(this) + (options.details || this.details ? '/detail' : '');
103
        },
104

    
105
        fetch: function(options) {
106
            // default to update
107
            if (!this.noUpdate) {
108
                if (!options) { options = {} };
109
                if (options.update === undefined) { options.update = true };
110
                if (!options.removeMissing && options.refresh) { options.removeMissing = true };
111
            }
112
            // custom event foreach fetch
113
            return bb.Collection.prototype.fetch.call(this, options)
114
        },
115

    
116
        get_fetcher: function(timeout, fast, limit, initial, params) {
117
            var fetch_params = params;
118
            var timeout = parseInt(timeout);
119
            var fast = fast || 1000;
120
            var limit = limit;
121
            var initial_call = initial || true;
122
            
123
            var last_ajax = undefined;
124
            var cb = _.bind(function(){
125
                updater._ajax = last_ajax;
126
                if (last_ajax) {
127
                    last_ajax.abort();
128
                }
129
                last_ajax = this.fetch(fetch_params);
130
            }, this);
131
            var updater = new snf.api.updateHandler({'callback': cb, timeout:timeout, 
132
                                                    fast:fast, limit:limit, 
133
                                                    call_on_start:initial_call});
134

    
135
            snf.api.bind("call", _.bind(function(){ updater.faster()}, this));
136
            return updater;
137
        }
138
    });
139
    
140
    // Image model
141
    models.Image = models.Model.extend({
142
        path: 'images',
143

    
144
        get_size: function() {
145
            return parseInt(this.get('metadata') ? this.get('metadata').values.size : -1)
146
        },
147

    
148
        get_os: function() {
149
            return this.get("OS");
150
        }
151
    });
152

    
153
    // Flavor model
154
    models.Flavor = models.Model.extend({
155
        path: 'flavors',
156

    
157
        details_string: function() {
158
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
159
        },
160

    
161
        get_disk_size: function() {
162
            return parseInt(this.get("disk") * 1000)
163
        },
164

    
165
    });
166
    
167
    //network vms list helper
168
    var NetworkVMSList = function() {
169
        this.initialize = function() {
170
            this.vms = [];
171
            this.pending = [];
172
            this.pending_for_removal = [];
173
        }
174
        
175
        this.add_pending_for_remove = function(vm_id) {
176
            if (this.pending_for_removal.indexOf(vm_id) == -1) {
177
                this.pending_for_removal.push(vm_id);
178
            }
179

    
180
            if (this.pending_for_removal.length) {
181
                this.trigger("pending:remove:add");
182
            }
183
        },
184

    
185
        this.add_pending = function(vm_id) {
186
            if (this.pending.indexOf(vm_id) == -1) {
187
                this.pending[this.pending.length] = vm_id;
188
            }
189

    
190
            if (this.pending.length) {
191
                this.trigger("pending:add");
192
            }
193
        }
194

    
195
        this.check_pending = function() {
196
            var len = this.pending.length;
197
            var args = [this.pending];
198
            this.pending = _.difference(this.pending, this.vms);
199
            if (len != this.pending.length) {
200
                if (this.pending.length == 0) {
201
                    this.trigger("pending:clear");
202
                }
203
            }
204

    
205
            var len = this.pending_for_removal.length;
206
            this.pending_for_removal = _.intersection(this.pending_for_removal, this.vms);
207
            if (this.pending_for_removal.length == 0) {
208
                this.trigger("pending:remove:clear");
209
            }
210

    
211
        }
212

    
213

    
214
        this.add = function(vm_id) {
215
            if (this.vms.indexOf(vm_id) == -1) {
216
                this.vms[this.vms.length] = vm_id;
217
                this.trigger("network:connect", vm_id);
218
                this.check_pending();
219
                return true;
220
            }
221
        }
222

    
223
        this.remove = function(vm_id) {
224
            if (this.vms.indexOf(vm_id) > -1) {
225
                this.vms = _.without(this.vms, vm_id);
226
                this.trigger("network:disconnect", vm_id);
227
                this.check_pending();
228
                return true;
229
            }
230
        }
231

    
232
        this.get = function() {
233
            return this.vms;
234
        }
235

    
236
        this.list = function() {
237
            return storage.vms.filter(_.bind(function(vm){
238
                return this.vms.indexOf(vm.id) > -1;
239
            }, this))
240
        }
241

    
242
        this.initialize();
243
    };
244
    _.extend(NetworkVMSList.prototype, bb.Events);
245
    
246
    // vm networks list helper
247
    var VMNetworksList = function() {
248
        this.initialize = function() {
249
            this.networks = {};
250
            this.network_ids = [];
251
        }
252

    
253
        this.add = function(net_id, data) {
254
            if (!this.networks[net_id]) {
255
                this.networks[net_id] = data || {};
256
                this.network_ids[this.network_ids.length] = net_id;
257
                this.trigger("network:connect", net_id);
258
                return true;
259
            }
260
        }
261

    
262
        this.remove = function(net_id) {
263
            if (this.networks[net_id]) {
264
                delete this.networks[net_id];
265
                this.network_ids = _.without(this.network_ids, net_id);
266
                this.trigger("network:disconnect", net_id);
267
                return true;
268
            }
269
            return false;
270
        }
271

    
272
        this.get = function() {
273
            return this.networks;
274
        }
275

    
276
        this.list = function() {
277
            return storage.networks.filter(_.bind(function(net){
278
                return this.network_ids.indexOf(net.id) > -1;
279
            }, this))
280
        }
281

    
282
        this.initialize();
283
    };
284
    _.extend(VMNetworksList.prototype, bb.Events);
285

    
286
    // Image model
287
    models.Network = models.Model.extend({
288
        path: 'networks',
289
        has_status: true,
290
        
291
        initialize: function() {
292
            this.vms = new NetworkVMSList();
293
            this.vms.bind("pending:add", _.bind(this.handle_pending_connections, this, "add"));
294
            this.vms.bind("pending:clear", _.bind(this.handle_pending_connections, this, "clear"));
295
            this.vms.bind("pending:remove:add", _.bind(this.handle_pending_connections, this, "add"));
296
            this.vms.bind("pending:remove:clear", _.bind(this.handle_pending_connections, this, "clear"));
297

    
298
            ret = models.Network.__super__.initialize.apply(this, arguments);
299

    
300
            storage.vms.bind("change:linked_to_nets", _.bind(this.update_connections, this, "vm:change"));
301
            storage.vms.bind("add", _.bind(this.update_connections, this, "add"));
302
            storage.vms.bind("remove", _.bind(this.update_connections, this, "remove"));
303
            storage.vms.bind("reset", _.bind(this.update_connections, this, "reset"));
304
            this.bind("change:linked_to", _.bind(this.update_connections, this, "net:change"));
305
            this.update_connections();
306
            this.update_state();
307
            return ret;
308
        },
309

    
310
        update_state: function() {
311
            if (this.vms.pending.length) {
312
                this.set({state: "CONNECTING"});
313
                return
314
            }
315
            if (this.vms.pending_for_removal.length) {
316
                this.set({state: "DISCONNECTING"});
317
                return
318
            }   
319
            
320
            var firewalling = false;
321
            _.each(this.vms.get(), _.bind(function(vm_id){
322
                var vm = storage.vms.get(vm_id);
323
                if (!vm) { return };
324
                if (!_.isEmpty(vm.pending_firewalls)) {
325
                    this.set({state:"FIREWALLING"});
326
                    firewalling = true;
327
                    return false;
328
                }
329
            },this));
330
            if (firewalling) { return };
331

    
332
            this.set({state:"NORMAL"});
333
        },
334

    
335
        handle_pending_connections: function(action) {
336
            this.update_state();
337
        },
338

    
339
        // handle vm/network connections
340
        update_connections: function(action, model) {
341
            
342
            // vm removed disconnect vm from network
343
            if (action == "remove") {
344
                var removed_from_net = this.vms.remove(model.id);
345
                var removed_from_vm = model.networks.remove(this.id);
346
                if (removed_from_net) {this.trigger("vm:disconnect", model, this); this.change()};
347
                if (removed_from_vm) {model.trigger("network:disconnect", this, model); this.change()};
348
                return;
349
            }
350
            
351
            // update links for all vms
352
            var links = this.get("linked_to");
353
            storage.vms.each(_.bind(function(vm) {
354
                var vm_links = vm.get("linked_to") || [];
355
                if (vm_links.indexOf(this.id) > -1) {
356
                    // vm has connection to current network
357
                    if (links.indexOf(vm.id) > -1) {
358
                        // and network has connection to vm, so try
359
                        // to append it
360
                        var add_to_net = this.vms.add(vm.id);
361
                        var index = _.indexOf(vm_links, this.id);
362
                        var add_to_vm = vm.networks.add(this.id, vm.get("linked_to_nets")[index]);
363
                        
364
                        // call only if connection did not existed
365
                        if (add_to_net) {this.trigger("vm:connect", vm, this); this.change()};
366
                        if (add_to_vm) {vm.trigger("network:connect", this, vm); vm.change()};
367
                    } else {
368
                        // no connection, try to remove it
369
                        var removed_from_net = this.vms.remove(vm.id);
370
                        var removed_from_vm = vm.networks.remove(this.id);
371
                        if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
372
                        if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
373
                    }
374
                } else {
375
                    // vm has no connection to current network, try to remove it
376
                    var removed_from_net = this.vms.remove(vm.id);
377
                    var removed_from_vm = vm.networks.remove(this.id);
378
                    if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
379
                    if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
380
                }
381
            },this));
382
        },
383

    
384
        is_public: function() {
385
            return this.id == "public";
386
        },
387

    
388
        contains_vm: function(vm) {
389
            var net_vm_exists = this.vms.get().indexOf(vm.id) > -1;
390
            var vm_net_exists = vm.is_connected_to(this);
391
            return net_vm_exists && vm_net_exists;
392
        },
393

    
394
        add_vm: function (vm, callback) {
395
            return this.api.call(this.api_path() + "/action", "create", 
396
                                 {add:{serverRef:"" + vm.id}},
397
                                 _.bind(function(){
398
                                     this.vms.add_pending(vm.id);
399
                                     if (callback) {callback()}
400
                                 },this));
401
        },
402

    
403
        remove_vm: function (vm, callback) {
404
            return this.api.call(this.api_path() + "/action", "create", 
405
                                 {remove:{serverRef:"" + vm.id}},
406
                                 _.bind(function(){
407
                                     this.vms.add_pending_for_remove(vm.id);
408
                                     if (callback) {callback()}
409
                                 },this));
410
        },
411

    
412
        rename: function(name, callback) {
413
            return this.api.call(this.api_path(), "update", {network:{name:name}}, callback);
414
        },
415

    
416
        get_connectable_vms: function() {
417
            var servers = this.vms.list();
418
            return storage.vms.filter(function(vm){return servers.indexOf(vm.id) == -1})
419
        },
420

    
421
        state_message: function() {
422
            if (this.get("state") == "NORMAL" && this.is_public()) {
423
                return "Public network";
424
            }
425

    
426
            return models.Network.STATES[this.get("state")];
427
        },
428

    
429
        in_progress: function() {
430
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
431
        }
432
    });
433
    
434
    models.Network.STATES = {
435
        'NORMAL': 'Private network',
436
        'CONNECTING': 'Connecting...',
437
        'DISCONNECTING': 'Disconnecting...',
438
        'FIREWALLING': 'Firewall update...'
439
    }
440

    
441
    models.Network.STATES_TRANSITIONS = {
442
        'CONNECTING': ['NORMAL'],
443
        'DISCONNECTING': ['NORMAL'],
444
        'FIREWALLING': ['NORMAL']
445
    }
446

    
447
    // Virtualmachine model
448
    models.VM = models.Model.extend({
449

    
450
        path: 'servers',
451
        has_status: true,
452
        initialize: function(params) {
453
            this.networks = new VMNetworksList();
454
            
455
            this.pending_firewalls = {};
456
            
457
            models.VM.__super__.initialize.apply(this, arguments);
458

    
459
            this.set({state: params.status || "ERROR"});
460
            this.log = new snf.logging.logger("VM " + this.id);
461
            this.pending_action = undefined;
462
            
463
            // init stats parameter
464
            this.set({'stats': undefined}, {silent: true});
465
            // defaults to not update the stats
466
            // each view should handle this vm attribute 
467
            // depending on if it displays stat images or not
468
            this.do_update_stats = false;
469
            
470
            // interval time
471
            // this will dynamicaly change if the server responds that
472
            // images get refreshed on different intervals
473
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
474
            this.stats_available = false;
475

    
476
            // initialize interval
477
            this.init_stats_intervals(this.stats_update_interval);
478
            
479
            this.bind("change:progress", _.bind(this.update_building_progress, this));
480
            this.update_building_progress();
481

    
482
            this.bind("change:firewalls", _.bind(this.handle_firewall_change, this));
483
            
484
            // default values
485
            this.set({linked_to_nets:this.get("linked_to_nets") || []});
486
            this.set({firewalls:this.get("firewalls") || []});
487

    
488
            this.bind("change:state", _.bind(function(){if (this.state() == "DESTROY") { this.handle_destroy() }}, this))
489
        },
490

    
491
        handle_firewall_change: function() {
492

    
493
        },
494
        
495
        set_linked_to_nets: function(data) {
496
            this.set({"linked_to":_.map(data, function(n){ return n.id})});
497
            return data;
498
        },
499

    
500
        is_connected_to: function(net) {
501
            return _.filter(this.networks.list(), function(n){return n.id == net.id}).length > 0;
502
        },
503
        
504
        status: function(st) {
505
            if (!st) { return this.get("status")}
506
            return this.set({status:st});
507
        },
508

    
509
        set_status: function(st) {
510
            var new_state = this.state_for_api_status(st);
511
            var transition = false;
512

    
513
            if (this.state() != new_state) {
514
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
515
                    transition = this.state();
516
                }
517
            }
518
            
519
            // call it silently to avoid double change trigger
520
            this.set({'state': this.state_for_api_status(st)}, {silent: true});
521
            
522
            // trigger transition
523
            if (transition) { this.trigger("transition", {from:transition, to:new_state}) };
524
            return st;
525
        },
526

    
527
        update_building_progress: function() {
528
            if (this.is_building()) {
529
                var progress = this.get("progress");
530
                if (progress == 0) {
531
                    this.state("BUILD_INIT");
532
                    this.set({progress_message: BUILDING_MESSAGES['INIT']});
533
                }
534
                if (progress > 0 && progress < 99) {
535
                    this.state("BUILD_COPY");
536
                    var params = this.get_copy_details(true);
537
                    this.set({progress_message: BUILDING_MESSAGES['COPY'].format(params.copy, 
538
                                                                                 params.size, 
539
                                                                                 params.progress)});
540
                }
541
                if (progress == 100) {
542
                    this.state("BUILD_FINAL");
543
                    this.set({progress_message: BUILDING_MESSAGES['FINAL']});
544
                }
545
            } else {
546
            }
547
        },
548

    
549
        get_copy_details: function(human, image) {
550
            var human = human || false;
551
            var image = image || this.get_image();
552

    
553
            var progress = this.get('progress');
554
            var size = image.get_size();
555
            var size_copied = (size * progress / 100).toFixed(2);
556
            
557
            if (human) {
558
                size = util.readablizeBytes(size*1024*1024);
559
                size_copied = util.readablizeBytes(size_copied*1024*1024);
560
            }
561
            return {'progress': progress, 'size': size, 'copy': size_copied};
562
        },
563

    
564
        // clear and reinitialize update interval
565
        init_stats_intervals: function (interval) {
566
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
567
            this.stats_fetcher.start();
568
            this.update_stats(true);
569
        },
570
        
571
        get_stats_fetcher: function(timeout) {
572
            var cb = _.bind(function(data){
573
                this.update_stats();
574
            }, this);
575
            var fetcher = new snf.api.updateHandler({'callback': cb, timeout:timeout});
576
            return fetcher;
577
        },
578

    
579
        // do the api call
580
        update_stats: function(force) {
581
            // do not update stats if flag not set
582
            if (!this.do_update_stats && !force) {
583
                return;
584
            }
585

    
586
            // make the api call, execute handle_stats_update on sucess
587
            // TODO: onError handler ???
588
            stats_url = this.url() + "/stats";
589
            this.sync("GET", this, {
590
                handles_error:true, 
591
                url: stats_url, 
592
                refresh:true, 
593
                success: _.bind(this.handle_stats_update, this),
594
                error: _.bind(this.handle_stats_error, this)
595
            });
596
        },
597

    
598
        get_stats_image: function(stat, type) {
599
        },
600
        
601
        _set_stats: function(stats) {
602
            var silent = silent === undefined ? false : silent;
603
            // unavailable stats while building
604
            if (this.get("status") == "BUILD") { 
605
                this.stats_available = false;
606
            } else { this.stats_available = true; }
607

    
608
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
609
            
610
            this.set({stats: stats}, {silent:true});
611
            this.trigger("stats:update", stats);
612
        },
613

    
614
        unbind: function() {
615
            models.VM.__super__.unbind.apply(this, arguments);
616
        },
617

    
618
        handle_stats_error: function() {
619
            stats = {};
620
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
621
                stats[k] = false;
622
            });
623

    
624
            this.set({'stats': stats});
625
        },
626

    
627
        // this method gets executed after a successful vm stats api call
628
        handle_stats_update: function(data) {
629
            var self = this;
630
            // avoid browser caching
631
            
632
            if (data.stats && _.size(data.stats) > 0) {
633
                var ts = $.now();
634
                var stats = data.stats;
635
                var images_loaded = 0;
636
                var images = {};
637

    
638
                function check_images_loaded() {
639
                    images_loaded++;
640

    
641
                    if (images_loaded == 4) {
642
                        self._set_stats(images);
643
                    }
644
                }
645
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
646
                    
647
                    stats[k] = stats[k] + "?_=" + ts;
648
                    
649
                    var stat = k.slice(0,3);
650
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
651
                    var img = $("<img />");
652
                    var val = stats[k];
653
                    
654
                    // load stat image to a temporary dom element
655
                    // update model stats on image load/error events
656
                    img.load(function() {
657
                        images[k] = val;
658
                        check_images_loaded();
659
                    });
660

    
661
                    img.error(function() {
662
                        images[stat + type] = false;
663
                        check_images_loaded();
664
                    });
665

    
666
                    img.attr({'src': stats[k]});
667
                })
668
                data.stats = stats;
669
            }
670

    
671
            // do we need to change the interval ??
672
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
673
                this.stats_update_interval = data.stats.refresh * 1000;
674
                this.stats_fetcher.timeout = this.stats_update_interval;
675
                this.stats_fetcher.stop();
676
                this.stats_fetcher.start();
677
            }
678
        },
679

    
680
        // helper method that sets the do_update_stats
681
        // in the future this method could also make an api call
682
        // immediaetly if needed
683
        enable_stats_update: function() {
684
            this.do_update_stats = true;
685
        },
686
        
687
        handle_destroy: function() {
688
            this.stats_fetcher.stop();
689
        },
690

    
691
        require_reboot: function() {
692
            if (this.is_active()) {
693
                this.set({'reboot_required': true});
694
            }
695
        },
696
        
697
        set_pending_action: function(data) {
698
            this.pending_action = data;
699
            return data;
700
        },
701

    
702
        // machine has pending action
703
        update_pending_action: function(action, force) {
704
            this.set({pending_action: action});
705
        },
706

    
707
        clear_pending_action: function() {
708
            this.set({pending_action: undefined});
709
        },
710

    
711
        has_pending_action: function() {
712
            return this.get("pending_action") ? this.get("pending_action") : false;
713
        },
714
        
715
        // machine is active
716
        is_active: function() {
717
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
718
        },
719
        
720
        // machine is building 
721
        is_building: function() {
722
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
723
        },
724

    
725
        // user can connect to machine
726
        is_connectable: function() {
727
            // check if ips exist
728
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
729
                return false;
730
            }
731
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
732
        },
733
        
734
        set_firewalls: function(data) {
735
            _.each(data, _.bind(function(val, key){
736
                if (this.pending_firewalls && this.pending_firewalls[key] && this.pending_firewalls[key] == val) {
737
                        this.require_reboot();
738
                        this.remove_pending_firewall(key, val);
739
                }
740
            }, this));
741
            return data;
742
        },
743

    
744
        remove_pending_firewall: function(net_id, value) {
745
            if (this.pending_firewalls[net_id] == value) {
746
                delete this.pending_firewalls[net_id];
747
                storage.networks.get(net_id).update_state();
748
            }
749
        },
750
            
751
        remove_meta: function(key, complete, error) {
752
            var url = this.api_path() + "/meta/" + key;
753
            this.api.call(url, "delete", undefined, complete, error);
754
        },
755

    
756
        save_meta: function(meta, complete, error) {
757
            var url = this.api_path() + "/meta/" + meta.key;
758
            var payload = {meta:{}};
759
            payload.meta[meta.key] = meta.value;
760

    
761
            this.api.call(url, "update", payload, complete, error)
762
        },
763

    
764
        set_firewall: function(net_id, value, callback) {
765
            if (this.get("firewalls") && this.get("firewalls")[net_id] == value) { return }
766

    
767
            this.pending_firewalls[net_id] = value;
768
            this.trigger("change", this, this);
769
            this.api.call(this.api_path() + "/action", "create", {"firewallProfile":{"profile":value}}, callback);
770
            storage.networks.get(net_id).update_state();
771
        },
772

    
773
        firewall_pending: function(net_id) {
774
            return this.pending_firewalls[net_id] != undefined;
775
        },
776
        
777
        // update/get the state of the machine
778
        state: function() {
779
            var args = slice.call(arguments);
780
                
781
            // TODO: it might not be a good idea to set the state in set_state method
782
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
783
                this.set({'state': args[0]});
784
            }
785

    
786
            return this.get('state');
787
        },
788
        
789
        // get the state that the api status corresponds to
790
        state_for_api_status: function(status) {
791
            return this.state_transition(this.state(), status);
792
        },
793
        
794
        // vm state equals vm api status
795
        state_is_status: function(state) {
796
            return models.VM.STATUSES.indexOf(state) != -1;
797
        },
798
        
799
        // get transition state for the corresponging api status
800
        state_transition: function(state, new_status) {
801
            var statuses = models.VM.STATES_TRANSITIONS[state];
802
            if (statuses) {
803
                if (statuses.indexOf(new_status) > -1) {
804
                    return new_status;
805
                } else {
806
                    return state;
807
                }
808
            } else {
809
                return new_status;
810
            }
811
        },
812
        
813
        // the current vm state is a transition state
814
        in_transition: function() {
815
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
816
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
817
        },
818
        
819
        // get image object
820
        // TODO: update images synchronously if image not found
821
        get_image: function() {
822
            var image = storage.images.get(this.get('imageRef'));
823
            if (!image) {
824
                storage.images.update_unknown_id(this.get('imageRef'));
825
                image = storage.flavors.get(this.get('imageRef'));
826
            }
827
            return image;
828
        },
829
        
830
        // get flavor object
831
        // TODO: update flavors synchronously if image not found
832
        get_flavor: function() {
833
            var flv = storage.flavors.get(this.get('flavorRef'));
834
            if (!flv) {
835
                storage.flavors.update_unknown_id(this.get('flavorRef'));
836
                flv = storage.flavors.get(this.get('flavorRef'));
837
            }
838
            return flv;
839
        },
840

    
841
        // retrieve the metadata object
842
        get_meta: function() {
843
            //return {
844
                //'OS': 'debian',
845
                //'username': 'vinilios',
846
                //'group': 'webservers',
847
                //'meta2': 'meta value',
848
                //'looooooooooooooooong meta': 'short value',
849
                //'short meta': 'loooooooooooooooooooooooooooooooooong value',
850
                //'21421': 'fdsfds fds',
851
                //'21421': 'fdsfds fds',
852
                //'1fds 21421': 'fdsfds fds',
853
                //'fds 21421': 'fdsfds fds',
854
                //'fge 21421': 'fdsfds fds',
855
                //'21421 rew rew': 'fdsfds fds'
856
            //}
857
            try {
858
                return this.get('metadata').values
859
            } catch (err) {
860
                return {};
861
            }
862
        },
863
        
864
        // get metadata OS value
865
        get_os: function() {
866
            return this.get_meta().OS;
867
        },
868
        
869
        // get public ip addresses
870
        // TODO: public network is always the 0 index ???
871
        get_addresses: function(net_id) {
872
            var net_id = net_id || "public";
873
            
874
            var info = this.get_network_info(net_id);
875
            if (!info) { return {} };
876
            addrs = {};
877
            _.each(info.values, function(addr) {
878
                addrs["ip" + addr.version] = addr.addr;
879
            });
880
            return addrs
881
        },
882

    
883
        get_network_info: function(net_id) {
884
            var net_id = net_id || "public";
885
            
886
            if (!this.networks.network_ids.length) { return {} };
887

    
888
            var addresses = this.networks.get();
889
            try {
890
                return _.select(addresses, function(net, key){return key == net_id })[0];
891
            } catch (err) {
892
                //this.log.debug("Cannot find network {0}".format(net_id))
893
            }
894
        },
895

    
896
        firewall_profile: function(net_id) {
897
            var net_id = net_id || "public";
898
            var firewalls = this.get("firewalls");
899
            return firewalls[net_id];
900
        },
901

    
902
        has_firewall: function(net_id) {
903
            var net_id = net_id || "public";
904
            return ["ENABLED","PROTECTED"].indexOf(this.firewall_profile()) > -1;
905
        },
906
    
907
        // get actions that the user can execute
908
        // depending on the vm state/status
909
        get_available_actions: function() {
910
            return models.VM.AVAILABLE_ACTIONS[this.state()];
911
        },
912

    
913
        set_profile: function(profile, net_id) {
914
        },
915
        
916
        // call rename api
917
        rename: function(new_name) {
918
            //this.set({'name': new_name});
919
            this.sync("update", this, {
920
                data: {
921
                    'server': {
922
                        'name': new_name
923
                    }
924
                }, 
925
                // do the rename after the method succeeds
926
                success: _.bind(function(){
927
                    //this.set({name: new_name});
928
                    snf.api.trigger("call");
929
                }, this)
930
            });
931
        },
932
        
933
        get_console_url: function(data) {
934
            var url_params = {
935
                machine: this.get("name"),
936
                host_ip: this.get_addresses().ip4,
937
                host_ip_v6: this.get_addresses().ip6,
938
                host: data.host,
939
                port: data.port,
940
                password: data.password
941
            }
942
            return '/machines/console?' + $.param(url_params);
943
        },
944

    
945
        // action helper
946
        call: function(action_name, success, error) {
947
            var id_param = [this.id];
948

    
949
            success = success || function() {};
950
            error = error || function() {};
951

    
952
            var self = this;
953

    
954
            switch(action_name) {
955
                case 'start':
956
                    this.__make_api_call(this.get_action_url(), // vm actions url
957
                                         "create", // create so that sync later uses POST to make the call
958
                                         {start:{}}, // payload
959
                                         function() {
960
                                             // set state after successful call
961
                                             self.state("START"); 
962
                                             success.apply(this, arguments);
963
                                             snf.api.trigger("call");
964
                                         },  
965
                                         error, 'start');
966
                    break;
967
                case 'reboot':
968
                    this.__make_api_call(this.get_action_url(), // vm actions url
969
                                         "create", // create so that sync later uses POST to make the call
970
                                         {reboot:{type:"HARD"}}, // payload
971
                                         function() {
972
                                             // set state after successful call
973
                                             self.state("REBOOT"); 
974
                                             success.apply(this, arguments)
975
                                             snf.api.trigger("call");
976
                                             self.set({'reboot_required': false});
977
                                         },
978
                                         error, 'reboot');
979
                    break;
980
                case 'shutdown':
981
                    this.__make_api_call(this.get_action_url(), // vm actions url
982
                                         "create", // create so that sync later uses POST to make the call
983
                                         {shutdown:{}}, // payload
984
                                         function() {
985
                                             // set state after successful call
986
                                             self.state("SHUTDOWN"); 
987
                                             success.apply(this, arguments)
988
                                             snf.api.trigger("call");
989
                                         },  
990
                                         error, 'shutdown');
991
                    break;
992
                case 'console':
993
                    this.__make_api_call(this.url() + "/action", "create", {'console': {'type':'vnc'}}, function(data) {
994
                        var cons_data = data.console;
995
                        success.apply(this, [cons_data]);
996
                    }, undefined, 'console')
997
                    break;
998
                case 'destroy':
999
                    this.__make_api_call(this.url(), // vm actions url
1000
                                         "delete", // create so that sync later uses POST to make the call
1001
                                         undefined, // payload
1002
                                         function() {
1003
                                             // set state after successful call
1004
                                             self.state('DESTROY');
1005
                                             success.apply(this, arguments)
1006
                                         },  
1007
                                         error, 'destroy');
1008
                    break;
1009
                default:
1010
                    throw "Invalid VM action ("+action_name+")";
1011
            }
1012
        },
1013
        
1014
        __make_api_call: function(url, method, data, success, error, action) {
1015
            var self = this;
1016
            error = error || function(){};
1017
            success = success || function(){};
1018

    
1019
            var params = {
1020
                url: url,
1021
                data: data,
1022
                success: function(){ self.handle_action_succeed.apply(self, arguments); success.apply(this, arguments)},
1023
                error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)},
1024
                error_params: { ns: "Machines actions", 
1025
                                message: "'" + this.get("name") + "'" + " action failed", 
1026
                                extra_details: { 'Machine ID': this.id, 'URL': url, 'Action': action || "undefined" },
1027
                                allow_reload: false
1028
                              },
1029
                handles_error: true
1030
            }
1031
            this.sync(method, this, params);
1032
        },
1033

    
1034
        handle_action_succeed: function() {
1035
            this.trigger("action:success", arguments);
1036
        },
1037
        
1038
        reset_action_error: function() {
1039
            this.action_error = false;
1040
            this.trigger("action:fail:reset", this.action_error);
1041
        },
1042

    
1043
        handle_action_fail: function() {
1044
            this.action_error = arguments;
1045
            this.trigger("action:fail", arguments);
1046
        },
1047

    
1048
        get_action_url: function(name) {
1049
            return this.url() + "/action";
1050
        },
1051

    
1052
        get_connection_info: function(host_os, success, error) {
1053
            var url = "/machines/connect";
1054
            params = {
1055
                ip_address: this.get_addresses().ip4,
1056
                os: this.get_os(),
1057
                host_os: host_os,
1058
                srv: this.id
1059
            }
1060

    
1061
            url = url + "?" + $.param(params);
1062

    
1063
            var ajax = snf.api.sync("read", undefined, { url: url, 
1064
                                                         error:error, 
1065
                                                         success:success, 
1066
                                                         handles_error:1});
1067
        }
1068
    })
1069
    
1070
    models.VM.ACTIONS = [
1071
        'start',
1072
        'shutdown',
1073
        'reboot',
1074
        'console',
1075
        'destroy'
1076
    ]
1077

    
1078
    models.VM.AVAILABLE_ACTIONS = {
1079
        'UNKNWON'       : ['destroy'],
1080
        'BUILD'         : ['destroy'],
1081
        'REBOOT'        : ['shutdown', 'destroy', 'console'],
1082
        'STOPPED'       : ['start', 'destroy'],
1083
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1084
        'ERROR'         : ['destroy'],
1085
        'DELETED'        : [],
1086
        'DESTROY'       : [],
1087
        'BUILD_INIT'    : ['destroy'],
1088
        'BUILD_COPY'    : ['destroy'],
1089
        'BUILD_FINAL'   : ['destroy'],
1090
        'SHUTDOWN'      : ['destroy'],
1091
        'START'         : [],
1092
        'CONNECT'       : [],
1093
        'DISCONNECT'    : []
1094
    }
1095

    
1096
    // api status values
1097
    models.VM.STATUSES = [
1098
        'UNKNWON',
1099
        'BUILD',
1100
        'REBOOT',
1101
        'STOPPED',
1102
        'ACTIVE',
1103
        'ERROR',
1104
        'DELETED'
1105
    ]
1106

    
1107
    // api status values
1108
    models.VM.CONNECT_STATES = [
1109
        'ACTIVE',
1110
        'REBOOT',
1111
        'SHUTDOWN'
1112
    ]
1113

    
1114
    // vm states
1115
    models.VM.STATES = models.VM.STATUSES.concat([
1116
        'DESTROY',
1117
        'BUILD_INIT',
1118
        'BUILD_COPY',
1119
        'BUILD_FINAL',
1120
        'SHUTDOWN',
1121
        'START',
1122
        'CONNECT',
1123
        'DISCONNECT',
1124
        'FIREWALL'
1125
    ]);
1126
    
1127
    models.VM.STATES_TRANSITIONS = {
1128
        'DESTROY' : ['DELETED'],
1129
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1130
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1131
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1132
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1133
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1134
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1135
        'BUILD_COPY': ['ERROR', 'ACTIVE', 'BUILD_FINAL', 'DESTROY'],
1136
        'BUILD_FINAL': ['ERROR', 'ACTIVE', 'DESTROY'],
1137
        'BUILD_INIT': ['ERROR', 'ACTIVE', 'BUILD_COPY', 'BUILD_FINAL', 'DESTROY']
1138
    }
1139

    
1140
    models.VM.TRANSITION_STATES = [
1141
        'DESTROY',
1142
        'SHUTDOWN',
1143
        'START',
1144
        'REBOOT',
1145
        'BUILD'
1146
    ]
1147

    
1148
    models.VM.ACTIVE_STATES = [
1149
        'BUILD', 'REBOOT', 'ACTIVE',
1150
        'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL',
1151
        'SHUTDOWN', 'CONNECT', 'DISCONNECT', 'DESTROY'
1152
    ]
1153

    
1154
    models.VM.BUILDING_STATES = [
1155
        'BUILD', 'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL'
1156
    ]
1157

    
1158
    models.Networks = models.Collection.extend({
1159
        model: models.Network,
1160
        path: 'networks',
1161
        details: true,
1162
        //noUpdate: true,
1163
        defaults: {'linked_to':[]},
1164

    
1165
        parse: function (resp, xhr) {
1166
            // FIXME: depricated global var
1167
            if (!resp) { return []};
1168
               
1169
            var data = _.map(resp.networks.values, _.bind(this.parse_net_api_data, this));
1170
            return data;
1171
        },
1172

    
1173
        parse_net_api_data: function(data) {
1174
            if (data.servers && data.servers.values) {
1175
                data['linked_to'] = data.servers.values;
1176
            }
1177
            return data;
1178
        },
1179

    
1180
        create: function (name, callback) {
1181
            return this.api.call(this.path, "create", {network:{name:name}}, callback);
1182
        }
1183
    })
1184

    
1185
    models.Images = models.Collection.extend({
1186
        model: models.Image,
1187
        path: 'images',
1188
        details: true,
1189
        noUpdate: true,
1190
        
1191
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1192

    
1193
        // update collection model with id passed
1194
        // making a direct call to the flavor
1195
        // api url
1196
        update_unknown_id: function(id) {
1197
            var url = getUrl.call(this) + "/" + id;
1198
            this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, 
1199
            _.bind(function() {
1200
                this.add({id:id, name:"Unknown image", size:-1, progress:100, status:"DELETED"})
1201
            }, this), _.bind(function(image) {
1202
                this.add(image.image);
1203
            }, this));
1204
        },
1205

    
1206
        parse: function (resp, xhr) {
1207
            // FIXME: depricated global var
1208
            var data = _.map(resp.images.values, _.bind(this.parse_meta, this));
1209
            return resp.images.values;
1210
        },
1211

    
1212
        get_meta_key: function(img, key) {
1213
            if (img.metadata && img.metadata.values && img.metadata.values[key]) {
1214
                return img.metadata.values[key];
1215
            }
1216
            return undefined;
1217
        },
1218

    
1219
        parse_meta: function(img) {
1220
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1221
                img[key] = this.get_meta_key(img, key);
1222
            }, this));
1223
            return img;
1224
        },
1225

    
1226
        active: function() {
1227
            return this.filter(function(img){return img.get('status') != "DELETED"});
1228
        }
1229
    })
1230

    
1231
    models.Flavors = models.Collection.extend({
1232
        model: models.Flavor,
1233
        path: 'flavors',
1234
        details: true,
1235
        noUpdate: true,
1236
        
1237
        // update collection model with id passed
1238
        // making a direct call to the flavor
1239
        // api url
1240
        update_unknown_id: function(id) {
1241
            var url = getUrl.call(this) + "/" + id;
1242
            this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, 
1243
            _.bind(function() {
1244
                this.add({id:id, cpu:"", ram:"", disk:"", name: "", status:"DELETED"})
1245
            }, this), _.bind(function(flv) {
1246
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1247
                this.add(flv.flavor);
1248
            }, this));
1249
        },
1250

    
1251
        parse: function (resp, xhr) {
1252
            // FIXME: depricated global var
1253
            return resp.flavors.values;
1254
        },
1255

    
1256
        unavailable_values_for_image: function(img, flavors) {
1257
            var flavors = flavors || this.active();
1258
            var size = img.get_size();
1259
            
1260
            var index = {cpu:[], disk:[], ram:[]};
1261

    
1262
            _.each(this.active(), function(el) {
1263
                var img_size = size;
1264
                var flv_size = el.get_disk_size();
1265
                if (flv_size < img_size) {
1266
                    if (index.disk.indexOf(flv_size) == -1) {
1267
                        index.disk.push(flv_size);
1268
                    }
1269
                };
1270
            });
1271
            
1272
            return index;
1273
        },
1274

    
1275
        get_flavor: function(cpu, mem, disk, filter_list) {
1276
            if (!filter_list) { filter_list = this.models };
1277
            return this.select(function(flv){
1278
                if (flv.get("cpu") == cpu + "" &&
1279
                   flv.get("ram") == mem + "" &&
1280
                   flv.get("disk") == disk + "" &&
1281
                   filter_list.indexOf(flv) > -1) { return true; }
1282
            })[0]
1283
        },
1284
        
1285
        get_data: function(lst) {
1286
            var data = {'cpu': [], 'mem':[], 'disk':[]};
1287

    
1288
            _.each(lst, function(flv) {
1289
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1290
                    data.cpu.push(flv.get("cpu"));
1291
                }
1292
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1293
                    data.mem.push(flv.get("ram"));
1294
                }
1295
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1296
                    data.disk.push(flv.get("disk"));
1297
                }
1298
            })
1299
            
1300
            return data;
1301
        },
1302

    
1303
        active: function() {
1304
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1305
        }
1306
            
1307
    })
1308

    
1309
    models.VMS = models.Collection.extend({
1310
        model: models.VM,
1311
        path: 'servers',
1312
        details: true,
1313
        copy_image_meta: true,
1314
        
1315
        parse: function (resp, xhr) {
1316
            // FIXME: depricated after refactoring
1317
            var data = resp;
1318
            if (!resp) { return [] };
1319
            data = _.filter(_.map(resp.servers.values, _.bind(this.parse_vm_api_data, this)), function(v){return v});
1320
            return data;
1321
        },
1322
        
1323
        get_reboot_required: function() {
1324
            return this.filter(function(vm){return vm.get("reboot_required") == true})
1325
        },
1326

    
1327
        has_pending_actions: function() {
1328
            return this.filter(function(vm){return vm.pending_action}).length > 0;
1329
        },
1330

    
1331
        reset_pending_actions: function() {
1332
            this.each(function(vm) {
1333
                vm.clear_pending_action();
1334
            })
1335
        },
1336

    
1337
        reset_stats_update: function() {
1338
            this.each(function(vm) {
1339
                vm.do_update_stats = false;
1340
            })
1341
        },
1342
        
1343
        has_meta: function(vm_data) {
1344
            return vm_data.metadata && vm_data.metadata.values
1345
        },
1346

    
1347
        has_addresses: function(vm_data) {
1348
            return vm_data.metadata && vm_data.metadata.values
1349
        },
1350

    
1351
        parse_vm_api_data: function(data) {
1352
            // do not add non existing DELETED entries
1353
            if (data.status && data.status == "DELETED") {
1354
                if (!this.get(data.id)) {
1355
                    console.error("non exising deleted vm", data)
1356
                    return false;
1357
                }
1358
            }
1359

    
1360
            // OS attribute
1361
            if (this.has_meta(data)) {
1362
                data['OS'] = data.metadata.values.OS || "undefined";
1363
            }
1364
            
1365
            data['firewalls'] = {};
1366
            if (data['addresses'] && data['addresses'].values) {
1367
                data['linked_to_nets'] = data['addresses'].values;
1368
                _.each(data['addresses'].values, function(f){
1369
                    if (f['firewallProfile']) {
1370
                        data['firewalls'][f['id']] = f['firewallProfile']
1371
                    }
1372
                });
1373
            }
1374
            
1375
            // if vm has no metadata, no metadata object
1376
            // is in json response, reset it to force
1377
            // value update
1378
            if (!data['metadata']) {
1379
                data['metadata'] = {values:{}};
1380
            }
1381

    
1382
            return data;
1383
        },
1384

    
1385
        create: function (name, image, flavor, meta, extra, callback) {
1386
            if (this.copy_image_meta) {
1387
                meta['OS'] = image.get("OS");
1388
           }
1389
            
1390
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta}
1391
            opts = _.extend(opts, extra);
1392

    
1393
            this.api.call(this.path, "create", {'server': opts}, undefined, undefined, callback);
1394
        }
1395

    
1396
    })
1397
    
1398

    
1399
    // storage initialization
1400
    snf.storage.images = new models.Images();
1401
    snf.storage.flavors = new models.Flavors();
1402
    snf.storage.networks = new models.Networks();
1403
    snf.storage.vms = new models.VMS();
1404

    
1405
    //snf.storage.vms.fetch({update:true});
1406
    //snf.storage.images.fetch({update:true});
1407
    //snf.storage.flavors.fetch({update:true});
1408

    
1409
})(this);