Statistics
| Branch: | Tag: | Revision:

root / ui / static / snf / js / models.js @ 47276ec2

History | View | Annotate | Download (61.1 kB)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
179
        get_created_user: function() {
180
            return synnefo.config.os_created_users[this.get_os()] || "root";
181
        },
182

    
183
        get_sort_order: function() {
184
            return parseInt(this.get('metadata') ? this.get('metadata').values.sortorder : -1)
185
        },
186
        
187
        ssh_keys_path: function() {
188
            prepend = '';
189
            if (this.get_created_user() != 'root') {
190
                prepend = '/home'
191
            }
192
            return '{1}/{0}/.ssh/authorized_keys'.format(this.get_created_user(), prepend);
193
        },
194

    
195
        _supports_ssh: function() {
196
            if (synnefo.config.support_ssh_os_list.indexOf(this.get_os()) > -1) {
197
                return true;
198
            }
199
            return false;
200
        },
201

    
202
        supports: function(feature) {
203
            if (feature == "ssh") {
204
                return this._supports_ssh()
205
            }
206
            return false;
207
        },
208

    
209
        personality_data_for_keys: function(keys) {
210
            contents = '';
211
            _.each(keys, function(key){
212
                contents = contents + key.get("content") + "\n"
213
            });
214
            contents = $.base64.encode(contents);
215

    
216
            return {
217
                path: this.ssh_keys_path(),
218
                contents: contents
219
            }
220
        }
221
    });
222

    
223
    // Flavor model
224
    models.Flavor = models.Model.extend({
225
        path: 'flavors',
226

    
227
        details_string: function() {
228
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
229
        },
230

    
231
        get_disk_size: function() {
232
            return parseInt(this.get("disk") * 1000)
233
        }
234

    
235
    });
236
    
237
    //network vms list helper
238
    var NetworkVMSList = function() {
239
        this.initialize = function() {
240
            this.vms = [];
241
            this.pending = [];
242
            this.pending_for_removal = [];
243
        }
244
        
245
        this.add_pending_for_remove = function(vm_id) {
246
            if (this.pending_for_removal.indexOf(vm_id) == -1) {
247
                this.pending_for_removal.push(vm_id);
248
            }
249

    
250
            if (this.pending_for_removal.length) {
251
                this.trigger("pending:remove:add");
252
            }
253
        },
254

    
255
        this.add_pending = function(vm_id) {
256
            if (this.pending.indexOf(vm_id) == -1) {
257
                this.pending[this.pending.length] = vm_id;
258
            }
259

    
260
            if (this.pending.length) {
261
                this.trigger("pending:add");
262
            }
263
        }
264

    
265
        this.check_pending = function() {
266
            var len = this.pending.length;
267
            var args = [this.pending];
268
            this.pending = _.difference(this.pending, this.vms);
269
            if (len != this.pending.length) {
270
                if (this.pending.length == 0) {
271
                    this.trigger("pending:clear");
272
                }
273
            }
274

    
275
            var len = this.pending_for_removal.length;
276
            this.pending_for_removal = _.intersection(this.pending_for_removal, this.vms);
277
            if (this.pending_for_removal.length == 0) {
278
                this.trigger("pending:remove:clear");
279
            }
280

    
281
        }
282

    
283

    
284
        this.add = function(vm_id) {
285
            if (this.vms.indexOf(vm_id) == -1) {
286
                this.vms[this.vms.length] = vm_id;
287
                this.trigger("network:connect", vm_id);
288
                this.check_pending();
289
                return true;
290
            }
291
        }
292

    
293
        this.remove = function(vm_id) {
294
            if (this.vms.indexOf(vm_id) > -1) {
295
                this.vms = _.without(this.vms, vm_id);
296
                this.trigger("network:disconnect", vm_id);
297
                this.check_pending();
298
                return true;
299
            }
300
        }
301

    
302
        this.get = function() {
303
            return this.vms;
304
        }
305

    
306
        this.list = function() {
307
            return storage.vms.filter(_.bind(function(vm){
308
                return this.vms.indexOf(vm.id) > -1;
309
            }, this))
310
        }
311

    
312
        this.initialize();
313
    };
314
    _.extend(NetworkVMSList.prototype, bb.Events);
315
    
316
    // vm networks list helper
317
    var VMNetworksList = function() {
318
        this.initialize = function() {
319
            this.networks = {};
320
            this.network_ids = [];
321
        }
322

    
323
        this.add = function(net_id, data) {
324
            if (!this.networks[net_id]) {
325
                this.networks[net_id] = data || {};
326
                this.network_ids[this.network_ids.length] = net_id;
327
                this.trigger("network:connect", net_id);
328
                return true;
329
            }
330
        }
331

    
332
        this.remove = function(net_id) {
333
            if (this.networks[net_id]) {
334
                delete this.networks[net_id];
335
                this.network_ids = _.without(this.network_ids, net_id);
336
                this.trigger("network:disconnect", net_id);
337
                return true;
338
            }
339
            return false;
340
        }
341

    
342
        this.get = function() {
343
            return this.networks;
344
        }
345

    
346
        this.list = function() {
347
            return storage.networks.filter(_.bind(function(net){
348
                return this.network_ids.indexOf(net.id) > -1;
349
            }, this))
350
        }
351

    
352
        this.initialize();
353
    };
354
    _.extend(VMNetworksList.prototype, bb.Events);
355
        
