Statistics
| Branch: | Tag: | Revision:

root / ui / static / snf / js / models.js @ 6a3a5bf7

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

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

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

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

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

    
80
    })
81
    
82
    // Base object for all our model collections
83
    models.Collection = bb.Collection.extend({
84
        sync: snf.api.sync,
85
        api: snf.api,
86

    
87
        url: function(options) {
88
            return getUrl.call(this) + (options.details || this.details ? '/detail' : '');
89
        },
90

    
91
        fetch: function(options) {
92
            // default to update
93
            if (!this.noUpdate) {
94
                if (!options) { options = {} };
95
                if (options.update === undefined) { options.update = true };
96
                if (!options.removeMissing && options.refresh) { options.removeMissing = true };
97
            }
98
            // custom event foreach fetch
99
            return bb.Collection.prototype.fetch.call(this, options)
100
        },
101

    
102
        get_fetcher: function(timeout, fast, limit, initial, params) {
103
            var fetch_params = params;
104
            var timeout = parseInt(timeout);
105
            var fast = fast || 1000;
106
            var limit = limit;
107
            var initial_call = initial || true;
108
            
109
            var last_ajax = undefined;
110
            var cb = _.bind(function(){
111
                updater._ajax = last_ajax;
112
                if (last_ajax) {
113
                    last_ajax.abort();
114
                }
115
                last_ajax = this.fetch(fetch_params);
116
            }, this);
117
            var updater = new snf.api.updateHandler({'callback': cb, timeout:timeout, 
118
                                                    fast:fast, limit:limit, 
119
                                                    call_on_start:initial_call});
120

    
121
            snf.api.bind("call", _.bind(function(){ updater.faster()}, this));
122
            return updater;
123
        }
124
    });
125
    
126
    // Image model
127
    models.Image = models.Model.extend({
128
        path: 'images',
129

    
130
        get_size: function() {
131
            return parseInt(this.get('metadata') ? this.get('metadata').values.size : -1)
132
        },
133

    
134
        get_os: function() {
135
            return this.get("OS");
136
        }
137
    });
138

    
139
    // Flavor model
140
    models.Flavor = models.Model.extend({
141
        path: 'flavors',
142

    
143
        details_string: function() {
144
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
145
        },
146

    
147
        get_disk_size: function() {
148
            return parseInt(this.get("disk") * 1000)
149
        },
150

    
151
    });
152
    
153
    //network vms list helper
154
    var NetworkVMSList = function() {
155
        this.initialize = function() {
156
            this.vms = [];
157
            this.pending = [];
158
            this.pending_for_removal = [];
159
        }
160
        
161
        this.add_pending_for_remove = function(vm_id) {
162
            if (this.pending_for_removal.indexOf(vm_id) == -1) {
163
                this.pending_for_removal.push(vm_id);
164
            }
165

    
166
            if (this.pending_for_removal.length) {
167
                this.trigger("pending:remove:add");
168
            }
169
        },
170

    
171
        this.add_pending = function(vm_id) {
172
            if (this.pending.indexOf(vm_id) == -1) {
173
                this.pending[this.pending.length] = vm_id;
174
            }
175

    
176
            if (this.pending.length) {
177
                this.trigger("pending:add");
178
            }
179
        }
180

    
181
        this.check_pending = function() {
182
            var len = this.pending.length;
183
            var args = [this.pending];
184
            this.pending = _.difference(this.pending, this.vms);
185
            if (len != this.pending.length) {
186
                if (this.pending.length == 0) {
187
                    this.trigger("pending:clear");
188
                }
189
            }
190

    
191
            var len = this.pending_for_removal.length;
192
            this.pending_for_removal = _.intersection(this.pending_for_removal, this.vms);
193
            if (this.pending_for_removal.length == 0) {
194
                this.trigger("pending:remove:clear");
195
            }
196

    
197
        }
198

    
199

    
200
        this.add = function(vm_id) {
201
            if (this.vms.indexOf(vm_id) == -1) {
202
                this.vms[this.vms.length] = vm_id;
203
                this.trigger("network:connect", vm_id);
204
                this.check_pending();
205
                return true;
206
            }
207
        }
208

    
209
        this.remove = function(vm_id) {
210
            if (this.vms.indexOf(vm_id) > -1) {
211
                this.vms = _.without(this.vms, vm_id);
212
                this.trigger("network:disconnect", vm_id);
213
                this.check_pending();
214
                return true;
215
            }
216
        }
217

    
218
        this.get = function() {
219
            return this.vms;
220
        }
221

    
222
        this.list = function() {
223
            return storage.vms.filter(_.bind(function(vm){
224
                return this.vms.indexOf(vm.id) > -1;
225
            }, this))
226
        }
227

    
228
        this.initialize();
229
    };
230
    _.extend(NetworkVMSList.prototype, bb.Events);
231
    
232
    // vm networks list helper
