Statistics
| Branch: | Tag: | Revision:

root / ui / static / snf / js / models.js @ 101e6604

History | View | Annotate | Download (57.3 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
                    try { this.stop_stats_update();} catch (err) {};
48
                    this.collection.remove(this.id);
49
                }
50
            }
51
        },
52
        
53
        // custom set method to allow submodels to use
54
        // set_<attr> methods for handling the value of each
55
        // attribute and overriding the default set method
56
        // for specific parameters
57
        set: function(params, options) {
58
            _.each(params, _.bind(function(value, key){
59
                if (this["set_" + key]) {
60
                    params[key] = this["set_" + key](value);
61
                }
62
            }, this))
63
            var ret = bb.Model.prototype.set.call(this, params, options);
64
            return ret;
65
        },
66

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

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

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

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

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

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

    
95
    })
96
    
97
    // Base object for all our model collections
98
    models.Collection = bb.Collection.extend({
99
        sync: snf.api.sync,
100
        api: snf.api,
101
        supportIncUpdates: true,
102
        url: function(options) {
103
            return getUrl.call(this) + (options.details || this.details ? '/detail' : '');
104
        },
105

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

    
117
        get_fetcher: function(timeout, fast, limit, initial, params) {
118
            var fetch_params = params || {};
119
            fetch_params.skips_timeouts = true;
120

    
121
            var timeout = parseInt(timeout);
122
            var fast = fast || 1000;
123
            var limit = limit;
124
            var initial_call = initial || true;
125
            
126
            var last_ajax = undefined;
127
            var cb = _.bind(function() {
128
                // clone to avoid referenced objects
129
                var params = _.clone(fetch_params);
130
                updater._ajax = last_ajax;
131
                if (last_ajax) {
132
                    last_ajax.abort();
133
                }
134
                last_ajax = this.fetch(params);
135
            }, this);
136
            var updater = new snf.api.updateHandler({'callback': cb, timeout:timeout, 
137
                                                    fast:fast, limit:limit, 
138
                                                    call_on_start:initial_call});
139

    
140
            snf.api.bind("call", _.throttle(_.bind(function(){ updater.faster(true)}, this)), 1000);
141
            return updater;
142
        }
143
    });
144
    
145
    // Image model
146
    models.Image = models.Model.extend({
147
        path: 'images',
148

    
149
        get_size: function() {
150
            return parseInt(this.get('metadata') ? this.get('metadata').values.size : -1)
151
        },
152

    
153
        get_readable_size: function() {
154
            return this.get_size() > 0 ? util.readablizeBytes(this.get_size() * 1024 * 1024) : "unknown";
155
        },
156

    
157
        get_os: function() {
158
            return this.get("OS");
159
        },
160

    
161
        get_sort_order: function() {
162
            return parseInt(this.get('metadata') ? this.get('metadata').values.sortorder : -1)
163
        }
164
    });
165

    
166
    // Flavor model
167
    models.Flavor = models.Model.extend({
168
        path: 'flavors',
169

    
170
        details_string: function() {
171
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
172
        },
173

    
174
        get_disk_size: function() {
175
            return parseInt(this.get("disk") * 1000)
176
        }
177

    
178
    });
179
    
180
    //network vms list helper
181
    var NetworkVMSList = function() {
182
        this.initialize = function() {
183
            this.vms = [];
184
            this.pending = [];
185
            this.pending_for_removal = [];
186
        }
187
        
188
        this.add_pending_for_remove = function(vm_id) {
189
            if (this.pending_for_removal.indexOf(vm_id) == -1) {
190
                this.pending_for_removal.push(vm_id);
191
            }
192

    
193
            if (this.pending_for_removal.length) {
194
                this.trigger("pending:remove:add");
195
            }
196
        },
197

    
198
        this.add_pending = function(vm_id) {
199
            if (this.pending.indexOf(vm_id) == -1) {
200
                this.pending[this.pending.length] = vm_id;
201
            }
202

    
203
            if (this.pending.length) {
204
                this.trigger("pending:add");
205
            }
206
        }
207

    
208
        this.check_pending = function() {
209
            var len = this.pending.length;
210
            var args = [this.pending];
211
            this.pending = _.difference(this.pending, this.vms);
212
            if (len != this.pending.length) {
213
                if (this.pending.length == 0) {
214
                    this.trigger("pending:clear");
215
                }
216
            }
217

    
218
            var len = this.pending_for_removal.length;
219
            this.pending_for_removal = _.intersection(this.pending_for_removal, this.vms);
220
            if (this.pending_for_removal.length == 0) {
221
                this.trigger("pending:remove:clear");
222
            }
223

    
224
        }
225

    
226

    
227
        this.add = function(vm_id) {
228
            if (this.vms.indexOf(vm_id) == -1) {
229
                this.vms[this.vms.length] = vm_id;
230
                this.trigger("network:connect", vm_id);
231
                this.check_pending();
232
                return true;
233
            }
234
        }
235

    
236
        this.remove = function(vm_id) {
237
            if (this.vms.indexOf(vm_id) > -1) {
238
                this.vms = _.without(this.vms, vm_id);
239
                this.trigger("network:disconnect", vm_id);
240
                this.check_pending();
241
                return true;
242
            }
243
        }
244

    
245
        this.get = function() {
246
            return this.vms;
247
        }
248

    
249
        this.list = function() {
250
            return storage.vms.filter(_.bind(function(vm){
251
                return this.vms.indexOf(vm.id) > -1;
252
            }, this))
253
        }
254

    
255
        this.initialize();
256
    };
257
    _.extend(NetworkVMSList.prototype, bb.Events);
258
    
259
    // vm networks list helper
260
    var VMNetworksList = function() {
261
        this.initialize = function() {
262
            this.networks = {};
263
            this.network_ids = [];
264
        }
265

    
266
        this.add = function(net_id, data) {
267
            if (!this.networks[net_id]) {
268
                this.networks[net_id] = data || {};
269
                this.network_ids[this.network_ids.length] = net_id;
270
                this.trigger("network:connect", net_id);
271
                return true;
272
            }
273
        }
274

    
275
        this.remove = function(net_id) {
276
            if (this.networks[net_id]) {
277
                delete this.networks[net_id];
278
                this.network_ids = _.without(this.network_ids, net_id);
279
                this.trigger("network:disconnect", net_id);
280
                return true;
281
            }
282
            return false;
283
        }
284

    
285
        this.get = function() {
286
            return this.networks;
287
        }
288

    
289
        this.list = function() {
290
            return storage.networks.filter(_.bind(function(net){
291
                return this.network_ids.indexOf(net.id) > -1;
292
            }, this))
293
        }
294

    
295
        this.initialize();
296
    };