356
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
357
    _.extend(models.ParamsList.prototype, bb.Events, {
358

    
359
        initialize: function(parent, param_name) {
360
            this.parent = parent;
361
            this.actions = {};
362
            this.param_name = param_name;
363
            this.length = 0;
364
        },
365
        
366
        has_action: function(action) {
367
            return this.actions[action] ? true : false;
368
        },
369
            
370
        _parse_params: function(arguments) {
371
            if (arguments.length <= 1) {
372
                return [];
373
            }
374

    
375
            var args = _.toArray(arguments);
376
            return args.splice(1);
377
        },
378

    
379
        contains: function(action, params) {
380
            params = this._parse_params(arguments);
381
            var has_action = this.has_action(action);
382
            if (!has_action) { return false };
383

    
384
            var paramsEqual = false;
385
            _.each(this.actions[action], function(action_params) {
386
                if (_.isEqual(action_params, params)) {
387
                    paramsEqual = true;
388
                }
389
            });
390
                
391
            return paramsEqual;
392
        },
393
        
394
        is_empty: function() {
395
            return _.isEmpty(this.actions);
396
        },
397

    
398
        add: function(action, params) {
399
            params = this._parse_params(arguments);
400
            if (this.contains.apply(this, arguments)) { return this };
401
            var isnew = false
402
            if (!this.has_action(action)) {
403
                this.actions[action] = [];
404
                isnew = true;
405
            };
406

    
407
            this.actions[action].push(params);
408
            this.parent.trigger("change:" + this.param_name, this.parent, this);
409
            if (isnew) {
410
                this.trigger("add", action, params);
411
            } else {
412
                this.trigger("change", action, params);
413
            }
414
            return this;
415
        },
416
        
417
        remove_all: function(action) {
418
            if (this.has_action(action)) {
419
                delete this.actions[action];
420
                this.parent.trigger("change:" + this.param_name, this.parent, this);
421
                this.trigger("remove", action);
422
            }
423
            return this;
424
        },
425

    
426
        reset: function() {
427
            this.actions = {};
428
            this.parent.trigger("change:" + this.param_name, this.parent, this);
429
            this.trigger("reset");
430
            this.trigger("remove");
431
        },
432

    
433
        remove: function(action, params) {
434
            params = this._parse_params(arguments);
435
            if (!this.has_action(action)) { return this };
436
            var index = -1;
437
            _.each(this.actions[action], _.bind(function(action_params) {
438
                if (_.isEqual(action_params, params)) {
439
                    index = this.actions[action].indexOf(action_params);
440
                }
441
            }, this));
442
            
443
            if (index > -1) {
444
                this.actions[action].splice(index, 1);
445
                if (_.isEmpty(this.actions[action])) {
446
                    delete this.actions[action];
447
                }
448
                this.parent.trigger("change:" + this.param_name, this.parent, this);
449
                this.trigger("remove", action, params);
450
            }
451
        }
452

    
453
    });
454

    
455
    // Image model
456
    models.Network = models.Model.extend({
457
        path: 'networks',
458
        has_status: true,
459
        
460
        initialize: function() {
461
            this.vms = new NetworkVMSList();
462
            this.vms.bind("pending:add", _.bind(this.handle_pending_connections, this, "add"));
463
            this.vms.bind("pending:clear", _.bind(this.handle_pending_connections, this, "clear"));
464
            this.vms.bind("pending:remove:add", _.bind(this.handle_pending_connections, this, "add"));
465
            this.vms.bind("pending:remove:clear", _.bind(this.handle_pending_connections, this, "clear"));
466

    
467
            var ret = models.Network.__super__.initialize.apply(this, arguments);
468

    
469
            storage.vms.bind("change:linked_to_nets", _.bind(this.update_connections, this, "vm:change"));
470
            storage.vms.bind("add", _.bind(this.update_connections, this, "add"));
471
            storage.vms.bind("remove", _.bind(this.update_connections, this, "remove"));
472
            storage.vms.bind("reset", _.bind(this.update_connections, this, "reset"));
473

    
474
            this.bind("change:linked_to", _.bind(this.update_connections, this, "net:change"));
475
            this.update_connections();
476
            this.update_state();
477
            
478
            this.set({"actions": new models.ParamsList(this, "actions")});
479

    
480
            return ret;
481
        },
482

    
483
        update_state: function() {
484
            if (this.vms.pending.length) {
485
                this.set({state: "CONNECTING"});
486
                return
487
            }
488

    
489
            if (this.vms.pending_for_removal.length) {
490
                this.set({state: "DISCONNECTING"});
491
                return
492
            }   
493
            
494
            var firewalling = false;
495
            _.each(this.vms.get(), _.bind(function(vm_id){
496
                var vm = storage.vms.get(vm_id);
497
                if (!vm) { return };
498
                if (!_.isEmpty(vm.pending_firewalls)) {
499
                    this.set({state:"FIREWALLING"});
500
                    firewalling = true;
501
                    return false;
502
                }
503
            },this));
504
            if (firewalling) { return };
505

    
506
            this.set({state:"NORMAL"});
507
        },
508

    
509
        handle_pending_connections: function(action) {
510
            this.update_state();
511
        },
512

    
513
        // handle vm/network connections
514
        update_connections: function(action, model) {
515
            
516
            // vm removed disconnect vm from network
517
            if (action == "remove") {
518
                var removed_from_net = this.vms.remove(model.id);
519
                var removed_from_vm = model.networks.remove(this.id);
520
                if (removed_from_net) {this.trigger("vm:disconnect", model, this); this.change()};
521
                if (removed_from_vm) {model.trigger("network:disconnect", this, model); this.change()};
522
                return;
523
            }
524
            
525
            // update links for all vms
526
            var links = this.get("linked_to");
527
            storage.vms.each(_.bind(function(vm) {
528
                var vm_links = vm.get("linked_to") || [];
529
                if (vm_links.indexOf(this.id) > -1) {
530
                    // vm has connection to current network
531
                    if (links.indexOf(vm.id) > -1) {
532
                        // and network has connection to vm, so try
533
                        // to append it
534
                        var add_to_net = this.vms.add(vm.id);
535
                        var index = _.indexOf(vm_links, this.id);
536
                        var add_to_vm = vm.networks.add(this.id, vm.get("linked_to_nets")[index]);
537
                        
538
                        // call only if connection did not existed
539
                        if (add_to_net) {this.trigger("vm:connect", vm, this); this.change()};
540
                        if (add_to_vm) {vm.trigger("network:connect", this, vm); vm.change()};
541
                    } else {
542
                        // no connection, try to remove it
543
                        var removed_from_net = this.vms.remove(vm.id);
544
                        var removed_from_vm = vm.networks.remove(this.id);
545
                        if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
546
                        if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
547
                    }
548
                } else {
549
                    // vm has no connection to current network, try to remove it
550
                    var removed_from_net = this.vms.remove(vm.id);
551
                    var removed_from_vm = vm.networks.remove(this.id);
552
                    if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
553
                    if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
554
                }
555
            },this));
556
        },
557

    
558
        is_public: function() {
559
            return this.id == "public";
560
        },
561

    
562
        contains_vm: function(vm) {
563
            var net_vm_exists = this.vms.get().indexOf(vm.id) > -1;
564
            var vm_net_exists = vm.is_connected_to(this);
565
            return net_vm_exists && vm_net_exists;
566
        },
567
        
568
        call: function(action, params, success, error) {
569
            if (action == "destroy") {
570
                this.set({state:"DESTROY"});
571
                this.get("actions").remove("destroy");
572
                this.remove(_.bind(function(){
573
                    success();
574
                }, this), error);
575
            }
576
            
577
            if (action == "disconnect") {
578
                _.each(params, _.bind(function(vm_id) {
579
                    var vm = snf.storage.vms.get(vm_id);
580
                    this.get("actions").remove("disconnect", vm_id);
581
                    if (vm) {
582
                        this.remove_vm(vm, success, error);
583
                    }
584
                }, this));
585
            }
586
        },
587

    
588
        add_vm: function (vm, callback, error, options) {
589
            var payload = {add:{serverRef:"" + vm.id}};
590
            payload._options = options || {};
591
            return this.api.call(this.api_path() + "/action", "create", 
592
                                 payload,
593
                                 _.bind(function(){
594
                                     this.vms.add_pending(vm.id);
595
                                     if (callback) {callback()}
596
                                 },this), error);
597
        },
598

    
599
        remove_vm: function (vm, callback, error, options) {
600
            var payload = {remove:{serverRef:"" + vm.id}};
601
            payload._options = options || {};
602
            return this.api.call(this.api_path() + "/action", "create", 
603
                                 {remove:{serverRef:"" + vm.id}},
604
                                 _.bind(function(){
605
                                     this.vms.add_pending_for_remove(vm.id);
606
                                     if (callback) {callback()}
607
                                 },this), error);
608
        },
609

    
610
        rename: function(name, callback) {
611
            return this.api.call(this.api_path(), "update", {
612
                network:{name:name}, 
613
                _options:{
614
                    critical: false, 
615
                    error_params:{
616
                        title: "Network action failed",
617
                        ns: "Networks",
618
                        extra_details: {"Network id": this.id}
619
                    }
620
                }}, callback);
621
        },
622

    
623
        get_connectable_vms: function() {
624
            var servers = this.vms.list();
625
            return storage.vms.filter(function(vm){
626
                return servers.indexOf(vm) == -1 && !vm.in_error_state();
627
            })
628
        },
629

    
630
        state_message: function() {
631
            if (this.get("state") == "NORMAL" && this.is_public()) {
632
                return "Public network";
633
            }
634

    
635
            return models.Network.STATES[this.get("state")];
636
        },
637

    
638
        in_progress: function() {
639
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
640
        },
641

    
642
        do_all_pending_actions: function(success, error) {
643
            var destroy = this.get("actions").has_action("destroy");
644
            _.each(this.get("actions").actions, _.bind(function(params, action) {
645
                _.each(params, _.bind(function(with_params) {
646
                    this.call(action, with_params, success, error);
647
                }, this));
648
            }, this));
649
        }
650
    });
