Statistics
| Branch: | Tag: | Revision:

root / ui / static / snf / js / models.js @ 9ce969a7

History | View | Annotate | Download (49.9 kB)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
152
        get_os: function() {
153
            return this.get("OS");
154
        }
155
    });
156

    
157
    // Flavor model
158
    models.Flavor = models.Model.extend({
159
        path: 'flavors',
160

    
161
        details_string: function() {
162
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
163
        },
164

    
165
        get_disk_size: function() {
166
            return parseInt(this.get("disk") * 1000)
167
        },
168

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

    
184
            if (this.pending_for_removal.length) {
185
                this.trigger("pending:remove:add");
186
            }
187
        },
188

    
189
        this.add_pending = function(vm_id) {
190
            if (this.pending.indexOf(vm_id) == -1) {
191
                this.pending[this.pending.length] = vm_id;
192
            }
193

    
194
            if (this.pending.length) {
195
                this.trigger("pending:add");
196
            }
197
        }
198

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

    
209
            var len = this.pending_for_removal.length;
210
            this.pending_for_removal = _.intersection(this.pending_for_removal, this.vms);
211
            if (this.pending_for_removal.length == 0) {
212
                this.trigger("pending:remove:clear");
213
            }
214

    
215
        }
216

    
217

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

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

    
236
        this.get = function() {
237
            return this.vms;
238
        }
239

    
240
        this.list = function() {
241
            return storage.vms.filter(_.bind(function(vm){
242
                return this.vms.indexOf(vm.id) > -1;
243
            }, this))
244
        }
245

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

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

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

    
276
        this.get = function() {
277
            return this.networks;
278
        }
279

    
280
        this.list = function() {
281
            return storage.networks.filter(_.bind(function(net){
282
                return this.network_ids.indexOf(net.id) > -1;
283
            }, this))
284
        }
285

    
286
        this.initialize();
287
    };
288
    _.extend(VMNetworksList.prototype, bb.Events);
289

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

    
302
            ret = models.Network.__super__.initialize.apply(this, arguments);
303

    
304
            storage.vms.bind("change:linked_to_nets", _.bind(this.update_connections, this, "vm:change"));
305
            storage.vms.bind("add", _.bind(this.update_connections, this, "add"));
306
            storage.vms.bind("remove", _.bind(this.update_connections, this, "remove"));
307
            storage.vms.bind("reset", _.bind(this.update_connections, this, "reset"));
308
            this.bind("change:linked_to", _.bind(this.update_connections, this, "net:change"));
309
            this.update_connections();
310
            this.update_state();
311
            return ret;
312
        },
313

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

    
336
            this.set({state:"NORMAL"});
337
        },
338

    
339
        handle_pending_connections: function(action) {
340
            this.update_state();
341
        },
342

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

    
388
        is_public: function() {
389
            return this.id == "public";
390
        },
391

    
392
        contains_vm: function(vm) {
393
            var net_vm_exists = this.vms.get().indexOf(vm.id) > -1;
394
            var vm_net_exists = vm.is_connected_to(this);
395
            return net_vm_exists && vm_net_exists;
396
        },
397

    
398
        add_vm: function (vm, callback, error, options) {
399
            var payload = {add:{serverRef:"" + vm.id}};
400
            payload._options = options || {};
401
            return this.api.call(this.api_path() + "/action", "create", 
402
                                 payload,
403
                                 _.bind(function(){
404
                                     this.vms.add_pending(vm.id);
405
                                     if (callback) {callback()}
406
                                 },this), error);
407
        },
408

    
409
        remove_vm: function (vm, callback, error, options) {
410
            var payload = {remove:{serverRef:"" + vm.id}};
411
            payload._options = options || {};
412
            return this.api.call(this.api_path() + "/action", "create", 
413
                                 {remove:{serverRef:"" + vm.id}},
414
                                 _.bind(function(){
415
                                     this.vms.add_pending_for_remove(vm.id);
416
                                     if (callback) {callback()}
417
                                 },this), error);
418
        },
419

    
420
        rename: function(name, callback) {
421
            return this.api.call(this.api_path(), "update", {network:{name:name}}, callback);
422
        },
423

    
424
        get_connectable_vms: function() {
425
            var servers = this.vms.list();
426
            return storage.vms.filter(function(vm){return servers.indexOf(vm.id) == -1})
427
        },
428

    
429
        state_message: function() {
430
            if (this.get("state") == "NORMAL" && this.is_public()) {
431
                return "Public network";
432
            }
433

    
434
            return models.Network.STATES[this.get("state")];
435
        },