233
    var VMNetworksList = function() {
234
        this.initialize = function() {
235
            this.networks = {};
236
            this.network_ids = [];
237
        }
238

    
239
        this.add = function(net_id, data) {
240
            if (!this.networks[net_id]) {
241
                this.networks[net_id] = data || {};
242
                this.network_ids[this.network_ids.length] = net_id;
243
                this.trigger("network:connect", net_id);
244
                return true;
245
            }
246
        }
247

    
248
        this.remove = function(net_id) {
249
            if (this.networks[net_id]) {
250
                delete this.networks[net_id];
251
                this.network_ids = _.without(this.network_ids, net_id);
252
                this.trigger("network:disconnect", net_id);
253
                return true;
254
            }
255
            return false;
256
        }
257

    
258
        this.get = function() {
259
            return this.networks;
260
        }
261

    
262
        this.list = function() {
263
            return storage.networks.filter(_.bind(function(net){
264
                return this.network_ids.indexOf(net.id) > -1;
265
            }, this))
266
        }
267

    
268
        this.initialize();
269
    };
270
    _.extend(VMNetworksList.prototype, bb.Events);
271

    
272
    // Image model
273
    models.Network = models.Model.extend({
274
        path: 'networks',
275
        has_status: true,
276
        
277
        initialize: function() {
278
            this.vms = new NetworkVMSList();
279
            this.vms.bind("pending:add", _.bind(this.handle_pending_connections, this, "add"));
280
            this.vms.bind("pending:clear", _.bind(this.handle_pending_connections, this, "clear"));
281
            this.vms.bind("pending:remove:add", _.bind(this.handle_pending_connections, this, "add"));
282
            this.vms.bind("pending:remove:clear", _.bind(this.handle_pending_connections, this, "clear"));
283

    
284
            ret = models.Network.__super__.initialize.apply(this, arguments);
285

    
286
            storage.vms.bind("change:linked_to_nets", _.bind(this.update_connections, this, "vm:change"));
287
            storage.vms.bind("add", _.bind(this.update_connections, this, "add"));
288
            storage.vms.bind("remove", _.bind(this.update_connections, this, "remove"));
289
            storage.vms.bind("reset", _.bind(this.update_connections, this, "reset"));
290
            this.bind("change:linked_to", _.bind(this.update_connections, this, "net:change"));
291
            this.update_connections();
292
            this.update_state();
293
            return ret;
294
        },
295

    
296
        update_state: function() {
297
            if (this.vms.pending.length) {
298
                this.set({state: "CONNECTING"});
299
                return
300
            }
301
            if (this.vms.pending_for_removal.length) {
302
                this.set({state: "DISCONNECTING"});
303
                return
304
            }   
305
            
306
            var firewalling = false;
307
            _.each(this.vms.get(), _.bind(function(vm_id){
308
                var vm = storage.vms.get(vm_id);
309
                if (!vm) { return };
310
                if (!_.isEmpty(vm.pending_firewalls)) {
311
                    this.set({state:"FIREWALLING"});
312
                    firewalling = true;
313
                    return false;
314
                }
315
            },this));
316
            if (firewalling) { return };
317

    
318
            this.set({state:"NORMAL"});
319
        },
320

    
321
        handle_pending_connections: function(action) {
322
            this.update_state();
323
        },
324

    
325
        // handle vm/network connections
326
        update_connections: function(action, model) {
327
            
328
            // vm removed disconnect vm from network
329
            if (action == "remove") {
330
                var removed_from_net = this.vms.remove(model.id);
331
                var removed_from_vm = model.networks.remove(this.id);
332
                if (removed_from_net) {this.trigger("vm:disconnect", model, this); this.change()};
333
                if (removed_from_vm) {vm.trigger("network:disconnect", this, model); this.change()};
334
                return;
335
            }
336
            
337
            // update links for all vms
338
            var links = this.get("linked_to");
339
            storage.vms.each(_.bind(function(vm) {
340
                var vm_links = vm.get("linked_to") || [];
341
                if (vm_links.indexOf(this.id) > -1) {
342
                    // vm has connection to current network
343
                    if (links.indexOf(vm.id) > -1) {
344
                        // and network has connection to vm, so try
345
                        // to append it
346
                        var add_to_net = this.vms.add(vm.id);
347
                        var index = _.indexOf(vm_links, this.id);
348
                        var add_to_vm = vm.networks.add(this.id, vm.get("linked_to_nets")[index]);
349
                        
350
                        // call only if connection did not existed
351
                        if (add_to_net) {this.trigger("vm:connect", vm, this); this.change()};
352
                        if (add_to_vm) {vm.trigger("network:connect", this, vm); vm.change()};
353
                    } else {
354
                        // no connection, try to remove it
355
                        var removed_from_net = this.vms.remove(vm.id);
356
                        var removed_from_vm = vm.networks.remove(this.id);
357
                        if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
358
                        if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
359
                    }
360
                } else {
361
                    // vm has no connection to current network, try to remove it
362
                    var removed_from_net = this.vms.remove(vm.id);
363
                    var removed_from_vm = vm.networks.remove(this.id);
364
                    if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
365
                    if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
366
                }
367
            },this));
368
        },
369

    
370
        is_public: function() {
371
            return this.id == "public";
372
        },
373

    
374
        contains_vm: function(vm) {
375
            var net_vm_exists = this.vms.get().indexOf(vm.id) > -1;
376
            var vm_net_exists = vm.is_connected_to(this);
377
            return net_vm_exists && vm_net_exists;
378
        },
379

    
380
        add_vm: function (vm, callback) {
381
            return this.api.call(this.api_path() + "/action", "create", 
382
                                 {add:{serverRef:"" + vm.id}},
383
                                 _.bind(function(){
384
                                     this.vms.add_pending(vm.id);
385
                                     if (callback) {callback()}
386
                                 },this));
387
        },
388

    
389
        remove_vm: function (vm, callback) {
390
            return this.api.call(this.api_path() + "/action", "create", 
391
                                 {remove:{serverRef:"" + vm.id}},
392
                                 _.bind(function(){
393
                                     this.vms.add_pending_for_remove(vm.id);
394
                                     if (callback) {callback()}
395
                                 },this));
396
        },
397

    
398
        rename: function(name, callback) {
399
            return this.api.call(this.api_path(), "update", {network:{name:name}}, callback);
400
        },
401

    
402
        get_connectable_vms: function() {
403
            var servers = this.vms.list();
404
            return storage.vms.filter(function(vm){return servers.indexOf(vm.id) == -1})
405
        },
406

    
407
        state_message: function() {
408
            if (this.get("state") == "NORMAL" && this.is_public()) {
409
                return "Public network";
410
            }
411

    
412
            return models.Network.STATES[this.get("state")];
413
        },
414

    
415
        in_progress: function() {
416
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
417
        }
418
    });