651
    
652
    models.Network.STATES = {
653
        'NORMAL': 'Private network',
654
        'CONNECTING': 'Connecting...',
655
        'DISCONNECTING': 'Disconnecting...',
656
        'FIREWALLING': 'Firewall update...',
657
        'DESTROY': 'Destroying...'
658
    }
659

    
660
    models.Network.STATES_TRANSITIONS = {
661
        'CONNECTING': ['NORMAL'],
662
        'DISCONNECTING': ['NORMAL'],
663
        'FIREWALLING': ['NORMAL']
664
    }
665

    
666
    // Virtualmachine model
667
    models.VM = models.Model.extend({
668

    
669
        path: 'servers',
670
        has_status: true,
671
        initialize: function(params) {
672
            this.networks = new VMNetworksList();
673
            
674
            this.pending_firewalls = {};
675
            
676
            models.VM.__super__.initialize.apply(this, arguments);
677

    
678
            this.set({state: params.status || "ERROR"});
679
            this.log = new snf.logging.logger("VM " + this.id);
680
            this.pending_action = undefined;
681
            
682
            // init stats parameter
683
            this.set({'stats': undefined}, {silent: true});
684
            // defaults to not update the stats
685
            // each view should handle this vm attribute 
686
            // depending on if it displays stat images or not
687
            this.do_update_stats = false;
688
            
689
            // interval time
690
            // this will dynamicaly change if the server responds that
691
            // images get refreshed on different intervals
692
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
693
            this.stats_available = false;
694

    
695
            // initialize interval
696
            this.init_stats_intervals(this.stats_update_interval);
697
            
698
            this.bind("change:progress", _.bind(this.update_building_progress, this));
699
            this.update_building_progress();
700

    
701
            this.bind("change:firewalls", _.bind(this.handle_firewall_change, this));
702
            
703
            // default values
704
            this.set({linked_to_nets:this.get("linked_to_nets") || []});
705
            this.set({firewalls:this.get("firewalls") || []});
706

    
707
            this.bind("change:state", _.bind(function(){if (this.state() == "DESTROY") { this.handle_destroy() }}, this))
708
        },
709

    
710
        handle_firewall_change: function() {
711

    
712
        },
713
        
714
        set_linked_to_nets: function(data) {
715
            this.set({"linked_to":_.map(data, function(n){ return n.id})});
716
            return data;
717
        },
718

    
719
        is_connected_to: function(net) {
720
            return _.filter(this.networks.list(), function(n){return n.id == net.id}).length > 0;
721
        },
722
        
723
        status: function(st) {
724
            if (!st) { return this.get("status")}
725
            return this.set({status:st});
726
        },
727

    
728
        set_status: function(st) {
729
            var new_state = this.state_for_api_status(st);
730
            var transition = false;
731

    
732
            if (this.state() != new_state) {
733
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
734
                    transition = this.state();
735
                }
736
            }
737
            
738
            // call it silently to avoid double change trigger
739
            this.set({'state': this.state_for_api_status(st)}, {silent: true});
740
            
741
            // trigger transition
742
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
743
                this.trigger("transition", {from:transition, to:new_state}) 
744
            };
745
            return st;
746
        },
747

    
748
        update_building_progress: function() {
749
            if (this.is_building()) {
750
                var progress = this.get("progress");
751
                if (progress == 0) {
752
                    this.state("BUILD_INIT");
753
                    this.set({progress_message: BUILDING_MESSAGES['INIT']});
754
                }
755
                if (progress > 0 && progress < 99) {
756
                    this.state("BUILD_COPY");
757
                    var params = this.get_copy_details(true);
758
                    this.set({progress_message: BUILDING_MESSAGES['COPY'].format(params.copy, 
759
                                                                                 params.size, 
760
                                                                                 params.progress)});
761
                }
762
                if (progress == 100) {
763
                    this.state("BUILD_FINAL");
764
                    this.set({progress_message: BUILDING_MESSAGES['FINAL']});
765
                }
766
            } else {
767
            }
768
        },