297
    _.extend(VMNetworksList.prototype, bb.Events);
298
        
299
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
300
    _.extend(models.ParamsList.prototype, bb.Events, {
301

    
302
        initialize: function(parent, param_name) {
303
            this.parent = parent;
304
            this.actions = {};
305
            this.param_name = param_name;
306
            this.length = 0;
307
        },
308
        
309
        has_action: function(action) {
310
            return this.actions[action] ? true : false;
311
        },
312
            
313
        _parse_params: function(arguments) {
314
            if (arguments.length <= 1) {
315
                return [];
316
            }
317

    
318
            var args = _.toArray(arguments);
319
            return args.splice(1);
320
        },
321

    
322
        contains: function(action, params) {
323
            params = this._parse_params(arguments);
324
            var has_action = this.has_action(action);
325
            if (!has_action) { return false };
326

    
327
            var paramsEqual = false;
328
            _.each(this.actions[action], function(action_params) {
329
                if (_.isEqual(action_params, params)) {
330
                    paramsEqual = true;
331
                }
332
            });
333
                
334
            return paramsEqual;
335
        },
336
        
337
        is_empty: function() {
338
            return _.isEmpty(this.actions);
339
        },
340

    
341
        add: function(action, params) {
342
            params = this._parse_params(arguments);
343
            if (this.contains.apply(this, arguments)) { return this };
344
            var isnew = false
345
            if (!this.has_action(action)) {
346
                this.actions[action] = [];
347
                isnew = true;
348
            };
349

    
350
            this.actions[action].push(params);
351
            this.parent.trigger("change:" + this.param_name, this.parent, this);
352
            if (isnew) {
353
                this.trigger("add", action, params);
354
            } else {
355
                this.trigger("change", action, params);
356
            }
357
            return this;
358
        },
359
        
360
        remove_all: function(action) {
361
            if (this.has_action(action)) {
362
                delete this.actions[action];
363
                this.parent.trigger("change:" + this.param_name, this.parent, this);
364
                this.trigger("remove", action);
365
            }
366
            return this;
367
        },
368

    
369
        reset: function() {
370
            this.actions = {};
371
            this.parent.trigger("change:" + this.param_name, this.parent, this);
372
            this.trigger("reset");
373
            this.trigger("remove");
374
        },
375

    
376
        remove: function(action, params) {
377
            params = this._parse_params(arguments);
378
            if (!this.has_action(action)) { return this };
379
            var index = -1;
380
            _.each(this.actions[action], _.bind(function(action_params) {
381
                if (_.isEqual(action_params, params)) {
382
                    index = this.actions[action].indexOf(action_params);
383
                }
384
            }, this));
385
            
386
            if (index > -1) {
387
                this.actions[action].splice(index, 1);
388
                if (_.isEmpty(this.actions[action])) {
389
                    delete this.actions[action];
390
                }
391
                this.parent.trigger("change:" + this.param_name, this.parent, this);
392
                this.trigger("remove", action, params);
393
            }
394
        }
395

    
396
    });
397

    
398
    // Image model
