Statistics
| Branch: | Tag: | Revision:

root / ui / static / snf / js / models.js @ 859a4609

History | View | Annotate | Download (60 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(baseurl) {
22
        var baseurl = baseurl || snf.config.api_url;
23
        return baseurl + "/" + this.path;
24
    }
25
    
26
    // i18n
27
    BUILDING_MESSAGES = window.BUILDING_MESSAGES || {'INIT': 'init', 'COPY': '{0}, {1}, {2}', 'FINAL': 'final'};
28

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

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

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

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

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

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

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

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

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

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

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

    
122
        create: function(model, options) {
123
            var coll = this;
124
            options || (options = {});
125
            model = this._prepareModel(model, options);
126
            if (!model) return false;
127
            var success = options.success;
128
            options.success = function(nextModel, resp, xhr) {
129
                if (success) success(nextModel, resp, xhr);
130
            };
131
            model.save(null, options);
132
            return model;
133
        },
134

    
135
        get_fetcher: function(timeout, fast, limit, initial, params) {
136
            var fetch_params = params || {};
137
            fetch_params.skips_timeouts = true;
138

    
139
            var timeout = parseInt(timeout);
140
            var fast = fast || 1000;
141
            var limit = limit;
142
            var initial_call = initial || true;
143
            
144
            var last_ajax = undefined;
145
            var cb = _.bind(function() {
146
                // clone to avoid referenced objects
147
                var params = _.clone(fetch_params);
148
                updater._ajax = last_ajax;
149
                if (last_ajax) {
150
                    last_ajax.abort();
151
                }
152
                last_ajax = this.fetch(params);
153
            }, this);
154
            var updater = new snf.api.updateHandler({'callback': cb, timeout:timeout, 
155
                                                    fast:fast, limit:limit, 
156
                                                    call_on_start:initial_call});
157

    
158
            snf.api.bind("call", _.throttle(_.bind(function(){ updater.faster(true)}, this)), 1000);
159
            return updater;
160
        }
161
    });
162
    
163
    // Image model
164
    models.Image = models.Model.extend({
165
        path: 'images',
166

    
167
        get_size: function() {
168
            return parseInt(this.get('metadata') ? this.get('metadata').values.size : -1)
169
        },
170

    
171
        get_readable_size: function() {
172
            return this.get_size() > 0 ? util.readablizeBytes(this.get_size() * 1024 * 1024) : "unknown";
173
        },
174

    
175
        get_os: function() {
176
            return this.get("OS");
177
        },
178

    
179
        get_sort_order: function() {
180
            return parseInt(this.get('metadata') ? this.get('metadata').values.sortorder : -1)
181
        }
182
    });
183

    
184
    // Flavor model
185
    models.Flavor = models.Model.extend({
186
        path: 'flavors',
187

    
188
        details_string: function() {
189
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
190
        },
191

    
192
        get_disk_size: function() {
193
            return parseInt(this.get("disk") * 1000)
194
        }
195

    
196
    });
197
    
198
    //network vms list helper
199
    var NetworkVMSList = function() {
200
        this.initialize = function() {
201
            this.vms = [];
202
            this.pending = [];
203
            this.pending_for_removal = [];
204
        }
205
        
206
        this.add_pending_for_remove = function(vm_id) {
207
            if (this.pending_for_removal.indexOf(vm_id) == -1) {
208
                this.pending_for_removal.push(vm_id);
209
            }
210

    
211
            if (this.pending_for_removal.length) {
212
                this.trigger("pending:remove:add");
213
            }
214
        },
215

    
216
        this.add_pending = function(vm_id) {
217
            if (this.pending.indexOf(vm_id) == -1) {
218
                this.pending[this.pending.length] = vm_id;
219
            }
220

    
221
            if (this.pending.length) {
222
                this.trigger("pending:add");
223
            }
224
        }
225

    
226
        this.check_pending = function() {
227
            var len = this.pending.length;
228
            var args = [this.pending];
229
            this.pending = _.difference(this.pending, this.vms);
230
            if (len != this.pending.length) {
231
                if (this.pending.length == 0) {
232
                    this.trigger("pending:clear");
233
                }
234
            }
235

    
236
            var len = this.pending_for_removal.length;
237
            this.pending_for_removal = _.intersection(this.pending_for_removal, this.vms);
238
            if (this.pending_for_removal.length == 0) {
239
                this.trigger("pending:remove:clear");
240
            }
241

    
242
        }
243

    
244

    
245
        this.add = function(vm_id) {
246
            if (this.vms.indexOf(vm_id) == -1) {
247
                this.vms[this.vms.length] = vm_id;
248
                this.trigger("network:connect", vm_id);
249
                this.check_pending();
250
                return true;
251
            }
252
        }
253

    
254
        this.remove = function(vm_id) {
255
            if (this.vms.indexOf(vm_id) > -1) {
256
                this.vms = _.without(this.vms, vm_id);
257
                this.trigger("network:disconnect", vm_id);
258
                this.check_pending();
259
                return true;
260
            }
261
        }
262

    
263
        this.get = function() {
264
            return this.vms;
265
        }
266

    
267
        this.list = function() {
268
            return storage.vms.filter(_.bind(function(vm){
269
                return this.vms.indexOf(vm.id) > -1;
270
            }, this))
271
        }
272

    
273
        this.initialize();
274
    };
275
    _.extend(NetworkVMSList.prototype, bb.Events);
276
    
277
    // vm networks list helper
278
    var VMNetworksList = function() {
279
        this.initialize = function() {
280
            this.networks = {};
281
            this.network_ids = [];
282
        }
283

    
284
        this.add = function(net_id, data) {
285
            if (!this.networks[net_id]) {
286
                this.networks[net_id] = data || {};
287
                this.network_ids[this.network_ids.length] = net_id;
288
                this.trigger("network:connect", net_id);
289
                return true;
290
            }
291
        }
292

    
293
        this.remove = function(net_id) {
294
            if (this.networks[net_id]) {
295
                delete this.networks[net_id];
296
                this.network_ids = _.without(this.network_ids, net_id);
297
                this.trigger("network:disconnect", net_id);
298
                return true;
299
            }
300
            return false;
301
        }
302

    
303
        this.get = function() {
304
            return this.networks;
305
        }
306

    
307
        this.list = function() {
308
            return storage.networks.filter(_.bind(function(net){
309
                return this.network_ids.indexOf(net.id) > -1;
310
            }, this))
311
        }
312

    
313
        this.initialize();
314
    };
315
    _.extend(VMNetworksList.prototype, bb.Events);
316
        