769

    
770
        get_copy_details: function(human, image) {
771
            var human = human || false;
772
            var image = image || this.get_image();
773

    
774
            var progress = this.get('progress');
775
            var size = image.get_size();
776
            var size_copied = (size * progress / 100).toFixed(2);
777
            
778
            if (human) {
779
                size = util.readablizeBytes(size*1024*1024);
780
                size_copied = util.readablizeBytes(size_copied*1024*1024);
781
            }
782
            return {'progress': progress, 'size': size, 'copy': size_copied};
783
        },
784

    
785
        start_stats_update: function(force_if_empty) {
786
            var prev_state = this.do_update_stats;
787

    
788
            this.do_update_stats = true;
789
            
790
            // fetcher initialized ??
791
            if (!this.stats_fetcher) {
792
                this.init_stats_intervals();
793
            }
794

    
795

    
796
            // fetcher running ???
797
            if (!this.stats_fetcher.running || !prev_state) {
798
                this.stats_fetcher.start();
799
            }
800

    
801
            if (force_if_empty && this.get("stats") == undefined) {
802
                this.update_stats(true);
803
            }
804
        },
805

    
806
        stop_stats_update: function(stop_calls) {
807
            this.do_update_stats = false;
808

    
809
            if (stop_calls) {
810
                this.stats_fetcher.stop();
811
            }
812
        },
813

    
814
        // clear and reinitialize update interval
815
        init_stats_intervals: function (interval) {
816
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
817
            this.stats_fetcher.start();
818
        },
819
        
820
        get_stats_fetcher: function(timeout) {
821
            var cb = _.bind(function(data){
822
                this.update_stats();
823
            }, this);
824
            var fetcher = new snf.api.updateHandler({'callback': cb, timeout:timeout});
825
            return fetcher;
826
        },
827

    
828
        // do the api call
829
        update_stats: function(force) {
830
            // do not update stats if flag not set
831
            if ((!this.do_update_stats && !force) || this.updating_stats) {
832
                return;
833
            }
834

    
835
            // make the api call, execute handle_stats_update on sucess
836
            // TODO: onError handler ???
837
            stats_url = this.url() + "/stats";
838
            this.updating_stats = true;
839
            this.sync("GET", this, {
840
                handles_error:true, 
841
                url: stats_url, 
842
                refresh:true, 
843
                success: _.bind(this.handle_stats_update, this),
844
                error: _.bind(this.handle_stats_error, this),
845
                complete: _.bind(function(){this.updating_stats = false;}, this),
846
                critical: false,
847
                display: false,
848
                log_error: false
849
            });
850
        },
851

    
852
        get_stats_image: function(stat, type) {
853
        },
854
        
855
        _set_stats: function(stats) {
856
            var silent = silent === undefined ? false : silent;
857
            // unavailable stats while building
858
            if (this.get("status") == "BUILD") { 
859
                this.stats_available = false;
860
            } else { this.stats_available = true; }
861

    
862
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
863
            
864
            this.set({stats: stats}, {silent:true});
865
            this.trigger("stats:update", stats);
866
        },
867

    
868
        unbind: function() {
869
            models.VM.__super__.unbind.apply(this, arguments);
870
        },
871

    
872
        handle_stats_error: function() {
873
            stats = {};
874
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
875
                stats[k] = false;
876
            });
877

    
878
            this.set({'stats': stats});
879
        },
880

    
881
        // this method gets executed after a successful vm stats api call
882
        handle_stats_update: function(data) {
883
            var self = this;
884
            // avoid browser caching
885
            
886
            if (data.stats && _.size(data.stats) > 0) {
887
                var ts = $.now();
888
                var stats = data.stats;
889
                var images_loaded = 0;
890
                var images = {};
891

    
892
                function check_images_loaded() {
893
                    images_loaded++;
894

    
895
                    if (images_loaded == 4) {
896
                        self._set_stats(images);
897
                    }
898
                }
899
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
900
                    
901
                    stats[k] = stats[k] + "?_=" + ts;
902
                    
903
                    var stat = k.slice(0,3);
904
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
905
                    var img = $("<img />");
906
                    var val = stats[k];
907
                    
908
                    // load stat image to a temporary dom element
909
                    // update model stats on image load/error events
910
                    img.load(function() {
911
                        images[k] = val;
912
                        check_images_loaded();
913
                    });
914

    
915
                    img.error(function() {
916
                        images[stat + type] = false;
917
                        check_images_loaded();
918
                    });
919

    
920
                    img.attr({'src': stats[k]});
921
                })
922
                data.stats = stats;
923
            }
924

    
925
            // do we need to change the interval ??
926
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
927
                this.stats_update_interval = data.stats.refresh * 1000;
928
                this.stats_fetcher.timeout = this.stats_update_interval;
929
                this.stats_fetcher.stop();
930
                this.stats_fetcher.start(false);
931
            }
932
        },
933

    
934
        // helper method that sets the do_update_stats
935
        // in the future this method could also make an api call
936
        // immediaetly if needed
937
        enable_stats_update: function() {
938
            this.do_update_stats = true;
939
        },
940
        
941
        handle_destroy: function() {
942
            this.stats_fetcher.stop();
943
        },
944

    
945
        require_reboot: function() {
946
            if (this.is_active()) {
947
                this.set({'reboot_required': true});
948
            }
949
        },
950
        
951
        set_pending_action: function(data) {
952
            this.pending_action = data;
953
            return data;
954
        },
955

    
956
        // machine has pending action
957
        update_pending_action: function(action, force) {
958
            this.set({pending_action: action});
959
        },
960

    
961
        clear_pending_action: function() {
962
            this.set({pending_action: undefined});
963
        },
964

    
965
        has_pending_action: function() {
966
            return this.get("pending_action") ? this.get("pending_action") : false;
967
        },
968
        
969
        // machine is active
970
        is_active: function() {
971
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
972
        },
973
        
974
        // machine is building 
975
        is_building: function() {
976
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
977
        },
978
        
979
        in_error_state: function() {
980
            return this.state() === "ERROR"
981
        },
982

    
983
        // user can connect to machine
984
        is_connectable: function() {
985
            // check if ips exist
986
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
987
                return false;
988
            }
989
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
990
        },
991
        
992
        set_firewalls: function(data) {
993
            _.each(data, _.bind(function(val, key){
994
                if (this.pending_firewalls && this.pending_firewalls[key] && this.pending_firewalls[key] == val) {
995
                        this.require_reboot();
996
                        this.remove_pending_firewall(key, val);
997
                }
998
            }, this));
999
            return data;
1000
        },