436

    
437
        in_progress: function() {
438
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
439
        }
440
    });
441
    
442
    models.Network.STATES = {
443
        'NORMAL': 'Private network',
444
        'CONNECTING': 'Connecting...',
445
        'DISCONNECTING': 'Disconnecting...',
446
        'FIREWALLING': 'Firewall update...'
447
    }
448

    
449
    models.Network.STATES_TRANSITIONS = {
450
        'CONNECTING': ['NORMAL'],
451
        'DISCONNECTING': ['NORMAL'],
452
        'FIREWALLING': ['NORMAL']
453
    }
454

    
455
    // Virtualmachine model
456
    models.VM = models.Model.extend({
457

    
458
        path: 'servers',
459
        has_status: true,
460
        initialize: function(params) {
461
            this.networks = new VMNetworksList();
462
            
463
            this.pending_firewalls = {};
464
            
465
            models.VM.__super__.initialize.apply(this, arguments);
466

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

    
484
            // initialize interval
485
            this.init_stats_intervals(this.stats_update_interval);
486
            
487
            this.bind("change:progress", _.bind(this.update_building_progress, this));
488
            this.update_building_progress();
489

    
490
            this.bind("change:firewalls", _.bind(this.handle_firewall_change, this));
491
            
492
            // default values
493
            this.set({linked_to_nets:this.get("linked_to_nets") || []});
494
            this.set({firewalls:this.get("firewalls") || []});
495

    
496
            this.bind("change:state", _.bind(function(){if (this.state() == "DESTROY") { this.handle_destroy() }}, this))
497
        },
498

    
499
        handle_firewall_change: function() {
500

    
501
        },
502
        
503
        set_linked_to_nets: function(data) {
504
            this.set({"linked_to":_.map(data, function(n){ return n.id})});
505
            return data;
506
        },
507

    
508
        is_connected_to: function(net) {
509
            return _.filter(this.networks.list(), function(n){return n.id == net.id}).length > 0;
510
        },
511
        
512
        status: function(st) {
513
            if (!st) { return this.get("status")}
514
            return this.set({status:st});
515
        },
516

    
517
        set_status: function(st) {
518
            var new_state = this.state_for_api_status(st);
519
            var transition = false;
520

    
521
            if (this.state() != new_state) {
522
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
523
                    transition = this.state();
524
                }
525
            }
526
            
527
            // call it silently to avoid double change trigger
528
            this.set({'state': this.state_for_api_status(st)}, {silent: true});
529
            
530
            // trigger transition
531
            if (transition) { this.trigger("transition", {from:transition, to:new_state}) };
532
            return st;
533
        },
534

    
535
        update_building_progress: function() {
536
            if (this.is_building()) {
537
                var progress = this.get("progress");
538
                if (progress == 0) {
539
                    this.state("BUILD_INIT");
540
                    this.set({progress_message: BUILDING_MESSAGES['INIT']});
541
                }
542
                if (progress > 0 && progress < 99) {
543
                    this.state("BUILD_COPY");
544
                    var params = this.get_copy_details(true);
545
                    this.set({progress_message: BUILDING_MESSAGES['COPY'].format(params.copy, 
546
                                                                                 params.size, 
547
                                                                                 params.progress)});
548
                }
549
                if (progress == 100) {
550
                    this.state("BUILD_FINAL");
551
                    this.set({progress_message: BUILDING_MESSAGES['FINAL']});
552
                }
553
            } else {
554
            }
555
        },
556

    
557
        get_copy_details: function(human, image) {
558
            var human = human || false;
559
            var image = image || this.get_image();
560

    
561
            var progress = this.get('progress');
562
            var size = image.get_size();
563
            var size_copied = (size * progress / 100).toFixed(2);
564
            
565
            if (human) {
566
                size = util.readablizeBytes(size*1024*1024);
567
                size_copied = util.readablizeBytes(size_copied*1024*1024);
568
            }
569
            return {'progress': progress, 'size': size, 'copy': size_copied};
570
        },
571

    
572
        // clear and reinitialize update interval
573
        init_stats_intervals: function (interval) {
574
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
575
            this.stats_fetcher.start();
576
            this.update_stats(true);
577
        },
578
        
579
        get_stats_fetcher: function(timeout) {
580
            var cb = _.bind(function(data){
581
                this.update_stats();
582
            }, this);
583
            var fetcher = new snf.api.updateHandler({'callback': cb, timeout:timeout});
584
            return fetcher;
585
        },
586

    
587
        // do the api call
588
        update_stats: function(force) {
589
            // do not update stats if flag not set
590
            if (!this.do_update_stats && !force) {
591
                return;
592
            }
593

    
594
            // make the api call, execute handle_stats_update on sucess
595
            // TODO: onError handler ???
596
            stats_url = this.url() + "/stats";
597
            this.sync("GET", this, {
598
                handles_error:true, 
599
                url: stats_url, 
600
                refresh:true, 
601
                success: _.bind(this.handle_stats_update, this),
602
                error: _.bind(this.handle_stats_error, this)
603
            });
604
        },
605

    
606
        get_stats_image: function(stat, type) {
607
        },
608
        
609
        _set_stats: function(stats) {
610
            var silent = silent === undefined ? false : silent;
611
            // unavailable stats while building
612
            if (this.get("status") == "BUILD") { 
613
                this.stats_available = false;
614
            } else { this.stats_available = true; }
615

    
616
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
617
            
618
            this.set({stats: stats}, {silent:true});
619
            this.trigger("stats:update", stats);
620
        },
621

    
622
        unbind: function() {
623
            models.VM.__super__.unbind.apply(this, arguments);
624
        },
625

    
626
        handle_stats_error: function() {
627
            stats = {};
628
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
629
                stats[k] = false;
630
            });