317
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
318
    _.extend(models.ParamsList.prototype, bb.Events, {
319

    
320
        initialize: function(parent, param_name) {
321
            this.parent = parent;
322
            this.actions = {};
323
            this.param_name = param_name;
324
            this.length = 0;
325
        },
326
        
327
        has_action: function(action) {
328
            return this.actions[action] ? true : false;
329
        },
330
            
331
        _parse_params: function(arguments) {
332
            if (arguments.length <= 1) {
333
                return [];
334
            }
335

    
336
            var args = _.toArray(arguments);
337
            return args.splice(1);
338
        },
339

    
340
        contains: function(action, params) {
341
            params = this._parse_params(arguments);
342
            var has_action = this.has_action(action);
343
            if (!has_action) { return false };
344

    
345
            var paramsEqual = false;
346
            _.each(this.actions[action], function(action_params) {
347
                if (_.isEqual(action_params, params)) {
348
                    paramsEqual = true;
349
                }
350
            });
351
                
352
            return paramsEqual;
353
        },
354
        
355
        is_empty: function() {
356
            return _.isEmpty(this.actions);
357
        },
358

    
359
        add: function(action, params) {
360
            params = this._parse_params(arguments);
361
            if (this.contains.apply(this, arguments)) { return this };
362
            var isnew = false
363
            if (!this.has_action(action)) {
364
                this.actions[action] = [];
365
                isnew = true;
366
            };
367

    
368
            this.actions[action].push(params);
369
            this.parent.trigger("change:" + this.param_name, this.parent, this);
370
            if (isnew) {
371
                this.trigger("add", action, params);
372
            } else {
373
                this.trigger("change", action, params);
374
            }
375
            return this;
376
        },
377
        
378
        remove_all: function(action) {
379
            if (this.has_action(action)) {
380
                delete this.actions[action];
381
                this.parent.trigger("change:" + this.param_name, this.parent, this);
382
                this.trigger("remove", action);
383
            }
384
            return this;
385
        },
386

    
387
        reset: function() {
388
            this.actions = {};
389
            this.parent.trigger("change:" + this.param_name, this.parent, this);
390
            this.trigger("reset");
391
            this.trigger("remove");
392
        },
393

    
394
        remove: function(action, params) {
395
            params = this._parse_params(arguments);
396
            if (!this.has_action(action)) { return this };
397
            var index = -1;
398
            _.each(this.actions[action], _.bind(function(action_params) {
399
                if (_.isEqual(action_params, params)) {
400
                    index = this.actions[action].indexOf(action_params);
401
                }
402
            }, this));
403
            
404
            if (index > -1) {
405
                this.actions[action].splice(index, 1);
406
                if (_.isEmpty(this.actions[action])) {
407
                    delete this.actions[action];
408
                }
409
                this.parent.trigger("change:" + this.param_name, this.parent, this);
410
                this.trigger("remove", action, params);
411
            }
412
        }
413

    
414
    });
415

    
416
    // Image model
417
    models.Network = models.Model.extend({
418
        path: 'networks',
419
        has_status: true,
420
        
421
        initialize: function() {
422
            this.vms = new NetworkVMSList();
423
            this.vms.bind("pending:add", _.bind(this.handle_pending_connections, this, "add"));
424
            this.vms.bind("pending:clear", _.bind(this.handle_pending_connections, this, "clear"));
425
            this.vms.bind("pending:remove:add", _.bind(this.handle_pending_connections, this, "add"));
426
            this.vms.bind("pending:remove:clear", _.bind(this.handle_pending_connections, this, "clear"));
427

    
428
            var ret = models.Network.__super__.initialize.apply(this, arguments);
429

    
430
            storage.vms.bind("change:linked_to_nets", _.bind(this.update_connections, this, "vm:change"));
431
            storage.vms.bind("add", _.bind(this.update_connections, this, "add"));
432
            storage.vms.bind("remove", _.bind(this.update_connections, this, "remove"));
433
            storage.vms.bind("reset", _.bind(this.update_connections, this, "reset"));
434

    
435
            this.bind("change:linked_to", _.bind(this.update_connections, this, "net:change"));
436
            this.update_connections();
437
            this.update_state();
438
            
439
            this.set({"actions": new models.ParamsList(this, "actions")});
440

    
441
            return ret;
442
        },
443

    
444
        update_state: function() {
445
            if (this.vms.pending.length) {
446
                this.set({state: "CONNECTING"});
447
                return
448
            }
449

    
450
            if (this.vms.pending_for_removal.length) {
451
                this.set({state: "DISCONNECTING"});
452
                return
453
            }   
454
            
455
            var firewalling = false;
456
            _.each(this.vms.get(), _.bind(function(vm_id){
457
                var vm = storage.vms.get(vm_id);
458
                if (!vm) { return };
459
                if (!_.isEmpty(vm.pending_firewalls)) {
460
                    this.set({state:"FIREWALLING"});
461
                    firewalling = true;
462
                    return false;
463
                }
464
            },this));
465
            if (firewalling) { return };
466

    
467
            this.set({state:"NORMAL"});
468
        },
469

    
470
        handle_pending_connections: function(action) {
471
            this.update_state();
472
        },
473

    
474
        // handle vm/network connections
475
        update_connections: function(action, model) {
476
            
477
            // vm removed disconnect vm from network
478
            if (action == "remove") {
479
                var removed_from_net = this.vms.remove(model.id);
480
                var removed_from_vm = model.networks.remove(this.id);
481
                if (removed_from_net) {this.trigger("vm:disconnect", model, this); this.change()};
482
                if (removed_from_vm) {model.trigger("network:disconnect", this, model); this.change()};
483
                return;
484
            }
485
            
486
            // update links for all vms
487
            var links = this.get("linked_to");
488
            storage.vms.each(_.bind(function(vm) {
489
                var vm_links = vm.get("linked_to") || [];
490
                if (vm_links.indexOf(this.id) > -1) {
491
                    // vm has connection to current network
492
                    if (links.indexOf(vm.id) > -1) {
493
                        // and network has connection to vm, so try
494
                        // to append it
495
                        var add_to_net = this.vms.add(vm.id);
496
                        var index = _.indexOf(vm_links, this.id);
497
                        var add_to_vm = vm.networks.add(this.id, vm.get("linked_to_nets")[index]);
498
                        
499
                        // call only if connection did not existed
500
                        if (add_to_net) {this.trigger("vm:connect", vm, this); this.change()};
501
                        if (add_to_vm) {vm.trigger("network:connect", this, vm); vm.change()};
502
                    } else {
503
                        // no connection, try to remove it
504
                        var removed_from_net = this.vms.remove(vm.id);
505
                        var removed_from_vm = vm.networks.remove(this.id);
506
                        if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
507
                        if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
508
                    }
509
                } else {
510
                    // vm has no connection to current network, try to remove it
511
                    var removed_from_net = this.vms.remove(vm.id);
512
                    var removed_from_vm = vm.networks.remove(this.id);
513
                    if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
514
                    if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
515
                }
516
            },this));
517
        },
518

    
519
        is_public: function() {
520
            return this.id == "public";
521
        },
522

    
523
        contains_vm: function(vm) {
524
            var net_vm_exists = this.vms.get().indexOf(vm.id) > -1;
525
            var vm_net_exists = vm.is_connected_to(this);
526
            return net_vm_exists && vm_net_exists;
527
        },
528
        
529
        call: function(action, params, success, error) {
530
            if (action == "destroy") {
531
                this.set({state:"DESTROY"});
532
                this.get("actions").remove("destroy");
533
                this.remove(_.bind(function(){
534
                    success();
535
                }, this), error);
536
            }
537
            
538
            if (action == "disconnect") {
539
                _.each(params, _.bind(function(vm_id) {
540
                    var vm = snf.storage.vms.get(vm_id);
541
                    this.get("actions").remove("disconnect", vm_id);
542
                    if (vm) {
543
                        this.remove_vm(vm, success, error);
544
                    }
545
                }, this));
546
            }
547
        },
548

    
549
        add_vm: function (vm, callback, error, options) {
550
            var payload = {add:{serverRef:"" + vm.id}};
551
            payload._options = options || {};
552
            return this.api.call(this.api_path() + "/action", "create", 
553
                                 payload,
554
                                 _.bind(function(){
555
                                     this.vms.add_pending(vm.id);
556
                                     if (callback) {callback()}
557
                                 },this), error);
558
        },
559

    
560
        remove_vm: function (vm, callback, error, options) {
561
            var payload = {remove:{serverRef:"" + vm.id}};
562
            payload._options = options || {};
563
            return this.api.call(this.api_path() + "/action", "create", 
564
                                 {remove:{serverRef:"" + vm.id}},
565
                                 _.bind(function(){
566
                                     this.vms.add_pending_for_remove(vm.id);
567
                                     if (callback) {callback()}
568
                                 },this), error);
569
        },
570

    
571
        rename: function(name, callback) {
572
            return this.api.call(this.api_path(), "update", {
573
                network:{name:name}, 
574
                _options:{
575
                    critical: false, 
576
                    error_params:{
577
                        title: "Network action failed",
578
                        ns: "Networks",
579
                        extra_details: {"Network id": this.id}
580
                    }
581
                }}, callback);
582
        },
583

    
584
        get_connectable_vms: function() {
585
            var servers = this.vms.list();
586
            return storage.vms.filter(function(vm){
587
                return servers.indexOf(vm) == -1 && !vm.in_error_state();
588
            })
589
        },
590

    
591
        state_message: function() {
592
            if (this.get("state") == "NORMAL" && this.is_public()) {
593
                return "Public network";
594
            }
595

    
596
            return models.Network.STATES[this.get("state")];
597
        },
598

    
599
        in_progress: function() {
600
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
601
        },
602

    
603
        do_all_pending_actions: function(success, error) {
604
            var destroy = this.get("actions").has_action("destroy");
605
            _.each(this.get("actions").actions, _.bind(function(params, action) {
606
                _.each(params, _.bind(function(with_params) {
607
                    this.call(action, with_params, success, error);
608
                }, this));
609
            }, this));
610
        }
611
    });