1001

    
1002
        remove_pending_firewall: function(net_id, value) {
1003
            if (this.pending_firewalls[net_id] == value) {
1004
                delete this.pending_firewalls[net_id];
1005
                storage.networks.get(net_id).update_state();
1006
            }
1007
        },
1008
            
1009
        remove_meta: function(key, complete, error) {
1010
            var url = this.api_path() + "/meta/" + key;
1011
            this.api.call(url, "delete", undefined, complete, error);
1012
        },
1013

    
1014
        save_meta: function(meta, complete, error) {
1015
            var url = this.api_path() + "/meta/" + meta.key;
1016
            var payload = {meta:{}};
1017
            payload.meta[meta.key] = meta.value;
1018
            payload._options = {
1019
                critical:false, 
1020
                error_params: {
1021
                    title: "Machine metadata error",
1022
                    extra_details: {"Machine id": this.id}
1023
            }};
1024

    
1025
            this.api.call(url, "update", payload, complete, error);
1026
        },
1027

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

    
1031
            this.pending_firewalls[net_id] = value;
1032
            this.trigger("change", this, this);
1033
            var payload = {"firewallProfile":{"profile":value}};
1034
            payload._options = _.extend({critical: false}, options);
1035
            
1036
            // reset firewall state on error
1037
            var error_cb = _.bind(function() {
1038
                thi
1039
            }, this);
1040

    
1041
            this.api.call(this.api_path() + "/action", "create", payload, callback, error);
1042
            storage.networks.get(net_id).update_state();
1043
        },
1044

    
1045
        firewall_pending: function(net_id) {
1046
            return this.pending_firewalls[net_id] != undefined;
1047
        },
1048
        
1049
        // update/get the state of the machine
1050
        state: function() {
1051
            var args = slice.call(arguments);
1052
                
1053
            // TODO: it might not be a good idea to set the state in set_state method
1054
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1055
                this.set({'state': args[0]});
1056
            }
1057

    
1058
            return this.get('state');
1059
        },
1060
        
1061
        // get the state that the api status corresponds to
1062
        state_for_api_status: function(status) {
1063
            return this.state_transition(this.state(), status);
1064
        },
1065
        
1066
        // vm state equals vm api status
1067
        state_is_status: function(state) {
1068
            return models.VM.STATUSES.indexOf(state) != -1;
1069
        },
1070
        
1071
        // get transition state for the corresponging api status
1072
        state_transition: function(state, new_status) {
1073
            var statuses = models.VM.STATES_TRANSITIONS[state];
1074
            if (statuses) {
1075
                if (statuses.indexOf(new_status) > -1) {
1076
                    return new_status;
1077
                } else {
1078
                    return state;
1079
                }
1080
            } else {
1081
                return new_status;
1082
            }
1083
        },
1084
        
1085
        // the current vm state is a transition state
1086
        in_transition: function() {
1087
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
1088
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
1089
        },
1090
        
1091
        // get image object
1092
        // TODO: update images synchronously if image not found
1093
        get_image: function() {
1094
            var image = storage.images.get(this.get('imageRef'));
1095
            if (!image) {
1096
                storage.images.update_unknown_id(this.get('imageRef'));
1097
                image = storage.images.get(this.get('imageRef'));
1098
            }
1099
            return image;
1100
        },
1101
        
1102
        // get flavor object
1103
        get_flavor: function() {
1104
            var flv = storage.flavors.get(this.get('flavorRef'));
1105
            if (!flv) {
1106
                storage.flavors.update_unknown_id(this.get('flavorRef'));
1107
                flv = storage.flavors.get(this.get('flavorRef'));
1108
            }
1109
            return flv;
1110
        },
1111

    
1112
        // retrieve the metadata object
1113
        get_meta: function() {
1114
            try {
1115
                return this.get('metadata').values
1116
            } catch (err) {
1117
                return {};
1118
            }
1119
        },
1120
        
1121
        // get metadata OS value
1122
        get_os: function() {
1123
            return this.get_meta().OS || (this.get_image() ? this.get_image().get_os() || "okeanos" : "okeanos");
1124
        },
1125
        
1126
        // get public ip addresses
1127
        // TODO: public network is always the 0 index ???
1128
        get_addresses: function(net_id) {
1129
            var net_id = net_id || "public";
1130
            
1131
            var info = this.get_network_info(net_id);
1132
            if (!info) { return {} };
1133
            addrs = {};
1134
            _.each(info.values, function(addr) {
1135
                addrs["ip" + addr.version] = addr.addr;
1136
            });
1137
            return addrs
1138
        },
1139

    
1140
        get_network_info: function(net_id) {
1141
            var net_id = net_id || "public";
1142
            
1143
            if (!this.networks.network_ids.length) { return {} };
1144

    
1145
            var addresses = this.networks.get();
1146
            try {
1147
                return _.select(addresses, function(net, key){return key == net_id })[0];
1148
            } catch (err) {
1149
                //this.log.debug("Cannot find network {0}".format(net_id))
1150
            }
1151
        },
1152

    
1153
        firewall_profile: function(net_id) {
1154
            var net_id = net_id || "public";
1155
            var firewalls = this.get("firewalls");
1156
            return firewalls[net_id];
1157
        },
1158

    
1159
        has_firewall: function(net_id) {
1160
            var net_id = net_id || "public";
1161
            return ["ENABLED","PROTECTED"].indexOf(this.firewall_profile()) > -1;
1162
        },
1163
    
1164
        // get actions that the user can execute
1165
        // depending on the vm state/status
1166
        get_available_actions: function() {
1167
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1168
        },
1169

    
1170
        set_profile: function(profile, net_id) {
1171
        },
1172
        
1173
        // call rename api
1174
        rename: function(new_name) {
1175
            //this.set({'name': new_name});
1176
            this.sync("update", this, {
1177
                critical: true,
1178
                data: {
1179
                    'server': {
1180
                        'name': new_name
1181
                    }
1182
                }, 
1183
                // do the rename after the method succeeds
1184
                success: _.bind(function(){
1185
                    //this.set({name: new_name});
1186
                    snf.api.trigger("call");
1187
                }, this)
1188
            });
1189
        },
1190
        
1191
        get_console_url: function(data) {
1192
            var url_params = {
1193
                machine: this.get("name"),
1194
                host_ip: this.get_addresses().ip4,
1195
                host_ip_v6: this.get_addresses().ip6,
1196
                host: data.host,
1197
                port: data.port,
1198
                password: data.password
1199
            }
1200
            return '/machines/console?' + $.param(url_params);
1201
        },