631

    
632
            this.set({'stats': stats});
633
        },
634

    
635
        // this method gets executed after a successful vm stats api call
636
        handle_stats_update: function(data) {
637
            var self = this;
638
            // avoid browser caching
639
            
640
            if (data.stats && _.size(data.stats) > 0) {
641
                var ts = $.now();
642
                var stats = data.stats;
643
                var images_loaded = 0;
644
                var images = {};
645

    
646
                function check_images_loaded() {
647
                    images_loaded++;
648

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

    
669
                    img.error(function() {
670
                        images[stat + type] = false;
671
                        check_images_loaded();
672
                    });
673

    
674
                    img.attr({'src': stats[k]});
675
                })
676
                data.stats = stats;
677
            }
678

    
679
            // do we need to change the interval ??
680
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
681
                this.stats_update_interval = data.stats.refresh * 1000;
682
                this.stats_fetcher.timeout = this.stats_update_interval;
683
                this.stats_fetcher.stop();
684
                this.stats_fetcher.start();
685
            }
686
        },
687

    
688
        // helper method that sets the do_update_stats
689
        // in the future this method could also make an api call
690
        // immediaetly if needed
691
        enable_stats_update: function() {
692
            this.do_update_stats = true;
693
        },
694
        
695
        handle_destroy: function() {
696
            this.stats_fetcher.stop();
697
        },
698

    
699
        require_reboot: function() {
700
            if (this.is_active()) {
701
                this.set({'reboot_required': true});
702
            }
703
        },
704
        
705
        set_pending_action: function(data) {
706
            this.pending_action = data;
707
            return data;
708
        },
709

    
710
        // machine has pending action
711
        update_pending_action: function(action, force) {
712
            this.set({pending_action: action});
713
        },
714

    
715
        clear_pending_action: function() {
716
            this.set({pending_action: undefined});
717
        },
718

    
719
        has_pending_action: function() {
720
            return this.get("pending_action") ? this.get("pending_action") : false;
721
        },
722
        
723
        // machine is active
724
        is_active: function() {
725
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
726
        },
727
        
728
        // machine is building 
729
        is_building: function() {
730
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
731
        },
732

    
733
        // user can connect to machine
734
        is_connectable: function() {
735
            // check if ips exist
736
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
737
                return false;
738
            }
739
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
740
        },
741
        
742
        set_firewalls: function(data) {
743
            _.each(data, _.bind(function(val, key){
744
                if (this.pending_firewalls && this.pending_firewalls[key] && this.pending_firewalls[key] == val) {
745
                        this.require_reboot();
746
                        this.remove_pending_firewall(key, val);
747
                }
748
            }, this));
749
            return data;
750
        },
751

    
752
        remove_pending_firewall: function(net_id, value) {
753
            if (this.pending_firewalls[net_id] == value) {
754
                delete this.pending_firewalls[net_id];
755
                storage.networks.get(net_id).update_state();
756
            }
757
        },
758
            
759
        remove_meta: function(key, complete, error) {
760
            var url = this.api_path() + "/meta/" + key;
761
            this.api.call(url, "delete", undefined, complete, error);
762
        },