612
    
613
    models.Network.STATES = {
614
        'NORMAL': 'Private network',
615
        'CONNECTING': 'Connecting...',
616
        'DISCONNECTING': 'Disconnecting...',
617
        'FIREWALLING': 'Firewall update...',
618
        'DESTROY': 'Destroying...'
619
    }
620

    
621
    models.Network.STATES_TRANSITIONS = {
622
        'CONNECTING': ['NORMAL'],
623
        'DISCONNECTING': ['NORMAL'],
624
        'FIREWALLING': ['NORMAL']
625
    }
626

    
627
    // Virtualmachine model
628
    models.VM = models.Model.extend({
629

    
630
        path: 'servers',
631
        has_status: true,
632
        initialize: function(params) {
633
            this.networks = new VMNetworksList();
634
            
635
            this.pending_firewalls = {};
636
            
637
            models.VM.__super__.initialize.apply(this, arguments);
638

    
639
            this.set({state: params.status || "ERROR"});
640
            this.log = new snf.logging.logger("VM " + this.id);
641
            this.pending_action = undefined;
642
            
643
            // init stats parameter
644
            this.set({'stats': undefined}, {silent: true});
645
            // defaults to not update the stats
646
            // each view should handle this vm attribute 
647
            // depending on if it displays stat images or not
648
            this.do_update_stats = false;
649
            
650
            // interval time
651
            // this will dynamicaly change if the server responds that
652
            // images get refreshed on different intervals
653
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
654
            this.stats_available = false;
655

    
656
            // initialize interval
657
            this.init_stats_intervals(this.stats_update_interval);
658
            
659
            this.bind("change:progress", _.bind(this.update_building_progress, this));
660
            this.update_building_progress();
661

    
662
            this.bind("change:firewalls", _.bind(this.handle_firewall_change, this));
663
            
664
            // default values
665
            this.set({linked_to_nets:this.get("linked_to_nets") || []});
666
            this.set({firewalls:this.get("firewalls") || []});
667

    
668
            this.bind("change:state", _.bind(function(){if (this.state() == "DESTROY") { this.handle_destroy() }}, this))
669
        },
670

    
671
        handle_firewall_change: function() {
672

    
673
        },
674
        
675
        set_linked_to_nets: function(data) {
676
            this.set({"linked_to":_.map(data, function(n){ return n.id})});
677
            return data;
678
        },
679

    
680
        is_connected_to: function(net) {
681
            return _.filter(this.networks.list(), function(n){return n.id == net.id}).length > 0;
682
        },
683
        
684
        status: function(st) {
685
            if (!st) { return this.get("status")}
686
            return this.set({status:st});
687
        },
688

    
689
        set_status: function(st) {
690
            var new_state = this.state_for_api_status(st);
691
            var transition = false;
692

    
693
            if (this.state() != new_state) {
694
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
695
                    transition = this.state();
696
                }
697
            }
698
            
699
            // call it silently to avoid double change trigger
700
            this.set({'state': this.state_for_api_status(st)}, {silent: true});
701
            
702
            // trigger transition
703
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
704
                this.trigger("transition", {from:transition, to:new_state}) 
705
            };
706
            return st;
707
        },