399
    models.Network = models.Model.extend({
400
        path: 'networks',
401
        has_status: true,
402
        
403
        initialize: function() {
404
            this.vms = new NetworkVMSList();
405
            this.vms.bind("pending:add", _.bind(this.handle_pending_connections, this, "add"));
406
            this.vms.bind("pending:clear", _.bind(this.handle_pending_connections, this, "clear"));
407
            this.vms.bind("pending:remove:add", _.bind(this.handle_pending_connections, this, "add"));
408
            this.vms.bind("pending:remove:clear", _.bind(this.handle_pending_connections, this, "clear"));
409

    
410
            var ret = models.Network.__super__.initialize.apply(this, arguments);
411

    
412
            storage.vms.bind("change:linked_to_nets", _.bind(this.update_connections, this, "vm:change"));
413
            storage.vms.bind("add", _.bind(this.update_connections, this, "add"));
414
            storage.vms.bind("remove", _.bind(this.update_connections, this, "remove"));
415
            storage.vms.bind("reset", _.bind(this.update_connections, this, "reset"));
416

    
417
            this.bind("change:linked_to", _.bind(this.update_connections, this, "net:change"));
418
            this.update_connections();
419
            this.update_state();
420
            
421
            this.set({"actions": new models.ParamsList(this, "actions")});
422

    
423
            return ret;
424
        },
425

    
426
        update_state: function() {
427
            if (this.vms.pending.length) {
428
                this.set({state: "CONNECTING"});
429
                return
430
            }
431

    
432
            if (this.vms.pending_for_removal.length) {
433
                this.set({state: "DISCONNECTING"});
434
                return
435
            }   
436
            
437
            var firewalling = false;
438
            _.each(this.vms.get(), _.bind(function(vm_id){
439
                var vm = storage.vms.get(vm_id);
440
                if (!vm) { return };
441
                if (!_.isEmpty(vm.pending_firewalls)) {
442
                    this.set({state:"FIREWALLING"});
443
                    firewalling = true;
444
                    return false;
445
                }
446
            },this));
447
            if (firewalling) { return };
448

    
449
            this.set({state:"NORMAL"});
450
        },
451

    
452
        handle_pending_connections: function(action) {
453
            this.update_state();
454
        },
455

    
456
        // handle vm/network connections
457
        update_connections: function(action, model) {
458
            
459
            // vm removed disconnect vm from network
460
            if (action == "remove") {
461
                var removed_from_net = this.vms.remove(model.id);
462
                var removed_from_vm = model.networks.remove(this.id);
463
                if (removed_from_net) {this.trigger("vm:disconnect", model, this); this.change()};
464
                if (removed_from_vm) {model.trigger("network:disconnect", this, model); this.change()};
465
                return;
466
            }
467
            
468
            // update links for all vms
469
            var links = this.get("linked_to");
470
            storage.vms.each(_.bind(function(vm) {
471
                var vm_links = vm.get("linked_to") || [];
472
                if (vm_links.indexOf(this.id) > -1) {
473
                    // vm has connection to current network
474
                    if (links.indexOf(vm.id) > -1) {
475
                        // and network has connection to vm, so try
476
                        // to append it
477
                        var add_to_net = this.vms.add(vm.id);
478
                        var index = _.indexOf(vm_links, this.id);
479
                        var add_to_vm = vm.networks.add(this.id, vm.get("linked_to_nets")[index]);
480
                        
481
                        // call only if connection did not existed
482
                        if (add_to_net) {this.trigger("vm:connect", vm, this); this.change()};
483
                        if (add_to_vm) {vm.trigger("network:connect", this, vm); vm.change()};
484
                    } else {
485
                        // no connection, try to remove it
486
                        var removed_from_net = this.vms.remove(vm.id);
487
                        var removed_from_vm = vm.networks.remove(this.id);
488
                        if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
489
                        if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
490
                    }
491
                } else {
492
                    // vm has no connection to current network, try to remove it
493
                    var removed_from_net = this.vms.remove(vm.id);
494
                    var removed_from_vm = vm.networks.remove(this.id);
495
                    if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
496
                    if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
497
                }
498
            },this));
499
        },
500

    
501
        is_public: function() {
502
            return this.id == "public";
503
        },
504

    
505
        contains_vm: function(vm) {
506
            var net_vm_exists = this.vms.get().indexOf(vm.id) > -1;
507
            var vm_net_exists = vm.is_connected_to(this);
508
            return net_vm_exists && vm_net_exists;
509
        },
510
        
511
        call: function(action, params, success, error) {
512
            if (action == "destroy") {
513
                this.set({state:"DESTROY"});
514
                this.get("actions").remove("destroy");
515
                this.remove(_.bind(function(){
516
                    success();
517
                }, this), error);
518
            }
519
            
520
            if (action == "disconnect") {
521
                _.each(params, _.bind(function(vm_id) {
522
                    var vm = snf.storage.vms.get(vm_id);
523
                    this.get("actions").remove("disconnect", vm_id);
524
                    if (vm) {
525
                        this.remove_vm(vm, success, error);
526
                    }
527
                }, this));
528
            }
529
        },
530

    
531
        add_vm: function (vm, callback, error, options) {
532
            var payload = {add:{serverRef:"" + vm.id}};
533
            payload._options = options || {};
534
            return this.api.call(this.api_path() + "/action", "create", 
535
                                 payload,
536
                                 _.bind(function(){
537
                                     this.vms.add_pending(vm.id);
538
                                     if (callback) {callback()}
539
                                 },this), error);
540
        },
541

    
542
        remove_vm: function (vm, callback, error, options) {
543
            var payload = {remove:{serverRef:"" + vm.id}};
544
            payload._options = options || {};
545
            return this.api.call(this.api_path() + "/action", "create", 
546
                                 {remove:{serverRef:"" + vm.id}},
547
                                 _.bind(function(){
548
                                     this.vms.add_pending_for_remove(vm.id);
549
                                     if (callback) {callback()}
550
                                 },this), error);
551
        },
552

    
553
        rename: function(name, callback) {
554
            return this.api.call(this.api_path(), "update", {
555
                network:{name:name}, 
556
                _options:{
557
                    critical: false, 
558
                    error_params:{
559
                        title: "Network action failed",
560
                        ns: "Networks",
561
                        extra_details: {"Network id": this.id}
562
                    }
563
                }}, callback);
564
        },
565

    
566
        get_connectable_vms: function() {
567
            var servers = this.vms.list();
568
            return storage.vms.filter(function(vm){
569
                return servers.indexOf(vm) == -1 && !vm.in_error_state();
570
            })
571
        },
572

    
573
        state_message: function() {
574
            if (this.get("state") == "NORMAL" && this.is_public()) {
575
                return "Public network";
576
            }
577

    
578
            return models.Network.STATES[this.get("state")];
579
        },
580

    
581
        in_progress: function() {
582
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
583
        },
584

    
585
        do_all_pending_actions: function(success, error) {
586
            var destroy = this.get("actions").has_action("destroy");
587
            _.each(this.get("actions").actions, _.bind(function(params, action) {
588
                _.each(params, _.bind(function(with_params) {
589
                    this.call(action, with_params, success, error);
590
                }, this));
591
            }, this));
592
        }
593
    });
594
    
595
    models.Network.STATES = {
596
        'NORMAL': 'Private network',
597
        'CONNECTING': 'Connecting...',
598
        'DISCONNECTING': 'Disconnecting...',
599
        'FIREWALLING': 'Firewall update...',
600
        'DESTROY': 'Destroying...'
601
    }
602

    
603
    models.Network.STATES_TRANSITIONS = {
604
        'CONNECTING': ['NORMAL'],
605
        'DISCONNECTING': ['NORMAL'],
606
        'FIREWALLING': ['NORMAL']
607
    }
608

    
609
    // Virtualmachine model
610
    models.VM = models.Model.extend({
611

    
612
        path: 'servers',
613
        has_status: true,
614
        initialize: function(params) {
615
            this.networks = new VMNetworksList();
616
            
617
            this.pending_firewalls = {};
618
            
619
            models.VM.__super__.initialize.apply(this, arguments);
620

    
621
            this.set({state: params.status || "ERROR"});
622
            this.log = new snf.logging.logger("VM " + this.id);
623
            this.pending_action = undefined;
624
            
625
            // init stats parameter
626
            this.set({'stats': undefined}, {silent: true});
627
            // defaults to not update the stats
628
            // each view should handle this vm attribute 
629
            // depending on if it displays stat images or not
630
            this.do_update_stats = false;
631
            
632
            // interval time
633
            // this will dynamicaly change if the server responds that
634
            // images get refreshed on different intervals
635
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
636
            this.stats_available = false;
637

    
638
            // initialize interval
639
            this.init_stats_intervals(this.stats_update_interval);
640
            
641
            this.bind("change:progress", _.bind(this.update_building_progress, this));
642
            this.update_building_progress();
643

    
644
            this.bind("change:firewalls", _.bind(this.handle_firewall_change, this));
645
            
646
            // default values
647
            this.set({linked_to_nets:this.get("linked_to_nets") || []});
648
            this.set({firewalls:this.get("firewalls") || []});
649

    
650
            this.bind("change:state", _.bind(function(){if (this.state() == "DESTROY") { this.handle_destroy() }}, this))
651
        },
652

    
653
        handle_firewall_change: function() {
654

    
655
        },
656
        
657
        set_linked_to_nets: function(data) {
658
            this.set({"linked_to":_.map(data, function(n){ return n.id})});
659
            return data;
660
        },
661

    
662
        is_connected_to: function(net) {
663
            return _.filter(this.networks.list(), function(n){return n.id == net.id}).length > 0;
664
        },
665
        
666
        status: function(st) {
667
            if (!st) { return this.get("status")}
668
            return this.set({status:st});
669
        },
670

    
671
        set_status: function(st) {
672
            var new_state = this.state_for_api_status(st);
673
            var transition = false;
674

    
675
            if (this.state() != new_state) {
676
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
677
                    transition = this.state();
678
                }
679
            }
680
            
681
            // call it silently to avoid double change trigger
682
            this.set({'state': this.state_for_api_status(st)}, {silent: true});
683
            
684
            // trigger transition
685
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
686
                this.trigger("transition", {from:transition, to:new_state}) 
687
            };
688
            return st;
689
        },