763

    
764
        save_meta: function(meta, complete, error) {
765
            var url = this.api_path() + "/meta/" + meta.key;
766
            var payload = {meta:{}};
767
            payload.meta[meta.key] = meta.value;
768

    
769
            // inject error settings
770
            payload._options = {critical: false};
771

    
772
            this.api.call(url, "update", payload, complete, error)
773
        },
774

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

    
778
            this.pending_firewalls[net_id] = value;
779
            this.trigger("change", this, this);
780
            var payload = {"firewallProfile":{"profile":value}};
781
            payload._options = _.extend({critical: false}, options);
782
            
783
            // reset firewall state on error
784
            var error_cb = _.bind(function() {
785
                thi
786
            }, this);
787

    
788
            this.api.call(this.api_path() + "/action", "create", payload, callback, error);
789
            storage.networks.get(net_id).update_state();
790
        },
791

    
792
        firewall_pending: function(net_id) {
793
            return this.pending_firewalls[net_id] != undefined;
794
        },
795
        
796
        // update/get the state of the machine
797
        state: function() {
798
            var args = slice.call(arguments);
799
                
800
            // TODO: it might not be a good idea to set the state in set_state method
801
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
802
                this.set({'state': args[0]});
803
            }
804

    
805
            return this.get('state');
806
        },
807
        
808
        // get the state that the api status corresponds to
809
        state_for_api_status: function(status) {
810
            return this.state_transition(this.state(), status);
811
        },
812
        
813
        // vm state equals vm api status
814
        state_is_status: function(state) {
815
            return models.VM.STATUSES.indexOf(state) != -1;
816
        },
817
        
818
        // get transition state for the corresponging api status
819
        state_transition: function(state, new_status) {
820
            var statuses = models.VM.STATES_TRANSITIONS[state];
821
            if (statuses) {
822
                if (statuses.indexOf(new_status) > -1) {
823
                    return new_status;
824
                } else {
825
                    return state;
826
                }
827
            } else {
828
                return new_status;
829
            }
830
        },
831
        
832
        // the current vm state is a transition state
833
        in_transition: function() {
834
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
835
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
836
        },
837
        
838
        // get image object
839
        // TODO: update images synchronously if image not found
840
        get_image: function() {
841
            var image = storage.images.get(this.get('imageRef'));
842
            if (!image) {
843
                storage.images.update_unknown_id(this.get('imageRef'));
844
                image = storage.flavors.get(this.get('imageRef'));
845
            }
846
            return image;
847
        },
848
        
849
        // get flavor object
850
        // TODO: update flavors synchronously if image not found
851
        get_flavor: function() {
852
            var flv = storage.flavors.get(this.get('flavorRef'));
853
            if (!flv) {
854
                storage.flavors.update_unknown_id(this.get('flavorRef'));
855
                flv = storage.flavors.get(this.get('flavorRef'));
856
            }
857
            return flv;
858
        },
859

    
860
        // retrieve the metadata object
861
        get_meta: function() {
862
            //return {
863
                //'OS': 'debian',
864
                //'username': 'vinilios',
865
                //'group': 'webservers',
866
                //'meta2': 'meta value',
867
                //'looooooooooooooooong meta': 'short value',
868
                //'short meta': 'loooooooooooooooooooooooooooooooooong value',
869
                //'21421': 'fdsfds fds',
870
                //'21421': 'fdsfds fds',
871
                //'1fds 21421': 'fdsfds fds',
872
                //'fds 21421': 'fdsfds fds',
873
                //'fge 21421': 'fdsfds fds',
874
                //'21421 rew rew': 'fdsfds fds'
875
            //}
876
            try {
877
                return this.get('metadata').values
878
            } catch (err) {
879
                return {};
880
            }
881
        },
882
        
883
        // get metadata OS value
884
        get_os: function() {
885
            return this.get_meta().OS;
886
        },
887
        
888
        // get public ip addresses
889
        // TODO: public network is always the 0 index ???
890
        get_addresses: function(net_id) {
891
            var net_id = net_id || "public";
892
            
893
            var info = this.get_network_info(net_id);
894
            if (!info) { return {} };
895
            addrs = {};
896
            _.each(info.values, function(addr) {
897
                addrs["ip" + addr.version] = addr.addr;
898
            });
899
            return addrs
900
        },