708

    
709
        update_building_progress: function() {
710
            if (this.is_building()) {
711
                var progress = this.get("progress");
712
                if (progress == 0) {
713
                    this.state("BUILD_INIT");
714
                    this.set({progress_message: BUILDING_MESSAGES['INIT']});
715
                }
716
                if (progress > 0 && progress < 99) {
717
                    this.state("BUILD_COPY");
718
                    var params = this.get_copy_details(true);
719
                    this.set({progress_message: BUILDING_MESSAGES['COPY'].format(params.copy, 
720
                                                                                 params.size, 
721
                                                                                 params.progress)});
722
                }
723
                if (progress == 100) {
724
                    this.state("BUILD_FINAL");
725
                    this.set({progress_message: BUILDING_MESSAGES['FINAL']});
726
                }
727
            } else {
728
            }
729
        },
730

    
731
        get_copy_details: function(human, image) {
732
            var human = human || false;
733
            var image = image || this.get_image();
734

    
735
            var progress = this.get('progress');
736
            var size = image.get_size();
737
            var size_copied = (size * progress / 100).toFixed(2);
738
            
739
            if (human) {
740
                size = util.readablizeBytes(size*1024*1024);
741
                size_copied = util.readablizeBytes(size_copied*1024*1024);
742
            }
743
            return {'progress': progress, 'size': size, 'copy': size_copied};
744
        },
745

    
746
        start_stats_update: function(force_if_empty) {
747
            var prev_state = this.do_update_stats;
748

    
749
            this.do_update_stats = true;
750
            
751
            // fetcher initialized ??
752
            if (!this.stats_fetcher) {
753
                this.init_stats_intervals();
754
            }
755

    
756

    
757
            // fetcher running ???
758
            if (!this.stats_fetcher.running || !prev_state) {
759
                this.stats_fetcher.start();
760
            }
761

    
762
            if (force_if_empty && this.get("stats") == undefined) {
763
                this.update_stats(true);
764
            }
765
        },
766

    
767
        stop_stats_update: function(stop_calls) {
768
            this.do_update_stats = false;
769

    
770
            if (stop_calls) {
771
                this.stats_fetcher.stop();
772
            }
773
        },
774

    
775
        // clear and reinitialize update interval
776
        init_stats_intervals: function (interval) {
777
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
778
            this.stats_fetcher.start();
779
        },
780
        
781
        get_stats_fetcher: function(timeout) {
782
            var cb = _.bind(function(data){
783
                this.update_stats();
784
            }, this);
785
            var fetcher = new snf.api.updateHandler({'callback': cb, timeout:timeout});
786
            return fetcher;
787
        },
788

    
789
        // do the api call
790
        update_stats: function(force) {
791
            // do not update stats if flag not set
792
            if ((!this.do_update_stats && !force) || this.updating_stats) {
793
                return;
794
            }
795

    
796
            // make the api call, execute handle_stats_update on sucess
797
            // TODO: onError handler ???
798
            stats_url = this.url() + "/stats";
799
            this.updating_stats = true;
800
            this.sync("GET", this, {
801
                handles_error:true, 
802
                url: stats_url, 
803
                refresh:true, 
804
                success: _.bind(this.handle_stats_update, this),
805
                error: _.bind(this.handle_stats_error, this),
806
                complete: _.bind(function(){this.updating_stats = false;}, this),
807
                critical: false,
808
                display: false,
809
                log_error: false
810
            });
811
        },
812

    
813
        get_stats_image: function(stat, type) {
814
        },
815
        
816
        _set_stats: function(stats) {
817
            var silent = silent === undefined ? false : silent;
818
            // unavailable stats while building
819
            if (this.get("status") == "BUILD") { 
820
                this.stats_available = false;
821
            } else { this.stats_available = true; }
822

    
823
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
824
            
825
            this.set({stats: stats}, {silent:true});
826
            this.trigger("stats:update", stats);
827
        },
828

    
829
        unbind: function() {
830
            models.VM.__super__.unbind.apply(this, arguments);
831
        },
832

    
833
        handle_stats_error: function() {
834
            stats = {};
835
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
836
                stats[k] = false;
837
            });
838

    
839
            this.set({'stats': stats});
840
        },
841

    
842
        // this method gets executed after a successful vm stats api call
843
        handle_stats_update: function(data) {
844
            var self = this;
845
            // avoid browser caching
846
            
847
            if (data.stats && _.size(data.stats) > 0) {
848
                var ts = $.now();
849
                var stats = data.stats;
850
                var images_loaded = 0;
851
                var images = {};
852

    
853
                function check_images_loaded() {
854
                    images_loaded++;
855

    
856
                    if (images_loaded == 4) {
857
                        self._set_stats(images);
858
                    }
859
                }
860
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
861
                    
862
                    stats[k] = stats[k] + "?_=" + ts;
863
                    
864
                    var stat = k.slice(0,3);
865
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
866
                    var img = $("<img />");
867
                    var val = stats[k];
868
                    
869
                    // load stat image to a temporary dom element
870
                    // update model stats on image load/error events
871
                    img.load(function() {
872
                        images[k] = val;
873
                        check_images_loaded();
874
                    });
875

    
876
                    img.error(function() {
877
                        images[stat + type] = false;
878
                        check_images_loaded();
879
                    });
880

    
881
                    img.attr({'src': stats[k]});
882
                })
883
                data.stats = stats;
884
            }
885

    
886
            // do we need to change the interval ??
887
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
888
                this.stats_update_interval = data.stats.refresh * 1000;
889
                this.stats_fetcher.timeout = this.stats_update_interval;
890
                this.stats_fetcher.stop();
891
                this.stats_fetcher.start(false);
892
            }
893
        },
894

    
895
        // helper method that sets the do_update_stats
896
        // in the future this method could also make an api call
897
        // immediaetly if needed
898
        enable_stats_update: function() {
899
            this.do_update_stats = true;
900
        },
901
        
902
        handle_destroy: function() {
903
            this.stats_fetcher.stop();
904
        },
905

    
906
        require_reboot: function() {
907
            if (this.is_active()) {
908
                this.set({'reboot_required': true});
909
            }
910
        },
911
        
912
        set_pending_action: function(data) {
913
            this.pending_action = data;
914
            return data;
915
        },
916

    
917
        // machine has pending action
918
        update_pending_action: function(action, force) {
919
            this.set({pending_action: action});
920
        },
921

    
922
        clear_pending_action: function() {
923
            this.set({pending_action: undefined});
924
        },
925

    
926
        has_pending_action: function() {
927
            return this.get("pending_action") ? this.get("pending_action") : false;
928
        },
929
        
930
        // machine is active
931
        is_active: function() {
932
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
933
        },
934
        