690

    
691
        update_building_progress: function() {
692
            if (this.is_building()) {
693
                var progress = this.get("progress");
694
                if (progress == 0) {
695
                    this.state("BUILD_INIT");
696
                    this.set({progress_message: BUILDING_MESSAGES['INIT']});
697
                }
698
                if (progress > 0 && progress < 99) {
699
                    this.state("BUILD_COPY");
700
                    var params = this.get_copy_details(true);
701
                    this.set({progress_message: BUILDING_MESSAGES['COPY'].format(params.copy, 
702
                                                                                 params.size, 
703
                                                                                 params.progress)});
704
                }
705
                if (progress == 100) {
706
                    this.state("BUILD_FINAL");
707
                    this.set({progress_message: BUILDING_MESSAGES['FINAL']});
708
                }
709
            } else {
710
            }
711
        },
712

    
713
        get_copy_details: function(human, image) {
714
            var human = human || false;
715
            var image = image || this.get_image();
716

    
717
            var progress = this.get('progress');
718
            var size = image.get_size();
719
            var size_copied = (size * progress / 100).toFixed(2);
720
            
721
            if (human) {
722
                size = util.readablizeBytes(size*1024*1024);
723
                size_copied = util.readablizeBytes(size_copied*1024*1024);
724
            }
725
            return {'progress': progress, 'size': size, 'copy': size_copied};
726
        },
727

    
728
        start_stats_update: function(force_if_empty) {
729
            var prev_state = this.do_update_stats;
730

    
731
            this.do_update_stats = true;
732
            
733
            // fetcher initialized ??
734
            if (!this.stats_fetcher) {
735
                this.init_stats_intervals();
736
            }
737

    
738

    
739
            // fetcher running ???
740
            if (!this.stats_fetcher.running || !prev_state) {
741
                this.stats_fetcher.start();
742
            }
743

    
744
            if (force_if_empty && this.get("stats") == undefined) {
745
                this.update_stats(true);
746
            }
747
        },
748

    
749
        stop_stats_update: function(stop_calls) {
750
            this.do_update_stats = false;
751

    
752
            if (stop_calls) {
753
                this.stats_fetcher.stop();
754
            }
755
        },
756

    
757
        // clear and reinitialize update interval
758
        init_stats_intervals: function (interval) {
759
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
760
            this.stats_fetcher.start();
761
        },
762
        
763
        get_stats_fetcher: function(timeout) {
764
            var cb = _.bind(function(data){
765
                this.update_stats();
766
            }, this);
767
            var fetcher = new snf.api.updateHandler({'callback': cb, timeout:timeout});
768
            return fetcher;
769
        },
770

    
771
        // do the api call
772
        update_stats: function(force) {
773
            // do not update stats if flag not set
774
            if ((!this.do_update_stats && !force) || this.updating_stats) {
775
                return;
776
            }
777

    
778
            // make the api call, execute handle_stats_update on sucess
779
            // TODO: onError handler ???
780
            stats_url = this.url() + "/stats";
781
            this.updating_stats = true;
782
            this.sync("GET", this, {
783
                handles_error:true, 
784
                url: stats_url, 
785
                refresh:true, 
786
                success: _.bind(this.handle_stats_update, this),
787
                error: _.bind(this.handle_stats_error, this),
788
                complete: _.bind(function(){this.updating_stats = false;}, this),
789
                critical: false,
790
                display: false,
791
                log_error: false
792
            });
793
        },
794

    
795
        get_stats_image: function(stat, type) {
796
        },
797
        
798
        _set_stats: function(stats) {
799
            var silent = silent === undefined ? false : silent;
800
            // unavailable stats while building
801
            if (this.get("status") == "BUILD") { 
802
                this.stats_available = false;
803
            } else { this.stats_available = true; }
804

    
805
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
806
            
807
            this.set({stats: stats}, {silent:true});
808
            this.trigger("stats:update", stats);
809
        },
810

    
811
        unbind: function() {
812
            models.VM.__super__.unbind.apply(this, arguments);
813
        },
814

    
815
        handle_stats_error: function() {
816
            stats = {};
817
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
818
                stats[k] = false;
819
            });
820

    
821
            this.set({'stats': stats});
822
        },
823

    
824
        // this method gets executed after a successful vm stats api call
825
        handle_stats_update: function(data) {
826
            var self = this;
827
            // avoid browser caching
828
            
829
            if (data.stats && _.size(data.stats) > 0) {
830
                var ts = $.now();
831
                var stats = data.stats;
832
                var images_loaded = 0;
833
                var images = {};
834

    
835
                function check_images_loaded() {
836
                    images_loaded++;
837

    
838
                    if (images_loaded == 4) {
839
                        self._set_stats(images);
840
                    }
841
                }
842
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
843
                    
844
                    stats[k] = stats[k] + "?_=" + ts;
845
                    
846
                    var stat = k.slice(0,3);
847
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
848
                    var img = $("<img />");
849
                    var val = stats[k];
850
                    
851
                    // load stat image to a temporary dom element
852
                    // update model stats on image load/error events
853
                    img.load(function() {
854
                        images[k] = val;
855
                        check_images_loaded();
856
                    });
857

    
858
                    img.error(function() {
859
                        images[stat + type] = false;
860
                        check_images_loaded();
861
                    });
862

    
863
                    img.attr({'src': stats[k]});
864
                })
865
                data.stats = stats;
866
            }
867

    
868
            // do we need to change the interval ??
869
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
870
                this.stats_update_interval = data.stats.refresh * 1000;
871
                this.stats_fetcher.timeout = this.stats_update_interval;