901

    
902
        get_network_info: function(net_id) {
903
            var net_id = net_id || "public";
904
            
905
            if (!this.networks.network_ids.length) { return {} };
906

    
907
            var addresses = this.networks.get();
908
            try {
909
                return _.select(addresses, function(net, key){return key == net_id })[0];
910
            } catch (err) {
911
                //this.log.debug("Cannot find network {0}".format(net_id))
912
            }
913
        },
914

    
915
        firewall_profile: function(net_id) {
916
            var net_id = net_id || "public";
917
            var firewalls = this.get("firewalls");
918
            return firewalls[net_id];
919
        },
920

    
921
        has_firewall: function(net_id) {
922
            var net_id = net_id || "public";
923
            return ["ENABLED","PROTECTED"].indexOf(this.firewall_profile()) > -1;
924
        },
925
    
926
        // get actions that the user can execute
927
        // depending on the vm state/status
928
        get_available_actions: function() {
929
            return models.VM.AVAILABLE_ACTIONS[this.state()];
930
        },
931

    
932
        set_profile: function(profile, net_id) {
933
        },
934
        
935
        // call rename api
936
        rename: function(new_name) {
937
            //this.set({'name': new_name});
938
            this.sync("update", this, {
939
                data: {
940
                    'server': {
941
                        'name': new_name
942
                    }
943
                }, 
944
                // do the rename after the method succeeds
945
                success: _.bind(function(){
946
                    //this.set({name: new_name});
947
                    snf.api.trigger("call");
948
                }, this)
949
            });
950
        },
951
        
952
        get_console_url: function(data) {
953
            var url_params = {
954
                machine: this.get("name"),
955
                host_ip: this.get_addresses().ip4,
956
                host_ip_v6: this.get_addresses().ip6,
957
                host: data.host,
958
                port: data.port,
959
                password: data.password
960
            }
961
            return '/machines/console?' + $.param(url_params);
962
        },
963

    
964
        // action helper
965
        call: function(action_name, success, error) {
966
            var id_param = [this.id];
967

    
968
            success = success || function() {};
969
            error = error || function() {};
970

    
971
            var self = this;
972

    
973
            switch(action_name) {
974
                case 'start':
975
                    this.__make_api_call(this.get_action_url(), // vm actions url
976
                                         "create", // create so that sync later uses POST to make the call
977
                                         {start:{}}, // payload
978
                                         function() {
979
                                             // set state after successful call
980
                                             self.state("START"); 
981
                                             success.apply(this, arguments);
982
                                             snf.api.trigger("call");
983
                                         },  
984
                                         error, 'start');
985
                    break;
986
                case 'reboot':
987
                    this.__make_api_call(this.get_action_url(), // vm actions url
988
                                         "create", // create so that sync later uses POST to make the call
989
                                         {reboot:{type:"HARD"}}, // payload
990
                                         function() {
991
                                             // set state after successful call
992
                                             self.state("REBOOT"); 
993
                                             success.apply(this, arguments)
994
                                             snf.api.trigger("call");
995
                                             self.set({'reboot_required': false});
996
                                         },
997
                                         error, 'reboot');
998
                    break;
999
                case 'shutdown':
1000
                    this.__make_api_call(this.get_action_url(), // vm actions url
1001
                                         "create", // create so that sync later uses POST to make the call
1002
                                         {shutdown:{}}, // payload
1003
                                         function() {
1004
                                             // set state after successful call
1005
                                             self.state("SHUTDOWN"); 
1006
                                             success.apply(this, arguments)
1007
                                             snf.api.trigger("call");
1008
                                         },  
1009
                                         error, 'shutdown');
1010
                    break;
1011
                case 'console':
1012
                    this.__make_api_call(this.url() + "/action", "create", {'console': {'type':'vnc'}}, function(data) {
1013
                        var cons_data = data.console;
1014
                        success.apply(this, [cons_data]);
1015
                    }, undefined, 'console')
1016
                    break;
1017
                case 'destroy':
1018
                    this.__make_api_call(this.url(), // vm actions url
1019
                                         "delete", // create so that sync later uses POST to make the call
1020
                                         undefined, // payload
1021
                                         function() {
1022
                                             // set state after successful call
1023
                                             self.state('DESTROY');
1024
                                             success.apply(this, arguments)
1025
                                         },  
1026
                                         error, 'destroy');
1027
                    break;
1028
                default:
1029
                    throw "Invalid VM action ("+action_name+")";
1030
            }
1031
        },
1032
        