419
    
420
    models.Network.STATES = {
421
        'NORMAL': 'Private network',
422
        'CONNECTING': 'Connecting...',
423
        'DISCONNECTING': 'Disconnecting...',
424
        'FIREWALLING': 'Firewall update...'
425
    }
426

    
427
    models.Network.STATES_TRANSITIONS = {
428
        'CONNECTING': ['NORMAL'],
429
        'DISCONNECTING': ['NORMAL'],
430
        'FIREWALLING': ['NORMAL']
431
    }
432

    
433
    // Virtualmachine model
434
    models.VM = models.Model.extend({
435

    
436
        path: 'servers',
437
        has_status: true,
438
        initialize: function(params) {
439
            this.networks = new VMNetworksList();
440
            
441
            this.pending_firewalls = {};
442
            
443
            models.VM.__super__.initialize.apply(this, arguments);
444

    
445
            this.set({state: params.status || "ERROR"});
446
            this.log = new snf.logging.logger("VM " + this.id);
447
            this.pending_action = undefined;
448
            
449
            // init stats parameter
450
            this.set({'stats': undefined}, {silent: true});
451
            // defaults to not update the stats
452
            // each view should handle this vm attribute 
453
            // depending on if it displays stat images or not
454
            this.do_update_stats = false;
455
            
456
            // interval time
457
            // this will dynamicaly change if the server responds that
458
            // images get refreshed on different intervals
459
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
460

    
461
            // initialize interval
462
            this.init_stats_intervals(this.stats_update_interval);
463
            
464
            // clear stats intervals on update
465
            this.bind("remove", _.bind(function(){
466
                try {
467
                    window.clearInterval(this.stats_interval);
468
                } catch (err) {};
469
            }, this));
470
            
471
            this.bind("change:progress", _.bind(this.update_building_progress, this));
472
            this.update_building_progress();
473

    
474
            this.bind("change:firewalls", _.bind(this.handle_firewall_change, this));
475
            
476
            // default values
477
            this.set({linked_to_nets:this.get("linked_to_nets") || []});
478
            this.set({firewalls:this.get("firewalls") || []});
479

    
480
            this.action_error = false;
481
        },
482

    
483
        handle_firewall_change: function() {
484

    
485
        },
486
        
487
        set_linked_to_nets: function(data) {
488
            this.set({"linked_to":_.map(data, function(n){ return n.id})});
489
            return data;
490
        },
491

    
492
        is_connected_to: function(net) {
493
            return _.filter(this.networks.list(), function(n){return n.id == net.id}).length > 0;
494
        },
495
        
496
        status: function(st) {
497
            if (!st) { return this.get("status")}
498
            return this.set({status:st});
499
        },
500

    
501
        set_status: function(st) {
502
            var new_state = this.state_for_api_status(st);
503
            var transition = false;
504

    
505
            if (this.state() != new_state) {
506
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
507
                    transition = this.state();
508
                }
509
            }
510
            
511
            // call it silently to avoid double change trigger
512
            this.set({'state': this.state_for_api_status(st)}, {silent: true});
513
            
514
            // trigger transition
515
            if (transition) { this.trigger("transition", {from:transition, to:new_state}) };
516
            return st;
517
        },
518

    
519
        update_building_progress: function() {
520
            if (this.is_building()) {
521
                var progress = this.get("progress");
522
                if (progress == 0) {
523
                    this.state("BUILD_INIT");
524
                    this.set({progress_message: BUILDING_MESSAGES['INIT']});
525
                }
526
                if (progress > 0 && progress < 99) {
527
                    this.state("BUILD_COPY");
528
                    var params = this.get_copy_details(true);
529
                    this.set({progress_message: BUILDING_MESSAGES['COPY'].format(params.copy, 
530
                                                                                 params.size, 
531
                                                                                 params.progress)});
532
                }
533
                if (progress == 100) {
534
                    this.state("BUILD_FINAL");
535
                    this.set({progress_message: BUILDING_MESSAGES['FINAL']});
536
                }
537
            } else {
538
            }
539
        },
540

    
541
        get_copy_details: function(human, image) {
542
            var human = human || false;
543
            var image = image || this.get_image();
544

    
545
            var progress = this.get('progress');
546
            var size = image.get_size();
547
            var size_copied = (size * progress / 100).toFixed(2);
548
            
549
            if (human) {
550
                size = util.readablizeBytes(size*1024*1024);
551
                size_copied = util.readablizeBytes(size_copied*1024*1024);
552
            }
553
            return {'progress': progress, 'size': size, 'copy': size_copied};
554
        },
555

    
556
        // clear and reinitialize update interval
557
        init_stats_intervals: function (interval) {
558
            try {
559
                window.clearInterval(this.stats_interval);
560
            } catch (err){}
561
            //this.stats_interval = window.setInterval(_.bind(this.update_stats, this), interval);
562
            //this.update_stats(true);
563
        },
564
        
565
        // do the api call
566
        update_stats: function(force) {
567
            // do not update stats if flag not set
568
            if (!this.do_update_stats && !force) {
569
                return;
570
            }
571
            
572
            // make the api call, execute handle_stats_update on sucess
573
            // TODO: onError handler ???
574
            stats_url = this.url() + "/stats";
575
            this.sync("GET", this, {url: stats_url, refresh:true, success: _.bind(this.handle_stats_update, this)});
576
        },
577
        
578
        // this method gets executed after a successful vm stats api call
579
        handle_stats_update: function(data) {
580
            // avoid browser caching
581
            if (data.stats && _.size(data.stats) > 0) {
582
                var ts = $.now();
583
                var stats = data.stats;
584
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
585
                    stats[k] = stats[k] + "?_=" + ts;
586
                })
587
                data.stats = stats;
588
            }