872
                this.stats_fetcher.stop();
873
                this.stats_fetcher.start(false);
874
            }
875
        },
876

    
877
        // helper method that sets the do_update_stats
878
        // in the future this method could also make an api call
879
        // immediaetly if needed
880
        enable_stats_update: function() {
881
            this.do_update_stats = true;
882
        },
883
        
884
        handle_destroy: function() {
885
            this.stats_fetcher.stop();
886
        },
887

    
888
        require_reboot: function() {
889
            if (this.is_active()) {
890
                this.set({'reboot_required': true});
891
            }
892
        },
893
        
894
        set_pending_action: function(data) {
895
            this.pending_action = data;
896
            return data;
897
        },
898

    
899
        // machine has pending action
900
        update_pending_action: function(action, force) {
901
            this.set({pending_action: action});
902
        },
903

    
904
        clear_pending_action: function() {
905
            this.set({pending_action: undefined});
906
        },
907

    
908
        has_pending_action: function() {
909
            return this.get("pending_action") ? this.get("pending_action") : false;
910
        },
911
        
912
        // machine is active
913
        is_active: function() {
914
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
915
        },
916
        
917
        // machine is building 
918
        is_building: function() {
919
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
920
        },
921
        
922
        in_error_state: function() {
923
            return this.state() === "ERROR"
924
        },
925

    
926
        // user can connect to machine
927
        is_connectable: function() {
928
            // check if ips exist
929
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
930
                return false;
931
            }
932
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
933
        },
934
        
935
        set_firewalls: function(data) {
936
            _.each(data, _.bind(function(val, key){
937
                if (this.pending_firewalls && this.pending_firewalls[key] && this.pending_firewalls[key] == val) {
938
                        this.require_reboot();
939
                        this.remove_pending_firewall(key, val);
940
                }
941
            }, this));
942
            return data;
943
        },
944

    
945
        remove_pending_firewall: function(net_id, value) {
946
            if (this.pending_firewalls[net_id] == value) {
947
                delete this.pending_firewalls[net_id];
948
                storage.networks.get(net_id).update_state();
949
            }
950
        },
951
            
952
        remove_meta: function(key, complete, error) {
953
            var url = this.api_path() + "/meta/" + key;
954
            this.api.call(url, "delete", undefined, complete, error);
955
        },
956

    
957
        save_meta: function(meta, complete, error) {
958
            var url = this.api_path() + "/meta/" + meta.key;
959
            var payload = {meta:{}};
960
            payload.meta[meta.key] = meta.value;
961
            payload._options = {
962
                critical:false, 
963
                error_params: {
964
                    title: "Machine metadata error",
965
                    extra_details: {"Machine id": this.id}
966
            }};
967

    
968
            this.api.call(url, "update", payload, complete, error);
969
        },
970

    
971
        set_firewall: function(net_id, value, callback, error, options) {
972
            if (this.get("firewalls") && this.get("firewalls")[net_id] == value) { return }
973

    
974
            this.pending_firewalls[net_id] = value;
975
            this.trigger("change", this, this);
976
            var payload = {"firewallProfile":{"profile":value}};
977
            payload._options = _.extend({critical: false}, options);
978
            
979
            // reset firewall state on error
980
            var error_cb = _.bind(function() {
981
                thi
982
            }, this);
983

    
984
            this.api.call(this.api_path() + "/action", "create", payload, callback, error);
985
            storage.networks.get(net_id).update_state();
986
        },
987

    
988
        firewall_pending: function(net_id) {
989
            return this.pending_firewalls[net_id] != undefined;
990
        },
991
        
992
        // update/get the state of the machine
993
        state: function() {
994
            var args = slice.call(arguments);
995
                
996
            // TODO: it might not be a good idea to set the state in set_state method
997
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
998
                this.set({'state': args[0]});
999
            }
1000

    
1001
            return this.get('state');
1002
        },
1003
        
1004
        // get the state that the api status corresponds to
1005
        state_for_api_status: function(status) {
1006
            return this.state_transition(this.state(), status);
1007
        },
1008
        
1009
        // vm state equals vm api status
1010
        state_is_status: function(state) {
1011
            return models.VM.STATUSES.indexOf(state) != -1;
1012
        },
1013
        
1014
        // get transition state for the corresponging api status
1015
        state_transition: function(state, new_status) {
1016
            var statuses = models.VM.STATES_TRANSITIONS[state];
1017
            if (statuses) {
1018
                if (statuses.indexOf(new_status) > -1) {
1019
                    return new_status;
1020
                } else {
1021
                    return state;
1022
                }
1023
            } else {
1024
                return new_status;
1025
            }
1026
        },
1027
        
1028
        // the current vm state is a transition state
1029
        in_transition: function() {
1030
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
1031
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
1032
        },
1033
        
1034
        // get image object
1035
        // TODO: update images synchronously if image not found
1036
        get_image: function() {
1037
            var image = storage.images.get(this.get('imageRef'));
1038
            if (!image) {
1039
                storage.images.update_unknown_id(this.get('imageRef'));
1040
                image = storage.images.get(this.get('imageRef'));
1041
            }
1042
            return image;
1043
        },
1044
        
1045
        // get flavor object
1046
        get_flavor: function() {
1047
            var flv = storage.flavors.get(this.get('flavorRef'));
1048
            if (!flv) {
1049
                storage.flavors.update_unknown_id(this.get('flavorRef'));
1050
                flv = storage.flavors.get(this.get('flavorRef'));
1051
            }
1052
            return flv;
1053
        },
1054

    
1055
        // retrieve the metadata object
1056
        get_meta: function() {
1057
            try {
1058
                return this.get('metadata').values
1059
            } catch (err) {
1060
                return {};
1061
            }
1062
        },
1063
        
1064
        // get metadata OS value
1065
        get_os: function() {
1066
            return this.get_meta().OS || (this.get_image() ? this.get_image().get_os() || "okeanos" : "okeanos");
1067
        },
1068
        
1069
        // get public ip addresses
1070
        // TODO: public network is always the 0 index ???
1071
        get_addresses: function(net_id) {
1072
            var net_id = net_id || "public";
1073
            
1074
            var info = this.get_network_info(net_id);
1075
            if (!info) { return {} };
1076
            addrs = {};
1077
            _.each(info.values, function(addr) {
1078
                addrs["ip" + addr.version] = addr.addr;
1079
            });
1080
            return addrs
1081
        },