1033
        __make_api_call: function(url, method, data, success, error, action) {
1034
            var self = this;
1035
            error = error || function(){};
1036
            success = success || function(){};
1037

    
1038
            var params = {
1039
                url: url,
1040
                data: data,
1041
                success: function(){ self.handle_action_succeed.apply(self, arguments); success.apply(this, arguments)},
1042
                error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)},
1043
                error_params: { ns: "Machines actions", 
1044
                                message: "'" + this.get("name") + "'" + " action failed", 
1045
                                extra_details: { 'Machine ID': this.id, 'URL': url, 'Action': action || "undefined" },
1046
                                allow_reload: false
1047
                              },
1048
                display: false,
1049
                critical: false
1050
            }
1051
            this.sync(method, this, params);
1052
        },
1053

    
1054
        handle_action_succeed: function() {
1055
            this.trigger("action:success", arguments);
1056
        },
1057
        
1058
        reset_action_error: function() {
1059
            this.action_error = false;
1060
            this.trigger("action:fail:reset", this.action_error);
1061
        },
1062

    
1063
        handle_action_fail: function() {
1064
            this.action_error = arguments;
1065
            this.trigger("action:fail", arguments);
1066
        },
1067

    
1068
        get_action_url: function(name) {
1069
            return this.url() + "/action";
1070
        },
1071

    
1072
        get_connection_info: function(host_os, success, error) {
1073
            var url = "/machines/connect";
1074
            params = {
1075
                ip_address: this.get_addresses().ip4,
1076
                os: this.get_os(),
1077
                host_os: host_os,
1078
                srv: this.id
1079
            }
1080

    
1081
            url = url + "?" + $.param(params);
1082

    
1083
            var ajax = snf.api.sync("read", undefined, { url: url, 
1084
                                                         error:error, 
1085
                                                         success:success, 
1086
                                                         handles_error:1});
1087
        }
1088
    })
1089
    
1090
    models.VM.ACTIONS = [
1091
        'start',
1092
        'shutdown',
1093
        'reboot',
1094
        'console',
1095
        'destroy'
1096
    ]
1097

    
1098
    models.VM.AVAILABLE_ACTIONS = {
1099
        'UNKNWON'       : ['destroy'],
1100
        'BUILD'         : ['destroy'],
1101
        'REBOOT'        : ['shutdown', 'destroy', 'console'],
1102
        'STOPPED'       : ['start', 'destroy'],
1103
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1104
        'ERROR'         : ['destroy'],
1105
        'DELETED'        : [],
1106
        'DESTROY'       : [],
1107
        'BUILD_INIT'    : ['destroy'],
1108
        'BUILD_COPY'    : ['destroy'],
1109
        'BUILD_FINAL'   : ['destroy'],
1110
        'SHUTDOWN'      : ['destroy'],
1111
        'START'         : [],
1112
        'CONNECT'       : [],
1113
        'DISCONNECT'    : []
1114
    }
1115

    
1116
    // api status values
1117
    models.VM.STATUSES = [
1118
        'UNKNWON',
1119
        'BUILD',
1120
        'REBOOT',
1121
        'STOPPED',
1122
        'ACTIVE',
1123
        'ERROR',
1124
        'DELETED'
1125
    ]
1126

    
1127
    // api status values
1128
    models.VM.CONNECT_STATES = [
1129
        'ACTIVE',
1130
        'REBOOT',
1131
        'SHUTDOWN'
1132
    ]
1133

    
1134
    // vm states
1135
    models.VM.STATES = models.VM.STATUSES.concat([
1136
        'DESTROY',
1137
        'BUILD_INIT',
1138
        'BUILD_COPY',
1139
        'BUILD_FINAL',
1140
        'SHUTDOWN',
1141
        'START',
1142
        'CONNECT',
1143
        'DISCONNECT',
1144
        'FIREWALL'
1145
    ]);
1146
    
1147
    models.VM.STATES_TRANSITIONS = {
1148
        'DESTROY' : ['DELETED'],
1149
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1150
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1151
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1152
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1153
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1154
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1155
        'BUILD_COPY': ['ERROR', 'ACTIVE', 'BUILD_FINAL', 'DESTROY'],
1156
        'BUILD_FINAL': ['ERROR', 'ACTIVE', 'DESTROY'],
1157
        'BUILD_INIT': ['ERROR', 'ACTIVE', 'BUILD_COPY', 'BUILD_FINAL', 'DESTROY']
1158
    }