935
        // machine is building 
936
        is_building: function() {
937
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
938
        },
939
        
940
        in_error_state: function() {
941
            return this.state() === "ERROR"
942
        },
943

    
944
        // user can connect to machine
945
        is_connectable: function() {
946
            // check if ips exist
947
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
948
                return false;
949
            }
950
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
951
        },
952
        
953
        set_firewalls: function(data) {
954
            _.each(data, _.bind(function(val, key){
955
                if (this.pending_firewalls && this.pending_firewalls[key] && this.pending_firewalls[key] == val) {
956
                        this.require_reboot();
957
                        this.remove_pending_firewall(key, val);
958
                }
959
            }, this));
960
            return data;
961
        },
962

    
963
        remove_pending_firewall: function(net_id, value) {
964
            if (this.pending_firewalls[net_id] == value) {
965
                delete this.pending_firewalls[net_id];
966
                storage.networks.get(net_id).update_state();
967
            }
968
        },
969
            
970
        remove_meta: function(key, complete, error) {
971
            var url = this.api_path() + "/meta/" + key;
972
            this.api.call(url, "delete", undefined, complete, error);
973
        },
974

    
975
        save_meta: function(meta, complete, error) {
976
            var url = this.api_path() + "/meta/" + meta.key;
977
            var payload = {meta:{}};
978
            payload.meta[meta.key] = meta.value;
979
            payload._options = {
980
                critical:false, 
981
                error_params: {
982
                    title: "Machine metadata error",
983
                    extra_details: {"Machine id": this.id}
984
            }};
985

    
986
            this.api.call(url, "update", payload, complete, error);
987
        },
988

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

    
992
            this.pending_firewalls[net_id] = value;
993
            this.trigger("change", this, this);
994
            var payload = {"firewallProfile":{"profile":value}};
995
            payload._options = _.extend({critical: false}, options);
996
            
997
            // reset firewall state on error
998
            var error_cb = _.bind(function() {
999
                thi
1000
            }, this);
1001

    
1002
            this.api.call(this.api_path() + "/action", "create", payload, callback, error);
1003
            storage.networks.get(net_id).update_state();
1004
        },
1005

    
1006
        firewall_pending: function(net_id) {
1007
            return this.pending_firewalls[net_id] != undefined;
1008
        },
1009
        
1010
        // update/get the state of the machine
1011
        state: function() {
1012
            var args = slice.call(arguments);
1013
                
1014
            // TODO: it might not be a good idea to set the state in set_state method
1015
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1016
                this.set({'state': args[0]});
1017
            }
1018

    
1019
            return this.get('state');
1020
        },
1021
        
1022
        // get the state that the api status corresponds to
1023
        state_for_api_status: function(status) {
1024
            return this.state_transition(this.state(), status);
1025
        },
1026
        
1027
        // vm state equals vm api status
1028
        state_is_status: function(state) {
1029
            return models.VM.STATUSES.indexOf(state) != -1;
1030
        },
1031
        
1032
        // get transition state for the corresponging api status
1033
        state_transition: function(state, new_status) {
1034
            var statuses = models.VM.STATES_TRANSITIONS[state];
1035
            if (statuses) {
1036
                if (statuses.indexOf(new_status) > -1) {
1037
                    return new_status;
1038
                } else {
1039
                    return state;
1040
                }
1041
            } else {
1042
                return new_status;
1043
            }
1044
        },
1045
        
1046
        // the current vm state is a transition state
1047
        in_transition: function() {
1048
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
1049
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
1050
        },
1051
        
1052
        // get image object
1053
        // TODO: update images synchronously if image not found
1054
        get_image: function() {
1055
            var image = storage.images.get(this.get('imageRef'));
1056
            if (!image) {
1057
                storage.images.update_unknown_id(this.get('imageRef'));
1058
                image = storage.images.get(this.get('imageRef'));
1059
            }
1060
            return image;
1061
        },
1062
        
1063
        // get flavor object
1064
        get_flavor: function() {
1065
            var flv = storage.flavors.get(this.get('flavorRef'));
1066
            if (!flv) {
1067
                storage.flavors.update_unknown_id(this.get('flavorRef'));
1068
                flv = storage.flavors.get(this.get('flavorRef'));
1069
            }
1070
            return flv;
1071
        },
1072

    
1073
        // retrieve the metadata object
1074
        get_meta: function() {
1075
            try {
1076
                return this.get('metadata').values
1077
            } catch (err) {
1078
                return {};
1079
            }
1080
        },
1081
        
1082
        // get metadata OS value
1083
        get_os: function() {
1084
            return this.get_meta().OS || (this.get_image() ? this.get_image().get_os() || "okeanos" : "okeanos");
1085
        },
1086
        
1087
        // get public ip addresses
1088
        // TODO: public network is always the 0 index ???
1089
        get_addresses: function(net_id) {
1090
            var net_id = net_id || "public";
1091
            
1092
            var info = this.get_network_info(net_id);
1093
            if (!info) { return {} };
1094
            addrs = {};
1095
            _.each(info.values, function(addr) {
1096
                addrs["ip" + addr.version] = addr.addr;
1097
            });
1098
            return addrs
1099
        },
1100

    
1101
        get_network_info: function(net_id) {
1102
            var net_id = net_id || "public";
1103
            
1104
            if (!this.networks.network_ids.length) { return {} };
1105

    
1106
            var addresses = this.networks.get();
1107
            try {
1108
                return _.select(addresses, function(net, key){return key == net_id })[0];
1109
            } catch (err) {
1110
                //this.log.debug("Cannot find network {0}".format(net_id))
1111
            }
1112
        },
1113

    
1114
        firewall_profile: function(net_id) {
1115
            var net_id = net_id || "public";
1116
            var firewalls = this.get("firewalls");
1117
            return firewalls[net_id];
1118
        },
1119

    
1120
        has_firewall: function(net_id) {
1121
            var net_id = net_id || "public";
1122
            return ["ENABLED","PROTECTED"].indexOf(this.firewall_profile()) > -1;
1123
        },
1124
    
1125
        // get actions that the user can execute
1126
        // depending on the vm state/status
1127
        get_available_actions: function() {
1128
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1129
        },
1130

    
1131
        set_profile: function(profile, net_id) {
1132
        },
1133
        
1134
        // call rename api
1135
        rename: function(new_name) {
1136
            //this.set({'name': new_name});
1137
            this.sync("update", this, {
1138
                critical: true,
1139
                data: {
1140
                    'server': {
1141
                        'name': new_name
1142
                    }
1143
                }, 
1144
                // do the rename after the method succeeds
1145
                success: _.bind(function(){
1146
                    //this.set({name: new_name});
1147
                    snf.api.trigger("call");
1148
                }, this)
1149
            });
1150
        },