1082

    
1083
        get_network_info: function(net_id) {
1084
            var net_id = net_id || "public";
1085
            
1086
            if (!this.networks.network_ids.length) { return {} };
1087

    
1088
            var addresses = this.networks.get();
1089
            try {
1090
                return _.select(addresses, function(net, key){return key == net_id })[0];
1091
            } catch (err) {
1092
                //this.log.debug("Cannot find network {0}".format(net_id))
1093
            }
1094
        },
1095

    
1096
        firewall_profile: function(net_id) {
1097
            var net_id = net_id || "public";
1098
            var firewalls = this.get("firewalls");
1099
            return firewalls[net_id];
1100
        },
1101

    
1102
        has_firewall: function(net_id) {
1103
            var net_id = net_id || "public";
1104
            return ["ENABLED","PROTECTED"].indexOf(this.firewall_profile()) > -1;
1105
        },
1106
    
1107
        // get actions that the user can execute
1108
        // depending on the vm state/status
1109
        get_available_actions: function() {
1110
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1111
        },
1112

    
1113
        set_profile: function(profile, net_id) {
1114
        },
1115
        
1116
        // call rename api
1117
        rename: function(new_name) {
1118
            //this.set({'name': new_name});
1119
            this.sync("update", this, {
1120
                critical: true,
1121
                data: {
1122
                    'server': {
1123
                        'name': new_name
1124
                    }
1125
                }, 
1126
                // do the rename after the method succeeds
1127
                success: _.bind(function(){
1128
                    //this.set({name: new_name});
1129
                    snf.api.trigger("call");
1130
                }, this)
1131
            });
1132
        },
1133
        
1134
        get_console_url: function(data) {
1135
            var url_params = {
1136
                machine: this.get("name"),
1137
                host_ip: this.get_addresses().ip4,
1138
                host_ip_v6: this.get_addresses().ip6,
1139
                host: data.host,
1140
                port: data.port,
1141
                password: data.password
1142
            }
1143
            return '/machines/console?' + $.param(url_params);
1144
        },
1145

    
1146
        // action helper
1147
        call: function(action_name, success, error) {
1148
            var id_param = [this.id];
1149

    
1150
            success = success || function() {};
1151
            error = error || function() {};
1152

    
1153
            var self = this;
1154

    
1155
            switch(action_name) {
1156
                case 'start':
1157
                    this.__make_api_call(this.get_action_url(), // vm actions url
1158
                                         "create", // create so that sync later uses POST to make the call
1159
                                         {start:{}}, // payload
1160
                                         function() {
1161
                                             // set state after successful call
1162
                                             self.state("START"); 
1163
                                             success.apply(this, arguments);
1164
                                             snf.api.trigger("call");
1165
                                         },  
1166
                                         error, 'start');
1167
                    break;
1168
                case 'reboot':
1169
                    this.__make_api_call(this.get_action_url(), // vm actions url
1170
                                         "create", // create so that sync later uses POST to make the call
1171
                                         {reboot:{type:"HARD"}}, // payload
1172
                                         function() {
1173
                                             // set state after successful call
1174
                                             self.state("REBOOT"); 
1175
                                             success.apply(this, arguments)
1176
                                             snf.api.trigger("call");
1177
                                             self.set({'reboot_required': false});
1178
                                         },
1179
                                         error, 'reboot');
1180
                    break;
1181
                case 'shutdown':
1182
                    this.__make_api_call(this.get_action_url(), // vm actions url
1183
                                         "create", // create so that sync later uses POST to make the call
1184
                                         {shutdown:{}}, // payload
1185
                                         function() {
1186
                                             // set state after successful call
1187
                                             self.state("SHUTDOWN"); 
1188
                                             success.apply(this, arguments)
1189
                                             snf.api.trigger("call");
1190
                                         },  
1191
                                         error, 'shutdown');
1192
                    break;
1193
                case 'console':
1194
                    this.__make_api_call(this.url() + "/action", "create", {'console': {'type':'vnc'}}, function(data) {
1195
                        var cons_data = data.console;
1196
                        success.apply(this, [cons_data]);
1197
                    }, undefined, 'console')
1198
                    break;
1199
                case 'destroy':
1200
                    this.__make_api_call(this.url(), // vm actions url
1201
                                         "delete", // create so that sync later uses POST to make the call
1202
                                         undefined, // payload
1203
                                         function() {
1204
                                             // set state after successful call
1205
                                             self.state('DESTROY');
1206
                                             success.apply(this, arguments)
1207
                                         },  
1208
                                         error, 'destroy');
1209
                    break;
1210
                default:
1211
                    throw "Invalid VM action ("+action_name+")";
1212
            }
1213
        },
1214
        
1215
        __make_api_call: function(url, method, data, success, error, action) {
1216
            var self = this;
1217
            error = error || function(){};
1218
            success = success || function(){};
1219

    
1220
            var params = {
1221
                url: url,
1222
                data: data,
1223
                success: function(){ self.handle_action_succeed.apply(self, arguments); success.apply(this, arguments)},
1224
                error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)},
1225
                error_params: { ns: "Machines actions", 
1226
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1227
                                extra_details: { 'Machine ID': this.id, 'URL': url, 'Action': action || "undefined" },
1228
                                allow_reload: false
1229
                              },
1230
                display: false,
1231
                critical: false
1232
            }
1233
            this.sync(method, this, params);
1234
        },
1235

    
1236
        handle_action_succeed: function() {
1237
            this.trigger("action:success", arguments);
1238
        },
1239
        
1240
        reset_action_error: function() {
1241
            this.action_error = false;
1242
            this.trigger("action:fail:reset", this.action_error);
1243
        },
1244

    
1245
        handle_action_fail: function() {
1246
            this.action_error = arguments;
1247
            this.trigger("action:fail", arguments);
1248
        },
1249

    
1250
        get_action_url: function(name) {
1251
            return this.url() + "/action";
1252
        },
1253

    
1254
        get_connection_info: function(host_os, success, error) {
1255
            var url = "/machines/connect";
1256
            params = {
1257
                ip_address: this.get_addresses().ip4,
1258
                os: this.get_os(),
1259
                host_os: host_os,
1260
                srv: this.id
1261
            }
1262

    
1263
            url = url + "?" + $.param(params);
1264

    
1265
            var ajax = snf.api.sync("read", undefined, { url: url, 
1266
                                                         error:error, 
1267
                                                         success:success, 
1268
                                                         handles_error:1});
1269
        }