1159

    
1160
    models.VM.TRANSITION_STATES = [
1161
        'DESTROY',
1162
        'SHUTDOWN',
1163
        'START',
1164
        'REBOOT',
1165
        'BUILD'
1166
    ]
1167

    
1168
    models.VM.ACTIVE_STATES = [
1169
        'BUILD', 'REBOOT', 'ACTIVE',
1170
        'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL',
1171
        'SHUTDOWN', 'CONNECT', 'DISCONNECT', 'DESTROY'
1172
    ]
1173

    
1174
    models.VM.BUILDING_STATES = [
1175
        'BUILD', 'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL'
1176
    ]
1177

    
1178
    models.Networks = models.Collection.extend({
1179
        model: models.Network,
1180
        path: 'networks',
1181
        details: true,
1182
        //noUpdate: true,
1183
        defaults: {'linked_to':[]},
1184

    
1185
        parse: function (resp, xhr) {
1186
            // FIXME: depricated global var
1187
            if (!resp) { return []};
1188
               
1189
            var data = _.map(resp.networks.values, _.bind(this.parse_net_api_data, this));
1190
            return data;
1191
        },
1192

    
1193
        parse_net_api_data: function(data) {
1194
            if (data.servers && data.servers.values) {
1195
                data['linked_to'] = data.servers.values;
1196
            }
1197
            return data;
1198
        },
1199

    
1200
        create: function (name, callback) {
1201
            return this.api.call(this.path, "create", {network:{name:name}}, callback);
1202
        }
1203
    })
1204

    
1205
    models.Images = models.Collection.extend({
1206
        model: models.Image,
1207
        path: 'images',
1208
        details: true,
1209
        noUpdate: true,
1210
        
1211
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1212

    
1213
        // update collection model with id passed
1214
        // making a direct call to the flavor
1215
        // api url
1216
        update_unknown_id: function(id) {
1217
            var url = getUrl.call(this) + "/" + id;
1218
            this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, 
1219
            _.bind(function() {
1220
                this.add({id:id, name:"Unknown image", size:-1, progress:100, status:"DELETED"})
1221
            }, this), _.bind(function(image) {
1222
                this.add(image.image);
1223
            }, this));
1224
        },
1225

    
1226
        parse: function (resp, xhr) {
1227
            // FIXME: depricated global var
1228
            var data = _.map(resp.images.values, _.bind(this.parse_meta, this));
1229
            return resp.images.values;
1230
        },
1231

    
1232
        get_meta_key: function(img, key) {
1233
            if (img.metadata && img.metadata.values && img.metadata.values[key]) {
1234
                return img.metadata.values[key];
1235
            }
1236
            return undefined;
1237
        },
1238

    
1239
        parse_meta: function(img) {
1240
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1241
                img[key] = this.get_meta_key(img, key);
1242
            }, this));
1243
            return img;
1244
        },
1245

    
1246
        active: function() {
1247
            return this.filter(function(img){return img.get('status') != "DELETED"});
1248
        }
1249
    })
1250

    
1251
    models.Flavors = models.Collection.extend({
1252
        model: models.Flavor,
1253
        path: 'flavors',
1254
        details: true,
1255
        noUpdate: true,
1256
        
1257
        // update collection model with id passed
1258
        // making a direct call to the flavor
1259
        // api url
1260
        update_unknown_id: function(id) {
1261
            var url = getUrl.call(this) + "/" + id;
1262
            this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, 
1263
            _.bind(function() {
1264
                this.add({id:id, cpu:"", ram:"", disk:"", name: "", status:"DELETED"})
1265
            }, this), _.bind(function(flv) {
1266
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1267
                this.add(flv.flavor);
1268
            }, this));
1269
        },
1270

    
1271
        parse: function (resp, xhr) {
1272
            // FIXME: depricated global var
1273
            return resp.flavors.values;
1274
        },
1275

    
1276
        unavailable_values_for_image: function(img, flavors) {
1277
            var flavors = flavors || this.active();
1278
            var size = img.get_size();
1279
            
1280
            var index = {cpu:[], disk:[], ram:[]};
1281

    
1282
            _.each(this.active(), function(el) {
1283
                var img_size = size;
1284
                var flv_size = el.get_disk_size();
1285
                if (flv_size < img_size) {
1286
                    if (index.disk.indexOf(flv_size) == -1) {
1287
                        index.disk.push(flv_size);
1288
                    }
1289
                };
1290
            });
1291
            
1292
            return index;
1293
        },
1294

    
1295
        get_flavor: function(cpu, mem, disk, filter_list) {
1296
            if (!filter_list) { filter_list = this.models };
1297

    
1298
            return this.select(function(flv){
1299
                if (flv.get("cpu") == cpu + "" &&
1300
                   flv.get("ram") == mem + "" &&
1301
                   flv.get("disk") == disk + "" &&
1302
                   filter_list.indexOf(flv) > -1) { return true; }
1303
            })[0];
1304
        },
1305
        
1306
        get_data: function(lst) {
1307
            var data = {'cpu': [], 'mem':[], 'disk':[]};
1308

    
1309
            _.each(lst, function(flv) {
1310
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1311
                    data.cpu.push(flv.get("cpu"));
1312
                }
1313
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1314
                    data.mem.push(flv.get("ram"));
1315
                }
1316
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1317
                    data.disk.push(flv.get("disk"));
1318
                }
1319
            })