1202

    
1203
        // action helper
1204
        call: function(action_name, success, error) {
1205
            var id_param = [this.id];
1206

    
1207
            success = success || function() {};
1208
            error = error || function() {};
1209

    
1210
            var self = this;
1211

    
1212
            switch(action_name) {
1213
                case 'start':
1214
                    this.__make_api_call(this.get_action_url(), // vm actions url
1215
                                         "create", // create so that sync later uses POST to make the call
1216
                                         {start:{}}, // payload
1217
                                         function() {
1218
                                             // set state after successful call
1219
                                             self.state("START"); 
1220
                                             success.apply(this, arguments);
1221
                                             snf.api.trigger("call");
1222
                                         },  
1223
                                         error, 'start');
1224
                    break;
1225
                case 'reboot':
1226
                    this.__make_api_call(this.get_action_url(), // vm actions url
1227
                                         "create", // create so that sync later uses POST to make the call
1228
                                         {reboot:{type:"HARD"}}, // payload
1229
                                         function() {
1230
                                             // set state after successful call
1231
                                             self.state("REBOOT"); 
1232
                                             success.apply(this, arguments)
1233
                                             snf.api.trigger("call");
1234
                                             self.set({'reboot_required': false});
1235
                                         },
1236
                                         error, 'reboot');
1237
                    break;
1238
                case 'shutdown':
1239
                    this.__make_api_call(this.get_action_url(), // vm actions url
1240
                                         "create", // create so that sync later uses POST to make the call
1241
                                         {shutdown:{}}, // payload
1242
                                         function() {
1243
                                             // set state after successful call
1244
                                             self.state("SHUTDOWN"); 
1245
                                             success.apply(this, arguments)
1246
                                             snf.api.trigger("call");
1247
                                         },  
1248
                                         error, 'shutdown');
1249
                    break;
1250
                case 'console':
1251
                    this.__make_api_call(this.url() + "/action", "create", {'console': {'type':'vnc'}}, function(data) {
1252
                        var cons_data = data.console;
1253
                        success.apply(this, [cons_data]);
1254
                    }, undefined, 'console')
1255
                    break;
1256
                case 'destroy':
1257
                    this.__make_api_call(this.url(), // vm actions url
1258
                                         "delete", // create so that sync later uses POST to make the call
1259
                                         undefined, // payload
1260
                                         function() {
1261
                                             // set state after successful call
1262
                                             self.state('DESTROY');
1263
                                             success.apply(this, arguments)
1264
                                         },  
1265
                                         error, 'destroy');
1266
                    break;
1267
                default:
1268
                    throw "Invalid VM action ("+action_name+")";
1269
            }
1270
        },
1271
        
1272
        __make_api_call: function(url, method, data, success, error, action) {
1273
            var self = this;
1274
            error = error || function(){};
1275
            success = success || function(){};
1276

    
1277
            var params = {
1278
                url: url,
1279
                data: data,
1280
                success: function(){ self.handle_action_succeed.apply(self, arguments); success.apply(this, arguments)},
1281
                error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)},
1282
                error_params: { ns: "Machines actions", 
1283
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1284
                                extra_details: { 'Machine ID': this.id, 'URL': url, 'Action': action || "undefined" },
1285
                                allow_reload: false
1286
                              },
1287
                display: false,
1288
                critical: false
1289
            }
1290
            this.sync(method, this, params);
1291
        },
1292

    
1293
        handle_action_succeed: function() {
1294
            this.trigger("action:success", arguments);
1295
        },
1296
        
1297
        reset_action_error: function() {
1298
            this.action_error = false;
1299
            this.trigger("action:fail:reset", this.action_error);
1300
        },
1301

    
1302
        handle_action_fail: function() {
1303
            this.action_error = arguments;
1304
            this.trigger("action:fail", arguments);
1305
        },
1306

    
1307
        get_action_url: function(name) {
1308
            return this.url() + "/action";
1309
        },
1310

    
1311
        get_connection_info: function(host_os, success, error) {
1312
            var url = "/machines/connect";
1313
            params = {
1314
                ip_address: this.get_addresses().ip4,
1315
                os: this.get_os(),
1316
                host_os: host_os,
1317
                srv: this.id
1318
            }
1319

    
1320
            url = url + "?" + $.param(params);
1321

    
1322
            var ajax = snf.api.sync("read", undefined, { url: url, 
1323
                                                         error:error, 
1324
                                                         success:success, 
1325
                                                         handles_error:1});
1326
        }
1327
    })
1328
    
1329
    models.VM.ACTIONS = [
1330
        'start',
1331
        'shutdown',
1332
        'reboot',
1333
        'console',
1334
        'destroy'
1335
    ]
1336

    
1337
    models.VM.AVAILABLE_ACTIONS = {
1338
        'UNKNWON'       : ['destroy'],
1339
        'BUILD'         : ['destroy'],
1340
        'REBOOT'        : ['shutdown', 'destroy', 'console'],
1341
        'STOPPED'       : ['start', 'destroy'],
1342
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1343
        'ERROR'         : ['destroy'],
1344
        'DELETED'        : [],
1345
        'DESTROY'       : [],
1346
        'BUILD_INIT'    : ['destroy'],
1347
        'BUILD_COPY'    : ['destroy'],
1348
        'BUILD_FINAL'   : ['destroy'],
1349
        'SHUTDOWN'      : ['destroy'],
1350
        'START'         : [],
1351
        'CONNECT'       : [],
1352
        'DISCONNECT'    : []
1353
    }
1354

    
1355
    // api status values
1356
    models.VM.STATUSES = [
1357
        'UNKNWON',
1358
        'BUILD',
1359
        'REBOOT',
1360
        'STOPPED',
1361
        'ACTIVE',
1362
        'ERROR',
1363
        'DELETED'
1364
    ]
1365

    
1366
    // api status values
1367
    models.VM.CONNECT_STATES = [
1368
        'ACTIVE',
1369
        'REBOOT',
1370
        'SHUTDOWN'
1371
    ]
1372

    
1373
    // vm states
1374
    models.VM.STATES = models.VM.STATUSES.concat([
1375
        'DESTROY',
1376
        'BUILD_INIT',
1377
        'BUILD_COPY',
1378
        'BUILD_FINAL',
1379
        'SHUTDOWN',
1380
        'START',
1381
        'CONNECT',
1382
        'DISCONNECT',
1383
        'FIREWALL'
1384
    ]);