1270
    })
1271
    
1272
    models.VM.ACTIONS = [
1273
        'start',
1274
        'shutdown',
1275
        'reboot',
1276
        'console',
1277
        'destroy'
1278
    ]
1279

    
1280
    models.VM.AVAILABLE_ACTIONS = {
1281
        'UNKNWON'       : ['destroy'],
1282
        'BUILD'         : ['destroy'],
1283
        'REBOOT'        : ['shutdown', 'destroy', 'console'],
1284
        'STOPPED'       : ['start', 'destroy'],
1285
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1286
        'ERROR'         : ['destroy'],
1287
        'DELETED'        : [],
1288
        'DESTROY'       : [],
1289
        'BUILD_INIT'    : ['destroy'],
1290
        'BUILD_COPY'    : ['destroy'],
1291
        'BUILD_FINAL'   : ['destroy'],
1292
        'SHUTDOWN'      : ['destroy'],
1293
        'START'         : [],
1294
        'CONNECT'       : [],
1295
        'DISCONNECT'    : []
1296
    }
1297

    
1298
    // api status values
1299
    models.VM.STATUSES = [
1300
        'UNKNWON',
1301
        'BUILD',
1302
        'REBOOT',
1303
        'STOPPED',
1304
        'ACTIVE',
1305
        'ERROR',
1306
        'DELETED'
1307
    ]
1308

    
1309
    // api status values
1310
    models.VM.CONNECT_STATES = [
1311
        'ACTIVE',
1312
        'REBOOT',
1313
        'SHUTDOWN'
1314
    ]
1315

    
1316
    // vm states
1317
    models.VM.STATES = models.VM.STATUSES.concat([
1318
        'DESTROY',
1319
        'BUILD_INIT',
1320
        'BUILD_COPY',
1321
        'BUILD_FINAL',
1322
        'SHUTDOWN',
1323
        'START',
1324
        'CONNECT',
1325
        'DISCONNECT',
1326
        'FIREWALL'
1327
    ]);
1328
    
1329
    models.VM.STATES_TRANSITIONS = {
1330
        'DESTROY' : ['DELETED'],
1331
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1332
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1333
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1334
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1335
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1336
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1337
        'BUILD_COPY': ['ERROR', 'ACTIVE', 'BUILD_FINAL', 'DESTROY'],
1338
        'BUILD_FINAL': ['ERROR', 'ACTIVE', 'DESTROY'],
1339
        'BUILD_INIT': ['ERROR', 'ACTIVE', 'BUILD_COPY', 'BUILD_FINAL', 'DESTROY']
1340
    }
1341

    
1342
    models.VM.TRANSITION_STATES = [
1343
        'DESTROY',
1344
        'SHUTDOWN',
1345
        'START',
1346
        'REBOOT',
1347
        'BUILD'
1348
    ]
1349

    
1350
    models.VM.ACTIVE_STATES = [
1351
        'BUILD', 'REBOOT', 'ACTIVE',
1352
        'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL',
1353
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1354
    ]
1355

    
1356
    models.VM.BUILDING_STATES = [
1357
        'BUILD', 'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL'
1358
    ]
1359

    
1360
    models.Networks = models.Collection.extend({
1361
        model: models.Network,
1362
        path: 'networks',
1363
        details: true,
1364
        //noUpdate: true,
1365
        defaults: {'linked_to':[]},
1366

    
1367
        parse: function (resp, xhr) {
1368
            // FIXME: depricated global var
1369
            if (!resp) { return []};
1370
               
1371
            var data = _.map(resp.networks.values, _.bind(this.parse_net_api_data, this));
1372
            return data;
1373
        },
1374

    
1375
        reset_pending_actions: function() {
1376
            this.each(function(net) {
1377
                net.get("actions").reset();
1378
            })
1379
        },
1380

    
1381
        do_all_pending_actions: function() {
1382
            this.each(function(net) {
1383
                net.do_all_pending_actions();
1384
            })
1385
        },
1386

    
1387
        parse_net_api_data: function(data) {
1388
            if (data.servers && data.servers.values) {
1389
                data['linked_to'] = data.servers.values;
1390
            }
1391
            return data;
1392
        },
1393

    
1394
        create: function (name, callback) {
1395
            return this.api.call(this.path, "create", {network:{name:name}}, callback);
1396
        }
1397
    })
1398

    
1399
    models.Images = models.Collection.extend({
1400
        model: models.Image,
1401
        path: 'images',
1402
        details: true,
1403
        noUpdate: true,
1404
        supportIncUpdates: false,
1405
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1406

    
1407
        // update collection model with id passed
1408
        // making a direct call to the image
1409
        // api url
1410
        update_unknown_id: function(id) {
1411
            var url = getUrl.call(this) + "/" + id;
1412
            this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, 
1413
            _.bind(function() {
1414
                this.add({id:id, name:"Unknown image", size:-1, progress:100, status:"DELETED"})
1415
            }, this), _.bind(function(image) {
1416
                this.add(image.image);
1417
            }, this));
1418
        },
1419

    
1420
        parse: function (resp, xhr) {
1421
            // FIXME: depricated global var
1422
            var data = _.map(resp.images.values, _.bind(this.parse_meta, this));
1423
            return resp.images.values;
1424
        },
1425

    
1426
        get_meta_key: function(img, key) {
1427
            if (img.metadata && img.metadata.values && img.metadata.values[key]) {
1428
                return img.metadata.values[key];
1429
            }
1430
            return undefined;
1431
        },
1432

    
1433
        comparator: function(img) {
1434
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1435
        },
1436

    
1437
        parse_meta: function(img) {
1438
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1439
                img[key] = this.get_meta_key(img, key) || "";
1440
            }, this));
1441
            return img;
1442
        },
1443

    
1444
        active: function() {
1445
            return this.filter(function(img){return img.get('status') != "DELETED"});
1446
        }
1447
    })