589
            this.set({'stats': data.stats}, {silent:true});
590
            // trigger the event
591
            this.trigger("stats:update");
592
    
593
            // do we need to change the interval ??
594
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
595
                this.stats_update_interval = data.stats.refresh * 1000;
596
                this.init_stats_intervals(this.stats_update_interval);
597
            }
598
        },
599
        
600
        // helper method that sets the do_update_stats
601
        // in the future this method could also make an api call
602
        // immediaetly if needed
603
        enable_stats_update: function() {
604
            this.do_update_stats = true;
605
        },
606

    
607
        require_reboot: function() {
608
            if (this.is_active()) {
609
                this.set({'reboot_required': true});
610
            }
611
        },
612
        
613
        set_pending_action: function(data) {
614
            this.pending_action = data;
615
            return data;
616
        },
617

    
618
        // machine has pending action
619
        update_pending_action: function(action, force) {
620
            this.set({pending_action: action});
621
        },
622

    
623
        clear_pending_action: function() {
624
            this.set({pending_action: undefined});
625
        },
626

    
627
        has_pending_action: function() {
628
            return this.get("pending_action") ? this.get("pending_action") : false;
629
        },
630
        
631
        // machine is active
632
        is_active: function() {
633
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
634
        },
635
        
636
        // machine is building 
637
        is_building: function() {
638
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
639
        },
640

    
641
        // user can connect to machine
642
        is_connectable: function() {
643
            // check if ips exist
644
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
645
                return false;
646
            }
647
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
648
        },
649
        
650
        set_firewalls: function(data) {
651
            _.each(data, _.bind(function(val, key){
652
                if (this.pending_firewalls && this.pending_firewalls[key] && this.pending_firewalls[key] == val) {
653
                        this.require_reboot();
654
                        this.remove_pending_firewall(key, val);
655
                }
656
            }, this));
657
            return data;
658
        },
659

    
660
        remove_pending_firewall: function(net_id, value) {
661
            if (this.pending_firewalls[net_id] == value) {
662
                delete this.pending_firewalls[net_id];
663
                storage.networks.get(net_id).update_state();
664
            }
665
        },
666
            
667
        remove_meta: function(key, complete, error) {
668
            var url = this.api_path() + "/meta/" + key;
669
            this.api.call(url, "delete", undefined, complete, error);
670
        },
671

    
672
        save_meta: function(meta, complete, error) {
673
            var url = this.api_path() + "/meta/" + meta.key;
674
            var payload = {meta:{}};
675
            payload.meta[meta.key] = meta.value;
676

    
677
            this.api.call(url, "update", payload, complete, error)
678
        },
679

    
680
        set_firewall: function(net_id, value, callback) {
681
            if (this.get("firewalls") && this.get("firewalls")[net_id] == value) { return }
682

    
683
            this.pending_firewalls[net_id] = value;
684
            this.trigger("change", this, this);
685
            this.api.call(this.api_path() + "/action", "create", {"firewallProfile":{"profile":value}}, callback);
686
            storage.networks.get(net_id).update_state();
687
        },
688

    
689
        firewall_pending: function(net_id) {
690
            return this.pending_firewalls[net_id] != undefined;
691
        },