1151
        
1152
        get_console_url: function(data) {
1153
            var url_params = {
1154
                machine: this.get("name"),
1155
                host_ip: this.get_addresses().ip4,
1156
                host_ip_v6: this.get_addresses().ip6,
1157
                host: data.host,
1158
                port: data.port,
1159
                password: data.password
1160
            }
1161
            return '/machines/console?' + $.param(url_params);
1162
        },
1163

    
1164
        // action helper
1165
        call: function(action_name, success, error) {
1166
            var id_param = [this.id];
1167

    
1168
            success = success || function() {};
1169
            error = error || function() {};
1170

    
1171
            var self = this;
1172

    
1173
            switch(action_name) {
1174
                case 'start':
1175
                    this.__make_api_call(this.get_action_url(), // vm actions url
1176
                                         "create", // create so that sync later uses POST to make the call
1177
                                         {start:{}}, // payload
1178
                                         function() {
1179
                                             // set state after successful call
1180
                                             self.state("START"); 
1181
                                             success.apply(this, arguments);
1182
                                             snf.api.trigger("call");
1183
                                         },  
1184
                                         error, 'start');
1185
                    break;
1186
                case 'reboot':
1187
                    this.__make_api_call(this.get_action_url(), // vm actions url
1188
                                         "create", // create so that sync later uses POST to make the call
1189
                                         {reboot:{type:"HARD"}}, // payload
1190
                                         function() {
1191
                                             // set state after successful call
1192
                                             self.state("REBOOT"); 
1193
                                             success.apply(this, arguments)
1194
                                             snf.api.trigger("call");
1195
                                             self.set({'reboot_required': false});
1196
                                         },
1197
                                         error, 'reboot');
1198
                    break;
1199
                case 'shutdown':
1200
                    this.__make_api_call(this.get_action_url(), // vm actions url
1201
                                         "create", // create so that sync later uses POST to make the call
1202
                                         {shutdown:{}}, // payload
1203
                                         function() {
1204
                                             // set state after successful call
1205
                                             self.state("SHUTDOWN"); 
1206
                                             success.apply(this, arguments)
1207
                                             snf.api.trigger("call");
1208
                                         },  
1209
                                         error, 'shutdown');
1210
                    break;
1211
                case 'console':
1212
                    this.__make_api_call(this.url() + "/action", "create", {'console': {'type':'vnc'}}, function(data) {
1213
                        var cons_data = data.console;
1214
                        success.apply(this, [cons_data]);
1215
                    }, undefined, 'console')
1216
                    break;
1217
                case 'destroy':
1218
                    this.__make_api_call(this.url(), // vm actions url
1219
                                         "delete", // create so that sync later uses POST to make the call
1220
                                         undefined, // payload
1221
                                         function() {
1222
                                             // set state after successful call
1223
                                             self.state('DESTROY');
1224
                                             success.apply(this, arguments)
1225
                                         },  
1226
                                         error, 'destroy');
1227
                    break;
1228
                default:
1229
                    throw "Invalid VM action ("+action_name+")";
1230
            }
1231
        },
1232
        
1233
        __make_api_call: function(url, method, data, success, error, action) {
1234
            var self = this;
1235
            error = error || function(){};
1236
            success = success || function(){};
1237

    
1238
            var params = {
1239
                url: url,
1240
                data: data,
1241
                success: function(){ self.handle_action_succeed.apply(self, arguments); success.apply(this, arguments)},
1242
                error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)},
1243
                error_params: { ns: "Machines actions", 
1244
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1245
                                extra_details: { 'Machine ID': this.id, 'URL': url, 'Action': action || "undefined" },
1246
                                allow_reload: false
1247
                              },
1248
                display: false,
1249
                critical: false
1250
            }
1251
            this.sync(method, this, params);
1252
        },
1253

    
1254
        handle_action_succeed: function() {
1255
            this.trigger("action:success", arguments);
1256
        },
1257
        
1258
        reset_action_error: function() {
1259
            this.action_error = false;
1260
            this.trigger("action:fail:reset", this.action_error);
1261
        },
1262

    
1263
        handle_action_fail: function() {
1264
            this.action_error = arguments;
1265
            this.trigger("action:fail", arguments);
1266
        },
1267

    
1268
        get_action_url: function(name) {
1269
            return this.url() + "/action";
1270
        },
1271

    
1272
        get_connection_info: function(host_os, success, error) {
1273
            var url = "/machines/connect";
1274
            params = {
1275
                ip_address: this.get_addresses().ip4,
1276
                os: this.get_os(),
1277
                host_os: host_os,
1278
                srv: this.id
1279
            }
1280

    
1281
            url = url + "?" + $.param(params);
1282

    
1283
            var ajax = snf.api.sync("read", undefined, { url: url, 
1284
                                                         error:error, 
1285
                                                         success:success, 
1286
                                                         handles_error:1});
1287
        }
1288
    })
1289
    
1290
    models.VM.ACTIONS = [
1291
        'start',
1292
        'shutdown',
1293
        'reboot',
1294
        'console',
1295
        'destroy'
1296
    ]
1297

    
1298
    models.VM.AVAILABLE_ACTIONS = {
1299
        'UNKNWON'       : ['destroy'],
1300
        'BUILD'         : ['destroy'],
1301
        'REBOOT'        : ['shutdown', 'destroy', 'console'],
1302
        'STOPPED'       : ['start', 'destroy'],
1303
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1304
        'ERROR'         : ['destroy'],
1305
        'DELETED'        : [],
1306
        'DESTROY'       : [],
1307
        'BUILD_INIT'    : ['destroy'],
1308
        'BUILD_COPY'    : ['destroy'],
1309
        'BUILD_FINAL'   : ['destroy'],
1310
        'SHUTDOWN'      : ['destroy'],
1311
        'START'         : [],
1312
        'CONNECT'       : [],
1313
        'DISCONNECT'    : []
1314
    }
1315

    
1316
    // api status values
1317
    models.VM.STATUSES = [
1318
        'UNKNWON',
1319
        'BUILD',
1320
        'REBOOT',
1321
        'STOPPED',
1322
        'ACTIVE',
1323
        'ERROR',
1324
        'DELETED'
1325
    ]
1326

    
1327
    // api status values
1328
    models.VM.CONNECT_STATES = [
1329
        'ACTIVE',
1330
        'REBOOT',
1331
        'SHUTDOWN'
1332
    ]
1333

    
1334
    // vm states