1385
    
1386
    models.VM.STATES_TRANSITIONS = {
1387
        'DESTROY' : ['DELETED'],
1388
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1389
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1390
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1391
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1392
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1393
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1394
        'BUILD_COPY': ['ERROR', 'ACTIVE', 'BUILD_FINAL', 'DESTROY'],
1395
        'BUILD_FINAL': ['ERROR', 'ACTIVE', 'DESTROY'],
1396
        'BUILD_INIT': ['ERROR', 'ACTIVE', 'BUILD_COPY', 'BUILD_FINAL', 'DESTROY']
1397
    }
1398

    
1399
    models.VM.TRANSITION_STATES = [
1400
        'DESTROY',
1401
        'SHUTDOWN',
1402
        'START',
1403
        'REBOOT',
1404
        'BUILD'
1405
    ]
1406

    
1407
    models.VM.ACTIVE_STATES = [
1408
        'BUILD', 'REBOOT', 'ACTIVE',
1409
        'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL',
1410
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1411
    ]
1412

    
1413
    models.VM.BUILDING_STATES = [
1414
        'BUILD', 'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL'
1415
    ]
1416

    
1417
    models.Networks = models.Collection.extend({
1418
        model: models.Network,
1419
        path: 'networks',
1420
        details: true,
1421
        //noUpdate: true,
1422
        defaults: {'linked_to':[]},
1423

    
1424
        parse: function (resp, xhr) {
1425
            // FIXME: depricated global var
1426
            if (!resp) { return []};
1427
               
1428
            var data = _.map(resp.networks.values, _.bind(this.parse_net_api_data, this));
1429
            return data;
1430
        },
1431

    
1432
        reset_pending_actions: function() {
1433
            this.each(function(net) {
1434
                net.get("actions").reset();
1435
            })
1436
        },
1437

    
1438
        do_all_pending_actions: function() {
1439
            this.each(function(net) {
1440
                net.do_all_pending_actions();
1441
            })
1442
        },
1443

    
1444
        parse_net_api_data: function(data) {
1445
            if (data.servers && data.servers.values) {
1446
                data['linked_to'] = data.servers.values;
1447
            }
1448
            return data;
1449
        },
1450

    
1451
        create: function (name, callback) {
1452
            return this.api.call(this.path, "create", {network:{name:name}}, callback);
1453
        }
1454
    })
1455

    
1456
    models.Images = models.Collection.extend({
1457
        model: models.Image,
1458
        path: 'images',
1459
        details: true,
1460
        noUpdate: true,
1461
        supportIncUpdates: false,
1462
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1463

    
1464
        // update collection model with id passed
1465
        // making a direct call to the image
1466
        // api url
1467
        update_unknown_id: function(id) {
1468
            var url = getUrl.call(this) + "/" + id;
1469
            this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, 
1470
            _.bind(function() {
1471
                this.add({id:id, name:"Unknown image", size:-1, progress:100, status:"DELETED"})
1472
            }, this), _.bind(function(image) {
1473
                this.add(image.image);
1474
            }, this));
1475
        },
1476

    
1477
        parse: function (resp, xhr) {
1478
            // FIXME: depricated global var
1479
            var data = _.map(resp.images.values, _.bind(this.parse_meta, this));
1480
            return resp.images.values;
1481
        },
1482

    
1483
        get_meta_key: function(img, key) {
1484
            if (img.metadata && img.metadata.values && img.metadata.values[key]) {
1485
                return img.metadata.values[key];
1486
            }
1487
            return undefined;
1488
        },
1489

    
1490
        comparator: function(img) {
1491
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1492
        },
1493

    
1494
        parse_meta: function(img) {
1495
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1496
                img[key] = this.get_meta_key(img, key) || "";
1497
            }, this));
1498
            return img;
1499
        },
1500

    
1501
        active: function() {
1502
            return this.filter(function(img){return img.get('status') != "DELETED"});
1503
        }
1504
    })
1505

    
1506
    models.Flavors = models.Collection.extend({
1507
        model: models.Flavor,
1508
        path: 'flavors',
1509
        details: true,
1510
        noUpdate: true,
1511
        supportIncUpdates: false,
1512
        // update collection model with id passed
1513
        // making a direct call to the flavor
1514
        // api url
1515
        update_unknown_id: function(id) {
1516
            var url = getUrl.call(this) + "/" + id;
1517
            this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, 
1518
            _.bind(function() {
1519
                this.add({id:id, cpu:"", ram:"", disk:"", name: "", status:"DELETED"})
1520
            }, this), _.bind(function(flv) {
1521
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1522
                this.add(flv.flavor);
1523
            }, this));
1524
        },
1525

    
1526
        parse: function (resp, xhr) {
1527
            // FIXME: depricated global var
1528
            return resp.flavors.values;
1529
        },
1530

    
1531
        comparator: function(flv) {
1532
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1533
        },
1534

    
1535
        unavailable_values_for_image: function(img, flavors) {
1536
            var flavors = flavors || this.active();
1537
            var size = img.get_size();
1538
            
1539
            var index = {cpu:[], disk:[], ram:[]};
1540

    
1541
            _.each(this.active(), function(el) {
1542
                var img_size = size;
1543
                var flv_size = el.get_disk_size();
1544
                if (flv_size < img_size) {
1545
                    if (index.disk.indexOf(flv_size) == -1) {
1546
                        index.disk.push(flv_size);
1547
                    }
1548
                };
1549
            });
1550
            
1551
            return index;
1552
        },
1553

    
1554
        get_flavor: function(cpu, mem, disk, filter_list) {
1555
            if (!filter_list) { filter_list = this.models };
1556

    
1557
            return this.select(function(flv){
1558
                if (flv.get("cpu") == cpu + "" &&
1559
                   flv.get("ram") == mem + "" &&
1560
                   flv.get("disk") == disk + "" &&
1561
                   filter_list.indexOf(flv) > -1) { return true; }
1562
            })[0];
1563
        },
1564
        
1565
        get_data: function(lst) {
1566
            var data = {'cpu': [], 'mem':[], 'disk':[]};
1567

    
1568
            _.each(lst, function(flv) {
1569
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1570
                    data.cpu.push(flv.get("cpu"));
1571
                }
1572
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1573
                    data.mem.push(flv.get("ram"));
1574
                }
1575
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1576
                    data.disk.push(flv.get("disk"));
1577
                }
1578
            })
1579
            
1580
            return data;
1581
        },