692
        
693
        // update/get the state of the machine
694
        state: function() {
695
            var args = slice.call(arguments);
696
                
697
            // TODO: it might not be a good idea to let api set the state
698
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
699
                this.set({'state': args[0]});
700
            }
701

    
702
            return this.get('state');
703
        },
704
        
705
        // get the state that the api status corresponds to
706
        state_for_api_status: function(status) {
707
            return this.state_transition(this.state(), status);
708
        },
709
        
710
        // vm state equals vm api status
711
        state_is_status: function(state) {
712
            return models.VM.STATUSES.indexOf(state) != -1;
713
        },
714
        
715
        // get transition state for the corresponging api status
716
        state_transition: function(state, new_status) {
717
            var statuses = models.VM.STATES_TRANSITIONS[state];
718
            if (statuses) {
719
                if (statuses.indexOf(new_status) > -1) {
720
                    return new_status;
721
                } else {
722
                    return state;
723
                }
724
            } else {
725
                return new_status;
726
            }
727
        },
728
        
729
        // the current vm state is a transition state
730
        in_transition: function() {
731
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
732
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
733
        },
734
        
735
        // get image object
736
        // TODO: update images synchronously if image not found
737
        get_image: function() {
738
            var image = storage.images.get(this.get('imageRef'));
739
            if (!image) {
740
                storage.images.update_unknown_id(this.get('imageRef'));
741
                image = storage.flavors.get(this.get('imageRef'));
742
            }
743
            return image;
744
        },
745
        
746
        // get flavor object
747
        // TODO: update flavors synchronously if image not found
748
        get_flavor: function() {
749
            var flv = storage.flavors.get(this.get('flavorRef'));
750
            if (!flv) {
751
                storage.flavors.update_unknown_id(this.get('flavorRef'));
752
                flv = storage.flavors.get(this.get('flavorRef'));
753
            }
754
            return flv;
755
        },
756

    
757
        // retrieve the metadata object
758
        get_meta: function() {
759
            //return {
760
                //'OS': 'debian',
761
                //'username': 'vinilios',
762
                //'group': 'webservers',
763
                //'meta2': 'meta value',
764
                //'looooooooooooooooong meta': 'short value',
765
                //'short meta': 'loooooooooooooooooooooooooooooooooong value',
766
                //'21421': 'fdsfds fds',
767
                //'21421': 'fdsfds fds',
768
                //'1fds 21421': 'fdsfds fds',
769
                //'fds 21421': 'fdsfds fds',
770
                //'fge 21421': 'fdsfds fds',
771
                //'21421 rew rew': 'fdsfds fds'
772
            //}
773
            try {
774
                return this.get('metadata').values
775
            } catch (err) {
776
                return {};
777
            }
778
        },
779
        
780
        // get metadata OS value
781
        get_os: function() {
782
            return this.get_meta().OS;
783
        },
784
        
785
        // get public ip addresses
786
        // TODO: public network is always the 0 index ???
787
        get_addresses: function(net_id) {
788
            var net_id = net_id || "public";
789
            
790
            var info = this.get_network_info(net_id);
791
            if (!info) { return {} };
792
            addrs = {};
793
            _.each(info.values, function(addr) {
794
                addrs["ip" + addr.version] = addr.addr;
795
            });
796
            return addrs
797
        },
798

    
799
        get_network_info: function(net_id) {
800
            var net_id = net_id || "public";
801
            
802
            if (!this.networks.network_ids.length) { return {} };
803

    
804
            var addresses = this.networks.get();
805
            try {
806
                return _.select(addresses, function(net, key){return key == net_id })[0];
807
            } catch (err) {
808
                //this.log.debug("Cannot find network {0}".format(net_id))
809
            }
810
        },
811

    
812
        firewall_profile: function(net_id) {
813
            var net_id = net_id || "public";
814
            var firewalls = this.get("firewalls");
815
            return firewalls[net_id];
816
        },
817

    
818
        has_firewall: function(net_id) {
819
            var net_id = net_id || "public";
820
            return ["ENABLED","PROTECTED"].indexOf(this.firewall_profile()) > -1;
821
        },
822
    
823
        // get actions that the user can execute
824
        // depending on the vm state/status
825
        get_available_actions: function() {
826
            return models.VM.AVAILABLE_ACTIONS[this.state()];
827
        },
828

    
829
        set_profile: function(profile, net_id) {
830
        },
831
        
832
        // call rename api
833
        rename: function(new_name) {
834
            //this.set({'name': new_name});
835
            this.sync("update", this, {
836
                data: {
837
                    'server': {
838
                        'name': new_name
839
                    }
840
                }, 
841
                // do the rename after the method succeeds
842
                success: _.bind(function(){
843
                    //this.set({name: new_name});
844
                    snf.api.trigger("call");
845
                }, this)
846
            });
847
        },
848
        
849
        get_console_url: function(data) {
850
            var url_params = {
851
                machine: this.get("name"),
852
                host_ip: this.get_addresses().ip4,
853
                host_ip_v6: this.get_addresses().ip6,
854
                host: data.host,
855
                port: data.port,
856
                password: data.password
857
            }
858
            return '/machines/console?' + $.param(url_params);
859
        },