1335
    models.VM.STATES = models.VM.STATUSES.concat([
1336
        'DESTROY',
1337
        'BUILD_INIT',
1338
        'BUILD_COPY',
1339
        'BUILD_FINAL',
1340
        'SHUTDOWN',
1341
        'START',
1342
        'CONNECT',
1343
        'DISCONNECT',
1344
        'FIREWALL'
1345
    ]);
1346
    
1347
    models.VM.STATES_TRANSITIONS = {
1348
        'DESTROY' : ['DELETED'],
1349
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1350
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1351
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1352
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1353
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1354
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1355
        'BUILD_COPY': ['ERROR', 'ACTIVE', 'BUILD_FINAL', 'DESTROY'],
1356
        'BUILD_FINAL': ['ERROR', 'ACTIVE', 'DESTROY'],
1357
        'BUILD_INIT': ['ERROR', 'ACTIVE', 'BUILD_COPY', 'BUILD_FINAL', 'DESTROY']
1358
    }
1359

    
1360
    models.VM.TRANSITION_STATES = [
1361
        'DESTROY',
1362
        'SHUTDOWN',
1363
        'START',
1364
        'REBOOT',
1365
        'BUILD'
1366
    ]
1367

    
1368
    models.VM.ACTIVE_STATES = [
1369
        'BUILD', 'REBOOT', 'ACTIVE',
1370
        'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL',
1371
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1372
    ]
1373

    
1374
    models.VM.BUILDING_STATES = [
1375
        'BUILD', 'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL'
1376
    ]
1377

    
1378
    models.Networks = models.Collection.extend({
1379
        model: models.Network,
1380
        path: 'networks',
1381
        details: true,
1382
        //noUpdate: true,
1383
        defaults: {'linked_to':[]},
1384

    
1385
        parse: function (resp, xhr) {
1386
            // FIXME: depricated global var
1387
            if (!resp) { return []};
1388
               
1389
            var data = _.map(resp.networks.values, _.bind(this.parse_net_api_data, this));
1390
            return data;
1391
        },
1392

    
1393
        reset_pending_actions: function() {
1394
            this.each(function(net) {
1395
                net.get("actions").reset();
1396
            })
1397
        },
1398

    
1399
        do_all_pending_actions: function() {
1400
            this.each(function(net) {
1401
                net.do_all_pending_actions();
1402
            })
1403
        },
1404

    
1405
        parse_net_api_data: function(data) {
1406
            if (data.servers && data.servers.values) {
1407
                data['linked_to'] = data.servers.values;
1408
            }
1409
            return data;
1410
        },
1411

    
1412
        create: function (name, callback) {
1413
            return this.api.call(this.path, "create", {network:{name:name}}, callback);
1414
        }
1415
    })
1416

    
1417
    models.Images = models.Collection.extend({
1418
        model: models.Image,
1419
        path: 'images',
1420
        details: true,
1421
        noUpdate: true,
1422
        supportIncUpdates: false,
1423
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1424

    
1425
        // update collection model with id passed
1426
        // making a direct call to the image
1427
        // api url
1428
        update_unknown_id: function(id) {
1429
            var url = getUrl.call(this) + "/" + id;
1430
            this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, 
1431
            _.bind(function() {
1432
                this.add({id:id, name:"Unknown image", size:-1, progress:100, status:"DELETED"})
1433
            }, this), _.bind(function(image) {
1434
                this.add(image.image);
1435
            }, this));
1436
        },
1437

    
1438
        parse: function (resp, xhr) {
1439
            // FIXME: depricated global var
1440
            var data = _.map(resp.images.values, _.bind(this.parse_meta, this));
1441
            return resp.images.values;
1442
        },
1443

    
1444
        get_meta_key: function(img, key) {
1445
            if (img.metadata && img.metadata.values && img.metadata.values[key]) {
1446
                return img.metadata.values[key];
1447
            }
1448
            return undefined;
1449
        },
1450

    
1451
        comparator: function(img) {
1452
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1453
        },
1454

    
1455
        parse_meta: function(img) {
1456
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1457
                img[key] = this.get_meta_key(img, key) || "";
1458
            }, this));
1459
            return img;
1460
        },
1461

    
1462
        active: function() {
1463
            return this.filter(function(img){return img.get('status') != "DELETED"});
1464
        }
1465
    })
1466

    
1467
    models.Flavors = models.Collection.extend({
1468
        model: models.Flavor,
1469
        path: 'flavors',
1470
        details: true,
1471
        noUpdate: true,
1472
        supportIncUpdates: false,
1473
        // update collection model with id passed
1474
        // making a direct call to the flavor
1475
        // api url
1476
        update_unknown_id: function(id) {
1477
            var url = getUrl.call(this) + "/" + id;
1478
            this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, 
1479
            _.bind(function() {
1480
                this.add({id:id, cpu:"", ram:"", disk:"", name: "", status:"DELETED"})
1481
            }, this), _.bind(function(flv) {
1482
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1483
                this.add(flv.flavor);
1484
            }, this));
1485
        },
1486

    
1487
        parse: function (resp, xhr) {
1488
            // FIXME: depricated global var
1489
            return resp.flavors.values;
1490
        },
1491

    
1492
        comparator: function(flv) {
1493
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1494
        },
1495

    
1496
        unavailable_values_for_image: function(img, flavors) {
1497
            var flavors = flavors || this.active();
1498
            var size = img.get_size();
1499
            
1500
            var index = {cpu:[], disk:[], ram:[]};
1501

    
1502
            _.each(this.active(), function(el) {
1503
                var img_size = size;
1504
                var flv_size = el.get_disk_size();
1505
                if (flv_size < img_size) {
1506
                    if (index.disk.indexOf(flv_size) == -1) {
1507
                        index.disk.push(flv_size);
1508
                    }
1509
                };
1510
            });
1511
            
1512
            return index;
1513
        },
1514

    
1515
        get_flavor: function(cpu, mem, disk, filter_list) {
1516
            if (!filter_list) { filter_list = this.models };
1517

    
1518
            return this.select(function(flv){
1519
                if (flv.get("cpu") == cpu + "" &&
1520
                   flv.get("ram") == mem + "" &&
1521
                   flv.get("disk") == disk + "" &&
1522
                   filter_list.indexOf(flv) > -1) { return true; }
1523
            })[0];
1524
        },
1525
        
1526
        get_data: function(lst) {
1527
            var data = {'cpu': [], 'mem':[], 'disk':[]};
1528

    
1529
            _.each(lst, function(flv) {
1530
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1531
                    data.cpu.push(flv.get("cpu"));
1532
                }
1533
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1534
                    data.mem.push(flv.get("ram"));
1535
                }
1536
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1537
                    data.disk.push(flv.get("disk"));
1538
                }
1539
            })