1448

    
1449
    models.Flavors = models.Collection.extend({
1450
        model: models.Flavor,
1451
        path: 'flavors',
1452
        details: true,
1453
        noUpdate: true,
1454
        supportIncUpdates: false,
1455
        // update collection model with id passed
1456
        // making a direct call to the flavor
1457
        // api url
1458
        update_unknown_id: function(id) {
1459
            var url = getUrl.call(this) + "/" + id;
1460
            this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, 
1461
            _.bind(function() {
1462
                this.add({id:id, cpu:"", ram:"", disk:"", name: "", status:"DELETED"})
1463
            }, this), _.bind(function(flv) {
1464
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1465
                this.add(flv.flavor);
1466
            }, this));
1467
        },
1468

    
1469
        parse: function (resp, xhr) {
1470
            // FIXME: depricated global var
1471
            return resp.flavors.values;
1472
        },
1473

    
1474
        comparator: function(flv) {
1475
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1476
        },
1477

    
1478
        unavailable_values_for_image: function(img, flavors) {
1479
            var flavors = flavors || this.active();
1480
            var size = img.get_size();
1481
            
1482
            var index = {cpu:[], disk:[], ram:[]};
1483

    
1484
            _.each(this.active(), function(el) {
1485
                var img_size = size;
1486
                var flv_size = el.get_disk_size();
1487
                if (flv_size < img_size) {
1488
                    if (index.disk.indexOf(flv_size) == -1) {
1489
                        index.disk.push(flv_size);
1490
                    }
1491
                };
1492
            });
1493
            
1494
            return index;
1495
        },
1496

    
1497
        get_flavor: function(cpu, mem, disk, filter_list) {
1498
            if (!filter_list) { filter_list = this.models };
1499

    
1500
            return this.select(function(flv){
1501
                if (flv.get("cpu") == cpu + "" &&
1502
                   flv.get("ram") == mem + "" &&
1503
                   flv.get("disk") == disk + "" &&
1504
                   filter_list.indexOf(flv) > -1) { return true; }
1505
            })[0];
1506
        },
1507
        
1508
        get_data: function(lst) {
1509
            var data = {'cpu': [], 'mem':[], 'disk':[]};
1510

    
1511
            _.each(lst, function(flv) {
1512
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1513
                    data.cpu.push(flv.get("cpu"));
1514
                }
1515
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1516
                    data.mem.push(flv.get("ram"));
1517
                }
1518
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1519
                    data.disk.push(flv.get("disk"));
1520
                }
1521
            })
1522
            
1523
            return data;
1524
        },
1525

    
1526
        active: function() {
1527
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1528
        }
1529
            
1530
    })
1531

    
1532
    models.VMS = models.Collection.extend({
1533
        model: models.VM,
1534
        path: 'servers',
1535
        details: true,
1536
        copy_image_meta: true,
1537
        
1538
        parse: function (resp, xhr) {
1539
            // FIXME: depricated after refactoring
1540
            var data = resp;
1541
            if (!resp) { return [] };
1542
            data = _.filter(_.map(resp.servers.values, _.bind(this.parse_vm_api_data, this)), function(v){return v});
1543
            return data;
1544
        },
1545
        
1546
        get_reboot_required: function() {
1547
            return this.filter(function(vm){return vm.get("reboot_required") == true})
1548
        },
1549

    
1550
        has_pending_actions: function() {
1551
            return this.filter(function(vm){return vm.pending_action}).length > 0;
1552
        },
1553

    
1554
        reset_pending_actions: function() {
1555
            this.each(function(vm) {
1556
                vm.clear_pending_action();
1557
            })
1558
        },
1559

    
1560
        do_all_pending_actions: function(success, error) {
1561
            this.each(function(vm) {
1562
                if (vm.has_pending_action()) {
1563
                    vm.call(vm.pending_action, success, error);
1564
                    vm.clear_pending_action();
1565
                }
1566
            })
1567
        },
1568
        
1569
        do_all_reboots: function(success, error) {
1570
            this.each(function(vm) {
1571
                if (vm.get("reboot_required")) {
1572
                    vm.call("reboot", success, error);
1573
                }
1574
            });
1575
        },
1576

    
1577
        reset_reboot_required: function() {
1578
            this.each(function(vm) {
1579
                vm.set({'reboot_required': undefined});
1580
            })
1581
        },
1582
        
1583
        stop_stats_update: function(exclude) {
1584
            var exclude = exclude || [];
1585
            this.each(function(vm) {
1586
                if (exclude.indexOf(vm) > -1) {
1587
                    return;
1588
                }
1589
                vm.stop_stats_update();
1590
            })
1591
        },
1592
        
1593
        has_meta: function(vm_data) {
1594
            return vm_data.metadata && vm_data.metadata.values
1595
        },
1596

    
1597
        has_addresses: function(vm_data) {
1598
            return vm_data.metadata && vm_data.metadata.values
1599
        },
1600

    
1601
        parse_vm_api_data: function(data) {
1602
            // do not add non existing DELETED entries
1603
            if (data.status && data.status == "DELETED") {
1604
                if (!this.get(data.id)) {
1605
                    console.error("non exising deleted vm", data)
1606
                    return false;
1607
                }
1608
            }
1609

    
1610
            // OS attribute
1611
            if (this.has_meta(data)) {
1612
                data['OS'] = data.metadata.values.OS || "okeanos";
1613
            }
1614
            
1615
            data['firewalls'] = {};
1616
            if (data['addresses'] && data['addresses'].values) {
1617
                data['linked_to_nets'] = data['addresses'].values;
1618
                _.each(data['addresses'].values, function(f){
1619
                    if (f['firewallProfile']) {
1620
                        data['firewalls'][f['id']] = f['firewallProfile']
1621
                    }
1622
                });
1623
            }
1624
            
1625
            // if vm has no metadata, no metadata object
1626
            // is in json response, reset it to force
1627
            // value update
1628
            if (!data['metadata']) {
1629
                data['metadata'] = {values:{}};
1630
            }
1631

    
1632
            return data;
1633
        },
1634

    
1635
        create: function (name, image, flavor, meta, extra, callback) {
1636
            if (this.copy_image_meta) {
1637
                meta['OS'] = image.get("OS");
1638
           }
1639
            
1640
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta}
1641
            opts = _.extend(opts, extra);
1642

    
1643
            this.api.call(this.path, "create", {'server': opts}, undefined, undefined, callback, {critical: false});
1644
        }
1645

    
1646
    })
1647
    
1648

    
1649
    // storage initialization
1650
    snf.storage.images = new models.Images();
1651
    snf.storage.flavors = new models.Flavors();
1652
    snf.storage.networks = new models.Networks();
1653
    snf.storage.vms = new models.VMS();
1654

    
1655
    //snf.storage.vms.fetch({update:true});
1656
    //snf.storage.images.fetch({update:true});
1657
    //snf.storage.flavors.fetch({update:true});
1658

    
1659
})(this);