860

    
861
        // action helper
862
        call: function(action_name, success, error) {
863
            var id_param = [this.id];
864

    
865
            success = success || function() {};
866
            error = error || function() {};
867

    
868
            var self = this;
869

    
870
            switch(action_name) {
871
                case 'start':
872
                    this.__make_api_call(this.get_action_url(), // vm actions url
873
                                         "create", // create so that sync later uses POST to make the call
874
                                         {start:{}}, // payload
875
                                         function() {
876
                                             // set state after successful call
877
                                             self.state("START"); 
878
                                             success.apply(this, arguments);
879
                                             snf.api.trigger("call");
880
                                         },  
881
                                         error, 'start');
882
                    break;
883
                case 'reboot':
884
                    this.__make_api_call(this.get_action_url(), // vm actions url
885
                                         "create", // create so that sync later uses POST to make the call
886
                                         {reboot:{type:"HARD"}}, // payload
887
                                         function() {
888
                                             // set state after successful call
889
                                             self.state("REBOOT"); 
890
                                             success.apply(this, arguments)
891
                                             snf.api.trigger("call");
892
                                             self.set({'reboot_required': false});
893
                                         },
894
                                         error, 'reboot');
895
                    break;
896
                case 'shutdown':
897
                    this.__make_api_call(this.get_action_url(), // vm actions url
898
                                         "create", // create so that sync later uses POST to make the call
899
                                         {shutdown:{}}, // payload
900
                                         function() {
901
                                             // set state after successful call
902
                                             self.state("SHUTDOWN"); 
903
                                             success.apply(this, arguments)
904
                                             snf.api.trigger("call");
905
                                         },  
906
                                         error, 'shutdown');
907
                    break;
908
                case 'console':
909
                    this.__make_api_call(this.url() + "/action", "create", {'console': {'type':'vnc'}}, function(data) {
910
                        var cons_data = data.console;
911
                        success.apply(this, [cons_data]);
912
                    }, undefined, 'console')
913
                    break;
914
                case 'destroy':
915
                    this.__make_api_call(this.url(), // vm actions url
916
                                         "delete", // create so that sync later uses POST to make the call
917
                                         undefined, // payload
918
                                         function() {
919
                                             // set state after successful call
920
                                             self.state('DESTROY');
921
                                             success.apply(this, arguments)
922
                                         },  
923
                                         error, 'destroy');
924
                    break;
925
                default:
926
                    throw "Invalid VM action ("+action_name+")";
927
            }
928
        },
929
        
930
        __make_api_call: function(url, method, data, success, error, action) {
931
            var self = this;
932
            error = error || function(){};
933
            success = success || function(){};
934

    
935
            var params = {
936
                url: url,
937
                data: data,
938
                success: function(){ self.handle_action_succeed.apply(self, arguments); success.apply(this, arguments)},
939
                error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)},
940
                error_params: { ns: "Machines actions", 
941
                                message: "'" + this.get("name") + "'" + " action failed", 
942
                                extra_details: { 'Machine ID': this.id, 'URL': url, 'Action': action || "undefined" },
943
                                allow_reload: false
944
                              },
945
                handles_error: true
946
            }
947
            this.sync(method, this, params);
948
        },
949

    
950
        handle_action_succeed: function() {
951
            this.trigger("action:success", arguments);
952
        },
953
        
954
        reset_action_error: function() {
955
            this.action_error = false;
956
            this.trigger("action:fail:reset", this.action_error);
957
        },
958

    
959
        handle_action_fail: function() {
960
            this.action_error = arguments;
961
            this.trigger("action:fail", arguments);
962
        },
963

    
964
        get_action_url: function(name) {
965
            return this.url() + "/action";
966
        },
967

    
968
        get_connection_info: function(host_os, success, error) {
969
            var url = "/machines/connect";
970
            params = {
971
                ip_address: this.get_addresses().ip4,
972
                os: this.get_os(),
973
                host_os: host_os,
974
                srv: this.id
975
            }
976

    
977
            url = url + "?" + $.param(params);
978

    
979
            var ajax = snf.api.sync("read", undefined, { url: url, 
980
                                                         error:error, 
981
                                                         success:success, 
982
                                                         handles_error:1});
983
        }
984
    })
985
    
986
    models.VM.ACTIONS = [
987
        'start',
988
        'shutdown',
989
        'reboot',
990
        'console',
991
        'destroy'
992
    ]
993

    
994
    models.VM.AVAILABLE_ACTIONS = {
995
        'UNKNWON'       : ['destroy'],
996
        'BUILD'         : ['destroy'],
997
        'REBOOT'        : ['shutdown', 'destroy', 'console'],
998
        'STOPPED'       : ['start', 'destroy'],
999
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1000
        'ERROR'         : ['destroy'],
1001
        'DELETE'        : [],
1002
        'DESTROY'       : [],
1003
        'BUILD_INIT'    : ['destroy'],
1004
        'BUILD_COPY'    : ['destroy'],
1005
        'BUILD_FINAL'   : ['destroy'],
1006
        'SHUTDOWN'      : ['destroy'],
1007
        'START'         : [],
1008
        'CONNECT'       : [],
1009
        'DISCONNECT'    : []
1010
    }