1582

    
1583
        active: function() {
1584
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1585
        }
1586
            
1587
    })
1588

    
1589
    models.VMS = models.Collection.extend({
1590
        model: models.VM,
1591
        path: 'servers',
1592
        details: true,
1593
        copy_image_meta: true,
1594
        
1595
        parse: function (resp, xhr) {
1596
            // FIXME: depricated after refactoring
1597
            var data = resp;
1598
            if (!resp) { return [] };
1599
            data = _.filter(_.map(resp.servers.values, _.bind(this.parse_vm_api_data, this)), function(v){return v});
1600
            return data;
1601
        },
1602
        
1603
        get_reboot_required: function() {
1604
            return this.filter(function(vm){return vm.get("reboot_required") == true})
1605
        },
1606

    
1607
        has_pending_actions: function() {
1608
            return this.filter(function(vm){return vm.pending_action}).length > 0;
1609
        },
1610

    
1611
        reset_pending_actions: function() {
1612
            this.each(function(vm) {
1613
                vm.clear_pending_action();
1614
            })
1615
        },
1616

    
1617
        do_all_pending_actions: function(success, error) {
1618
            this.each(function(vm) {
1619
                if (vm.has_pending_action()) {
1620
                    vm.call(vm.pending_action, success, error);
1621
                    vm.clear_pending_action();
1622
                }
1623
            })
1624
        },
1625
        
1626
        do_all_reboots: function(success, error) {
1627
            this.each(function(vm) {
1628
                if (vm.get("reboot_required")) {
1629
                    vm.call("reboot", success, error);
1630
                }
1631
            });
1632
        },
1633

    
1634
        reset_reboot_required: function() {
1635
            this.each(function(vm) {
1636
                vm.set({'reboot_required': undefined});
1637
            })
1638
        },
1639
        
1640
        stop_stats_update: function(exclude) {
1641
            var exclude = exclude || [];
1642
            this.each(function(vm) {
1643
                if (exclude.indexOf(vm) > -1) {
1644
                    return;
1645
                }
1646
                vm.stop_stats_update();
1647
            })
1648
        },
1649
        
1650
        has_meta: function(vm_data) {
1651
            return vm_data.metadata && vm_data.metadata.values
1652
        },
1653

    
1654
        has_addresses: function(vm_data) {
1655
            return vm_data.metadata && vm_data.metadata.values
1656
        },
1657

    
1658
        parse_vm_api_data: function(data) {
1659
            // do not add non existing DELETED entries
1660
            if (data.status && data.status == "DELETED") {
1661
                if (!this.get(data.id)) {
1662
                    console.error("non exising deleted vm", data)
1663
                    return false;
1664
                }
1665
            }
1666

    
1667
            // OS attribute
1668
            if (this.has_meta(data)) {
1669
                data['OS'] = data.metadata.values.OS || "okeanos";
1670
            }
1671
            
1672
            data['firewalls'] = {};
1673
            if (data['addresses'] && data['addresses'].values) {
1674
                data['linked_to_nets'] = data['addresses'].values;
1675
                _.each(data['addresses'].values, function(f){
1676
                    if (f['firewallProfile']) {
1677
                        data['firewalls'][f['id']] = f['firewallProfile']
1678
                    }
1679
                });
1680
            }
1681
            
1682
            // if vm has no metadata, no metadata object
1683
            // is in json response, reset it to force
1684
            // value update
1685
            if (!data['metadata']) {
1686
                data['metadata'] = {values:{}};
1687
            }
1688

    
1689
            return data;
1690
        },
1691

    
1692
        create: function (name, image, flavor, meta, extra, callback) {
1693
            if (this.copy_image_meta) {
1694
                meta['OS'] = image.get("OS");
1695
           }
1696
            
1697
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta}
1698
            opts = _.extend(opts, extra);
1699

    
1700
            this.api.call(this.path, "create", {'server': opts}, undefined, undefined, callback, {critical: false});
1701
        }
1702

    
1703
    })
1704

    
1705
    models.PublicKey = models.Model.extend({
1706
        path: 'keys',
1707
        base_url: '/ui/userdata',
1708
        details: false,
1709
        noUpdate: true,
1710

    
1711

    
1712
        get_public_key: function() {
1713
            return cryptico.publicKeyFromString(this.get("content"));
1714
        },
1715

    
1716
        get_filename: function() {
1717
            return "{0}.pub".format(this.get("name"));
1718
        },
1719

    
1720
        identify_type: function() {
1721
            try {
1722
                var cont = snf.util.validatePublicKey(this.get("content"));
1723
                var type = cont.split(" ")[0];
1724
                return synnefo.util.publicKeyTypesMap[type];
1725
            } catch (err) { return false };
1726
        }
1727

    
1728
    })
1729
    
1730
    models.PublicKeys = models.Collection.extend({
1731
        model: models.PublicKey,
1732
        details: false,
1733
        path: 'keys',
1734
        base_url: '/ui/userdata',
1735
        noUpdate: true,
1736

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

    
1739
        generate_new: function(passphrase, length, save) {
1740

    
1741
            var passphrase = passphrase || "";
1742
            var length = length || 1024;
1743
            var key = cryptico.generateRSAKey(passphrase, length);
1744
            
1745
            var b64enc = $.base64.encode;
1746
            return key;
1747
        },
1748

    
1749
        add_crypto_key: function(key, success, error, options) {
1750
            var options = options || {};
1751
            var m = new models.PublicKey();
1752
            var name_tpl = "public key";
1753
            var name = name_tpl;
1754
            var name_count = 1;
1755
            
1756
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
1757
                name = name_tpl + " " + name_count;
1758
                name_count++;
1759
            }
1760

    
1761
            m.set({name: name});
1762
            m.set({content: "ssh-rsa AAAAB3NzaC1yc2EA" + cryptico.publicKeyString(key)});
1763
            
1764
            options.success = function () { return success(m) };
1765
            options.errror = error;
1766
            
1767
            this.create(m.attributes, options);
1768
        }
1769
    })
1770
    
1771
    // storage initialization
1772
    snf.storage.images = new models.Images();
1773
    snf.storage.flavors = new models.Flavors();
1774
    snf.storage.networks = new models.Networks();
1775
    snf.storage.vms = new models.VMS();
1776
    snf.storage.keys = new models.PublicKeys();
1777

    
1778
    //snf.storage.vms.fetch({update:true});
1779
    //snf.storage.images.fetch({update:true});
1780
    //snf.storage.flavors.fetch({update:true});
1781

    
1782
})(this);