1540
            
1541
            return data;
1542
        },
1543

    
1544
        active: function() {
1545
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1546
        }
1547
            
1548
    })
1549

    
1550
    models.VMS = models.Collection.extend({
1551
        model: models.VM,
1552
        path: 'servers',
1553
        details: true,
1554
        copy_image_meta: true,
1555
        
1556
        parse: function (resp, xhr) {
1557
            // FIXME: depricated after refactoring
1558
            var data = resp;
1559
            if (!resp) { return [] };
1560
            data = _.filter(_.map(resp.servers.values, _.bind(this.parse_vm_api_data, this)), function(v){return v});
1561
            return data;
1562
        },
1563
        
1564
        get_reboot_required: function() {
1565
            return this.filter(function(vm){return vm.get("reboot_required") == true})
1566
        },
1567

    
1568
        has_pending_actions: function() {
1569
            return this.filter(function(vm){return vm.pending_action}).length > 0;
1570
        },
1571

    
1572
        reset_pending_actions: function() {
1573
            this.each(function(vm) {
1574
                vm.clear_pending_action();
1575
            })
1576
        },
1577

    
1578
        do_all_pending_actions: function(success, error) {
1579
            this.each(function(vm) {
1580
                if (vm.has_pending_action()) {
1581
                    vm.call(vm.pending_action, success, error);
1582
                    vm.clear_pending_action();
1583
                }
1584
            })
1585
        },
1586
        
1587
        do_all_reboots: function(success, error) {
1588
            this.each(function(vm) {
1589
                if (vm.get("reboot_required")) {
1590
                    vm.call("reboot", success, error);
1591
                }
1592
            });
1593
        },
1594

    
1595
        reset_reboot_required: function() {
1596
            this.each(function(vm) {
1597
                vm.set({'reboot_required': undefined});
1598
            })
1599
        },
1600
        
1601
        stop_stats_update: function(exclude) {
1602
            var exclude = exclude || [];
1603
            this.each(function(vm) {
1604
                if (exclude.indexOf(vm) > -1) {
1605
                    return;
1606
                }
1607
                vm.stop_stats_update();
1608
            })
1609
        },
1610
        
1611
        has_meta: function(vm_data) {
1612
            return vm_data.metadata && vm_data.metadata.values
1613
        },
1614

    
1615
        has_addresses: function(vm_data) {
1616
            return vm_data.metadata && vm_data.metadata.values
1617
        },
1618

    
1619
        parse_vm_api_data: function(data) {
1620
            // do not add non existing DELETED entries
1621
            if (data.status && data.status == "DELETED") {
1622
                if (!this.get(data.id)) {
1623
                    console.error("non exising deleted vm", data)
1624
                    return false;
1625
                }
1626
            }
1627

    
1628
            // OS attribute
1629
            if (this.has_meta(data)) {
1630
                data['OS'] = data.metadata.values.OS || "okeanos";
1631
            }
1632
            
1633
            data['firewalls'] = {};
1634
            if (data['addresses'] && data['addresses'].values) {
1635
                data['linked_to_nets'] = data['addresses'].values;
1636
                _.each(data['addresses'].values, function(f){
1637
                    if (f['firewallProfile']) {
1638
                        data['firewalls'][f['id']] = f['firewallProfile']
1639
                    }
1640
                });
1641
            }
1642
            
1643
            // if vm has no metadata, no metadata object
1644
            // is in json response, reset it to force
1645
            // value update
1646
            if (!data['metadata']) {
1647
                data['metadata'] = {values:{}};
1648
            }
1649

    
1650
            return data;
1651
        },
1652

    
1653
        create: function (name, image, flavor, meta, extra, callback) {
1654
            if (this.copy_image_meta) {
1655
                meta['OS'] = image.get("OS");
1656
           }
1657
            
1658
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta}
1659
            opts = _.extend(opts, extra);
1660

    
1661
            this.api.call(this.path, "create", {'server': opts}, undefined, undefined, callback, {critical: false});
1662
        }
1663

    
1664
    })
1665

    
1666
    models.PublicKey = models.Model.extend({
1667
        path: 'keys',
1668
        base_url: '/ui/userdata',
1669
        details: false,
1670
        noUpdate: true,
1671

    
1672

    
1673
        get_public_key: function() {
1674
            return cryptico.publicKeyFromString(this.get("content"));
1675
        },
1676

    
1677
        get_filename: function() {
1678
            return "{0}.pub".format(this.get("name"));
1679
        },
1680

    
1681
        identify_type: function() {
1682
            try {
1683
                var cont = snf.util.validatePublicKey(this.get("content"));
1684
                var type = cont.split(" ")[0];
1685
                return synnefo.util.publicKeyTypesMap[type];
1686
            } catch (err) { return false };
1687
        }
1688

    
1689
    })
1690
    
1691
    models.PublicKeys = models.Collection.extend({
1692
        model: models.PublicKey,
1693
        details: false,
1694
        path: 'keys',
1695
        base_url: '/ui/userdata',
1696
        noUpdate: true,
1697

    
1698
        comparator: function(i) { return -parseInt(i.id || 0) },
1699

    
1700
        generate_new: function(passphrase, length, save) {
1701

    
1702
            var passphrase = passphrase || "";
1703
            var length = length || 1024;
1704
            var key = cryptico.generateRSAKey(passphrase, length);
1705
            
1706
            var b64enc = $.base64.encode;
1707
            return key;
1708
        },
1709

    
1710
        add_crypto_key: function(key, success, error, options) {
1711
            var options = options || {};
1712
            var m = new models.PublicKey();
1713
            var name_tpl = "public key";
1714
            var name = name_tpl;
1715
            var name_count = 1;
1716
            
1717
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
1718
                name = name_tpl + " " + name_count;
1719
                name_count++;
1720
            }
1721

    
1722
            m.set({name: name});
1723
            m.set({content: "ssh-rsa AAAAB3NzaC1yc2EA" + cryptico.publicKeyString(key)});
1724
            
1725
            options.success = function () { return success(m) };
1726
            options.errror = error;
1727
            
1728
            this.create(m.attributes, options);
1729
        }
1730
    })
1731
    
1732
    // storage initialization
1733
    snf.storage.images = new models.Images();
1734
    snf.storage.flavors = new models.Flavors();
1735
    snf.storage.networks = new models.Networks();
1736
    snf.storage.vms = new models.VMS();
1737
    snf.storage.keys = new models.PublicKeys();
1738

    
1739
    //snf.storage.vms.fetch({update:true});
1740
    //snf.storage.images.fetch({update:true});
1741
    //snf.storage.flavors.fetch({update:true});
1742

    
1743
})(this);