1011

    
1012
    // api status values
1013
    models.VM.STATUSES = [
1014
        'UNKNWON',
1015
        'BUILD',
1016
        'REBOOT',
1017
        'STOPPED',
1018
        'ACTIVE',
1019
        'ERROR',
1020
        'DELETE'
1021
    ]
1022

    
1023
    // api status values
1024
    models.VM.CONNECT_STATES = [
1025
        'ACTIVE',
1026
        'REBOOT',
1027
        'SHUTDOWN'
1028
    ]
1029

    
1030
    // vm states
1031
    models.VM.STATES = models.VM.STATUSES.concat([
1032
        'DESTROY',
1033
        'BUILD_INIT',
1034
        'BUILD_COPY',
1035
        'BUILD_FINAL',
1036
        'SHUTDOWN',
1037
        'START',
1038
        'CONNECT',
1039
        'DISCONNECT',
1040
        'FIREWALL'
1041
    ]);
1042
    
1043
    models.VM.STATES_TRANSITIONS = {
1044
        'DESTROY' : ['DELETE'],
1045
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1046
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1047
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1048
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1049
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1050
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1051
        'BUILD_COPY': ['ERROR', 'ACTIVE', 'BUILD_FINAL', 'DESTROY'],
1052
        'BUILD_FINAL': ['ERROR', 'ACTIVE', 'DESTROY'],
1053
        'BUILD_INIT': ['ERROR', 'ACTIVE', 'BUILD_COPY', 'BUILD_FINAL', 'DESTROY']
1054
    }
1055

    
1056
    models.VM.TRANSITION_STATES = [
1057
        'DESTROY',
1058
        'SHUTDOWN',
1059
        'START',
1060
        'REBOOT',
1061
        'BUILD'
1062
    ]
1063

    
1064
    models.VM.ACTIVE_STATES = [
1065
        'BUILD', 'REBOOT', 'ACTIVE',
1066
        'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL',
1067
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1068
    ]
1069

    
1070
    models.VM.BUILDING_STATES = [
1071
        'BUILD', 'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL'
1072
    ]
1073

    
1074
    models.Networks = models.Collection.extend({
1075
        model: models.Network,
1076
        path: 'networks',
1077
        details: true,
1078
        //noUpdate: true,
1079
        defaults: {'linked_to':[]},
1080

    
1081
        parse: function (resp, xhr) {
1082
            // FIXME: depricated global var
1083
            if (!resp) { return []};
1084
               
1085
            var data = _.map(resp.networks.values, _.bind(this.parse_net_api_data, this));
1086
            return data;
1087
        },
1088

    
1089
        parse_net_api_data: function(data) {
1090
            if (data.servers && data.servers.values) {
1091
                data['linked_to'] = data.servers.values;
1092
            }
1093
            return data;
1094
        },
1095

    
1096
        create: function (name, callback) {
1097
            return this.api.call(this.path, "create", {network:{name:name}}, callback);
1098
        }
1099
    })
1100

    
1101
    models.Images = models.Collection.extend({
1102
        model: models.Image,
1103
        path: 'images',
1104
        details: true,
1105
        noUpdate: true,
1106
        
1107
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1108

    
1109
        // update collection model with id passed
1110
        // making a direct call to the flavor
1111
        // api url
1112
        update_unknown_id: function(id) {
1113
            var url = getUrl.call(this) + "/" + id;
1114
            this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, 
1115
            _.bind(function() {
1116
                this.add({id:id, name:"Unknown image", size:-1, progress:100, status:"DELETED"})
1117
            }, this), _.bind(function(image) {
1118
                this.add(image.image);
1119
            }, this));
1120
        },
1121

    
1122
        parse: function (resp, xhr) {
1123
            // FIXME: depricated global var
1124
            var data = _.map(resp.images.values, _.bind(this.parse_meta, this));
1125
            return resp.images.values;
1126
        },
1127

    
1128
        get_meta_key: function(img, key) {
1129
            if (img.metadata && img.metadata.values && img.metadata.values[key]) {
1130
                return img.metadata.values[key];
1131
            }
1132
            return undefined;
1133
        },
1134

    
1135
        parse_meta: function(img) {
1136
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1137
                img[key] = this.get_meta_key(img, key);
1138
            }, this));
1139
            return img;
1140
        },
1141

    
1142
        active: function() {
1143
            return this.filter(function(img){return img.get('status') != "DELETED"});
1144
        }
1145
    })