1320
            
1321
            return data;
1322
        },
1323

    
1324
        active: function() {
1325
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1326
        }
1327
            
1328
    })
1329

    
1330
    models.VMS = models.Collection.extend({
1331
        model: models.VM,
1332
        path: 'servers',
1333
        details: true,
1334
        copy_image_meta: true,
1335
        
1336
        parse: function (resp, xhr) {
1337
            // FIXME: depricated after refactoring
1338
            var data = resp;
1339
            if (!resp) { return [] };
1340
            data = _.filter(_.map(resp.servers.values, _.bind(this.parse_vm_api_data, this)), function(v){return v});
1341
            return data;
1342
        },
1343
        
1344
        get_reboot_required: function() {
1345
            return this.filter(function(vm){return vm.get("reboot_required") == true})
1346
        },
1347

    
1348
        has_pending_actions: function() {
1349
            return this.filter(function(vm){return vm.pending_action}).length > 0;
1350
        },
1351

    
1352
        reset_pending_actions: function() {
1353
            this.each(function(vm) {
1354
                vm.clear_pending_action();
1355
            })
1356
        },
1357

    
1358
        stop_stats_update: function() {
1359
            this.each(function(vm) {
1360
                vm.do_update_stats = false;
1361
            })
1362
        },
1363
        
1364
        has_meta: function(vm_data) {
1365
            return vm_data.metadata && vm_data.metadata.values
1366
        },
1367

    
1368
        has_addresses: function(vm_data) {
1369
            return vm_data.metadata && vm_data.metadata.values
1370
        },
1371

    
1372
        parse_vm_api_data: function(data) {
1373
            // do not add non existing DELETED entries
1374
            if (data.status && data.status == "DELETED") {
1375
                if (!this.get(data.id)) {
1376
                    console.error("non exising deleted vm", data)
1377
                    return false;
1378
                }
1379
            }
1380

    
1381
            // OS attribute
1382
            if (this.has_meta(data)) {
1383
                data['OS'] = data.metadata.values.OS || "undefined";
1384
            }
1385
            
1386
            data['firewalls'] = {};
1387
            if (data['addresses'] && data['addresses'].values) {
1388
                data['linked_to_nets'] = data['addresses'].values;
1389
                _.each(data['addresses'].values, function(f){
1390
                    if (f['firewallProfile']) {
1391
                        data['firewalls'][f['id']] = f['firewallProfile']
1392
                    }
1393
                });
1394
            }
1395
            
1396
            // if vm has no metadata, no metadata object
1397
            // is in json response, reset it to force
1398
            // value update
1399
            if (!data['metadata']) {
1400
                data['metadata'] = {values:{}};
1401
            }
1402

    
1403
            return data;
1404
        },
1405

    
1406
        create: function (name, image, flavor, meta, extra, callback) {
1407
            if (this.copy_image_meta) {
1408
                meta['OS'] = image.get("OS");
1409
           }
1410
            
1411
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta}
1412
            opts = _.extend(opts, extra);
1413

    
1414
            this.api.call(this.path, "create", {'server': opts}, undefined, undefined, callback, {critical: false});
1415
        }
1416

    
1417
    })
1418
    
1419

    
1420
    // storage initialization
1421
    snf.storage.images = new models.Images();
1422
    snf.storage.flavors = new models.Flavors();
1423
    snf.storage.networks = new models.Networks();
1424
    snf.storage.vms = new models.VMS();
1425

    
1426
    //snf.storage.vms.fetch({update:true});
1427
    //snf.storage.images.fetch({update:true});
1428
    //snf.storage.flavors.fetch({update:true});
1429

    
1430
})(this);