1146

    
1147
    models.Flavors = models.Collection.extend({
1148
        model: models.Flavor,
1149
        path: 'flavors',
1150
        details: true,
1151
        noUpdate: true,
1152
        
1153
        // update collection model with id passed
1154
        // making a direct call to the flavor
1155
        // api url
1156
        update_unknown_id: function(id) {
1157
            var url = getUrl.call(this) + "/" + id;
1158
            this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, 
1159
            _.bind(function() {
1160
                this.add({id:id, cpu:"", ram:"", disk:"", name: "", status:"DELETED"})
1161
            }, this), _.bind(function(flv) {
1162
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1163
                this.add(flv.flavor);
1164
            }, this));
1165
        },
1166

    
1167
        parse: function (resp, xhr) {
1168
            // FIXME: depricated global var
1169
            return resp.flavors.values;
1170
        },
1171

    
1172
        unavailable_values_for_image: function(img, flavors) {
1173
            var flavors = flavors || this.active();
1174
            var size = img.get_size();
1175
            
1176
            var index = {cpu:[], disk:[], ram:[]};
1177

    
1178
            _.each(this.active(), function(el) {
1179
                var img_size = size;
1180
                var flv_size = el.get_disk_size();
1181
                if (flv_size < img_size) {
1182
                    if (index.disk.indexOf(flv_size) == -1) {
1183
                        index.disk.push(flv_size);
1184
                    }
1185
                };
1186
            });
1187
            
1188
            return index;
1189
        },
1190

    
1191
        get_flavor: function(cpu, mem, disk, filter_list) {
1192
            if (!filter_list) { filter_list = this.models };
1193
            return this.select(function(flv){
1194
                if (flv.get("cpu") == cpu + "" &&
1195
                   flv.get("ram") == mem + "" &&
1196
                   flv.get("disk") == disk + "" &&
1197
                   filter_list.indexOf(flv) > -1) { return true; }
1198
            })[0]
1199
        },
1200
        
1201
        get_data: function(lst) {
1202
            var data = {'cpu': [], 'mem':[], 'disk':[]};
1203

    
1204
            _.each(lst, function(flv) {
1205
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1206
                    data.cpu.push(flv.get("cpu"));
1207
                }
1208
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1209
                    data.mem.push(flv.get("ram"));
1210
                }
1211
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1212
                    data.disk.push(flv.get("disk"));
1213
                }
1214
            })
1215
            
1216
            return data;
1217
        },
1218

    
1219
        active: function() {
1220
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1221
        }
1222
            
1223
    })
1224

    
1225
    models.VMS = models.Collection.extend({
1226
        model: models.VM,
1227
        path: 'servers',
1228
        details: true,
1229
        copy_image_meta: true,
1230
        
1231
        parse: function (resp, xhr) {
1232
            // FIXME: depricated after refactoring
1233
            var data = resp;
1234
            if (!resp) { return [] };
1235
            data = _.filter(_.map(resp.servers.values, _.bind(this.parse_vm_api_data, this)), function(v){return v});
1236
            return data;
1237
        },
1238
        
1239
        get_reboot_required: function() {
1240
            return this.filter(function(vm){return vm.get("reboot_required") == true})
1241
        },
1242

    
1243
        has_pending_actions: function() {
1244
            return this.filter(function(vm){return vm.pending_action}).length > 0;
1245
        },
1246

    
1247
        reset_pending_actions: function() {
1248
            this.each(function(vm) {
1249
                vm.clear_pending_action();
1250
            })
1251
        },
1252
        
1253
        has_meta: function(vm_data) {
1254
            return vm_data.metadata && vm_data.metadata.values
1255
        },
1256

    
1257
        has_addresses: function(vm_data) {
1258
            return vm_data.metadata && vm_data.metadata.values
1259
        },
1260

    
1261
        parse_vm_api_data: function(data) {
1262

    
1263
            // do not add non existing DELETED entries
1264
            if (data.status && data.status == "DELETED") {
1265
                if (!this.get(data.id)) {
1266
                    console.error("non exising deleted vm", data)
1267
                    return false;
1268
                }
1269
            }
1270

    
1271
            // OS attribute
1272
            if (this.has_meta(data)) {
1273
                data['OS'] = data.metadata.values.OS || "undefined";
1274
            }
1275
            
1276
            data['firewalls'] = {};
1277
            if (data['addresses'] && data['addresses'].values) {
1278
                data['linked_to_nets'] = data['addresses'].values;
1279
                _.each(data['addresses'].values, function(f){
1280
                    if (f['firewallProfile']) {
1281
                        data['firewalls'][f['id']] = f['firewallProfile']
1282
                    }
1283
                });
1284
            }
1285
            
1286
            // if vm has no metadata, no metadata object
1287
            // is in json response, reset it to force
1288
            // value update
1289
            if (!data['metadata']) {
1290
                data['metadata'] = {values:{}};
1291
            }
1292

    
1293
            return data;
1294
        },
1295

    
1296
        create: function (name, image, flavor, meta, extra, callback) {
1297
            if (this.copy_image_meta) {
1298
                meta['OS'] = image.get("OS");
1299
           }
1300
            
1301
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta}
1302
            opts = _.extend(opts, extra);
1303

    
1304
            this.api.call(this.path, "create", {'server': opts}, undefined, undefined, callback);
1305
        }
1306

    
1307
    })
1308
    
1309

    
1310
    // storage initialization
1311
    snf.storage.images = new models.Images();
1312
    snf.storage.flavors = new models.Flavors();
1313
    snf.storage.networks = new models.Networks();
1314
    snf.storage.vms = new models.VMS();
1315

    
1316
    //snf.storage.vms.fetch({update:true});
1317
    //snf.storage.images.fetch({update:true});
1318
    //snf.storage.flavors.fetch({update:true});
1319

    
1320
})(this);