Statistics
| Branch: | Tag: | Revision:

root / snf-app / synnefo / ui / static / snf / js / models.js @ d5ba5588

History | View | Annotate | Download (66.5 kB)

1
// Copyright 2011 GRNET S.A. All rights reserved.
2
// 
3
// Redistribution and use in source and binary forms, with or
4
// without modification, are permitted provided that the following
5
// conditions are met:
6
// 
7
//   1. Redistributions of source code must retain the above
8
//      copyright notice, this list of conditions and the following
9
//      disclaimer.
10
// 
11
//   2. Redistributions in binary form must reproduce the above
12
//      copyright notice, this list of conditions and the following
13
//      disclaimer in the documentation and/or other materials
14
//      provided with the distribution.
15
// 
16
// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
// POSSIBILITY OF SUCH DAMAGE.
28
// 
29
// The views and conclusions contained in the software and
30
// documentation are those of the authors and should not be
31
// interpreted as representing official policies, either expressed
32
// or implied, of GRNET S.A.
33
// 
34

    
35
;(function(root){
36
    
37
    // root
38
    var root = root;
39
    
40
    // setup namepsaces
41
    var snf = root.synnefo = root.synnefo || {};
42
    var models = snf.models = snf.models || {}
43
    var storage = snf.storage = snf.storage || {};
44
    var util = snf.util = snf.util || {};
45

    
46
    // shortcuts
47
    var bb = root.Backbone;
48
    var slice = Array.prototype.slice
49

    
50
    // logging
51
    var logger = new snf.logging.logger("SNF-MODELS");
52
    var debug = _.bind(logger.debug, logger);
53
    
54
    // get url helper
55
    var getUrl = function(baseurl) {
56
        var baseurl = baseurl || snf.config.api_urls[this.api_type];
57
        return baseurl + "/" + this.path;
58
    }
59
    
60
    // i18n
61
    BUILDING_MESSAGES = window.BUILDING_MESSAGES || {'INIT': 'init', 'COPY': '{0}, {1}, {2}', 'FINAL': 'final'};
62

    
63
    // Base object for all our models
64
    models.Model = bb.Model.extend({
65
        sync: snf.api.sync,
66
        api: snf.api,
67
        api_type: 'compute',
68
        has_status: false,
69

    
70
        initialize: function() {
71
            if (this.has_status) {
72
                this.bind("change:status", this.handle_remove);
73
                this.handle_remove();
74
            }
75
            
76
            this.api_call = _.bind(this.api.call, this);
77
            models.Model.__super__.initialize.apply(this, arguments)
78
        },
79

    
80
        handle_remove: function() {
81
            if (this.get("status") == 'DELETED') {
82
                if (this.collection) {
83
                    try { this.clear_pending_action();} catch (err) {};
84
                    try { this.reset_pending_actions();} catch (err) {};
85
                    try { this.stop_stats_update();} catch (err) {};
86
                    this.collection.remove(this.id);
87
                }
88
            }
89
        },
90
        
91
        // custom set method to allow submodels to use
92
        // set_<attr> methods for handling the value of each
93
        // attribute and overriding the default set method
94
        // for specific parameters
95
        set: function(params, options) {
96
            _.each(params, _.bind(function(value, key){
97
                if (this["set_" + key]) {
98
                    params[key] = this["set_" + key](value);
99
                }
100
            }, this))
101
            var ret = bb.Model.prototype.set.call(this, params, options);
102
            return ret;
103
        },
104

    
105
        url: function(options) {
106
            return getUrl.call(this, this.base_url) + "/" + this.id;
107
        },
108

    
109
        api_path: function(options) {
110
            return this.path + "/" + this.id;
111
        },
112

    
113
        parse: function(resp, xhr) {
114
            return resp.server;
115
        },
116

    
117
        remove: function() {
118
            this.api_call(this.api_path(), "delete");
119
        },
120

    
121
        changedKeys: function() {
122
            return _.keys(this.changedAttributes() || {});
123
        },
124

    
125
        hasOnlyChange: function(keys) {
126
            var ret = false;
127
            _.each(keys, _.bind(function(key) {
128
                if (this.changedKeys().length == 1 && this.changedKeys().indexOf(key) > -1) { ret = true};
129
            }, this));
130
            return ret;
131
        }
132

    
133
    })
134
    
135
    // Base object for all our model collections
136
    models.Collection = bb.Collection.extend({
137
        sync: snf.api.sync,
138
        api: snf.api,
139
        api_type: 'compute',
140
        supportIncUpdates: true,
141

    
142
        initialize: function() {
143
            models.Collection.__super__.initialize.apply(this, arguments);
144
            this.api_call = _.bind(this.api.call, this);
145
        },
146

    
147
        url: function(options, method) {
148
            return getUrl.call(this, this.base_url) + (
149
                    options.details || this.details && method != 'create' ? '/detail' : '');
150
        },
151

    
152
        fetch: function(options) {
153
            if (!options) { options = {} };
154
            // default to update
155
            if (!this.noUpdate) {
156
                if (options.update === undefined) { options.update = true };
157
                if (!options.removeMissing && options.refresh) { options.removeMissing = true };
158
            } else {
159
                if (options.refresh === undefined) {
160
                    options.refresh = true;
161
                }
162
            }
163
            // custom event foreach fetch
164
            return bb.Collection.prototype.fetch.call(this, options)
165
        },
166

    
167
        create: function(model, options) {
168
            var coll = this;
169
            options || (options = {});
170
            model = this._prepareModel(model, options);
171
            if (!model) return false;
172
            var success = options.success;
173
            options.success = function(nextModel, resp, xhr) {
174
                if (success) success(nextModel, resp, xhr);
175
            };
176
            model.save(null, options);
177
            return model;
178
        },
179

    
180
        get_fetcher: function(interval, increase, fast, increase_after_calls, max, initial_call, params) {
181
            var fetch_params = params || {};
182
            var handler_options = {};
183

    
184
            fetch_params.skips_timeouts = true;
185
            handler_options.interval = interval;
186
            handler_options.increase = increase;
187
            handler_options.fast = fast;
188
            handler_options.increase_after_calls = increase_after_calls;
189
            handler_options.max= max;
190
            handler_options.id = "collection id";
191

    
192
            var last_ajax = undefined;
193
            var callback = _.bind(function() {
194
                // clone to avoid referenced objects
195
                var params = _.clone(fetch_params);
196
                updater._ajax = last_ajax;
197
                
198
                // wait for previous request to finish
199
                if (last_ajax && last_ajax.readyState < 4 && last_ajax.statusText != "timeout") {
200
                    // opera readystate for 304 responses is 0
201
                    if (!($.browser.opera && last_ajax.readyState == 0 && last_ajax.status == 304)) {
202
                        return;
203
                    }
204
                }
205
                
206
                last_ajax = this.fetch(params);
207
            }, this);
208
            handler_options.callback = callback;
209

    
210
            var updater = new snf.api.updateHandler(_.clone(_.extend(handler_options, fetch_params)));
211
            snf.api.bind("call", _.throttle(_.bind(function(){ updater.faster(true)}, this)), 1000);
212
            return updater;
213
        }
214
    });
215
    
216
    // Image model
217
    models.Image = models.Model.extend({
218
        path: 'images',
219

    
220
        get_size: function() {
221
            return parseInt(this.get('metadata') ? this.get('metadata').values.size : -1)
222
        },
223

    
224
        get_meta: function(key) {
225
            if (this.get('metadata') && this.get('metadata').values && this.get('metadata').values[key]) {
226
                return this.get('metadata').values[key];
227
            }
228
            return undefined;
229
        },
230

    
231
        get_owner: function() {
232
            return this.get('owner') || synnefo.config.system_images_owner;
233
        },
234

    
235
        get_readable_size: function() {
236
            return this.get_size() > 0 ? util.readablizeBytes(this.get_size() * 1024 * 1024) : "unknown";
237
        },
238

    
239
        get_os: function() {
240
            return this.get("OS");
241
        },
242

    
243
        get_created_user: function() {
244
            return synnefo.config.os_created_users[this.get_os()] || "root";
245
        },
246

    
247
        get_sort_order: function() {
248
            return parseInt(this.get('metadata') ? this.get('metadata').values.sortorder : -1)
249
        },
250

    
251
        get_vm: function() {
252
            var vm_id = this.get("serverRef");
253
            var vm = undefined;
254
            vm = storage.vms.get(vm_id);
255
            return vm;
256
        },
257

    
258
        is_public: function() {
259
            return this.get('is_public') || true;
260
        },
261
        
262
        ssh_keys_path: function() {
263
            prepend = '';
264
            if (this.get_created_user() != 'root') {
265
                prepend = '/home'
266
            }
267
            return '{1}/{0}/.ssh/authorized_keys'.format(this.get_created_user(), prepend);
268
        },
269

    
270
        _supports_ssh: function() {
271
            if (synnefo.config.support_ssh_os_list.indexOf(this.get_os()) > -1) {
272
                return true;
273
            }
274
            return false;
275
        },
276

    
277
        supports: function(feature) {
278
            if (feature == "ssh") {
279
                return this._supports_ssh()
280
            }
281
            return false;
282
        },
283

    
284
        personality_data_for_keys: function(keys) {
285
            contents = '';
286
            _.each(keys, function(key){
287
                contents = contents + key.get("content") + "\n"
288
            });
289
            contents = $.base64.encode(contents);
290

    
291
            return {
292
                path: this.ssh_keys_path(),
293
                contents: contents
294
            }
295
        }
296
    });
297

    
298
    // Flavor model
299
    models.Flavor = models.Model.extend({
300
        path: 'flavors',
301

    
302
        details_string: function() {
303
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
304
        },
305

    
306
        get_disk_size: function() {
307
            return parseInt(this.get("disk") * 1000)
308
        },
309

    
310
        get_disk_template_info: function() {
311
            var info = snf.config.flavors_disk_templates_info[this.get("disk_template")];
312
            if (!info) {
313
                info = { name: this.get("disk_template"), description:'' };
314
            }
315
            return info
316
        }
317

    
318
    });
319
    
320
    //network vms list helper
321
    var NetworkVMSList = function() {
322
        this.initialize = function() {
323
            this.vms = [];
324
            this.pending = [];
325
            this.pending_for_removal = [];
326
        }
327
        
328
        this.add_pending_for_remove = function(vm_id) {
329
            if (this.pending_for_removal.indexOf(vm_id) == -1) {
330
                this.pending_for_removal.push(vm_id);
331
            }
332

    
333
            if (this.pending_for_removal.length) {
334
                this.trigger("pending:remove:add");
335
            }
336
        },
337

    
338
        this.add_pending = function(vm_id) {
339
            if (this.pending.indexOf(vm_id) == -1) {
340
                this.pending[this.pending.length] = vm_id;
341
            }
342

    
343
            if (this.pending.length) {
344
                this.trigger("pending:add");
345
            }
346
        }
347

    
348
        this.check_pending = function() {
349
            var len = this.pending.length;
350
            var args = [this.pending];
351
            this.pending = _.difference(this.pending, this.vms);
352
            if (len != this.pending.length) {
353
                if (this.pending.length == 0) {
354
                    this.trigger("pending:clear");
355
                }
356
            }
357

    
358
            var len = this.pending_for_removal.length;
359
            this.pending_for_removal = _.intersection(this.pending_for_removal, this.vms);
360
            if (this.pending_for_removal.length == 0) {
361
                this.trigger("pending:remove:clear");
362
            }
363

    
364
        }
365

    
366

    
367
        this.add = function(vm_id) {
368
            if (this.vms.indexOf(vm_id) == -1) {
369
                this.vms[this.vms.length] = vm_id;
370
                this.trigger("network:connect", vm_id);
371
                this.check_pending();
372
                return true;
373
            }
374
        }
375

    
376
        this.remove = function(vm_id) {
377
            if (this.vms.indexOf(vm_id) > -1) {
378
                this.vms = _.without(this.vms, vm_id);
379
                this.trigger("network:disconnect", vm_id);
380
                this.check_pending();
381
                return true;
382
            }
383
        }
384

    
385
        this.get = function() {
386
            return this.vms;
387
        }
388

    
389
        this.list = function() {
390
            return storage.vms.filter(_.bind(function(vm){
391
                return this.vms.indexOf(vm.id) > -1;
392
            }, this))
393
        }
394

    
395
        this.initialize();
396
    };
397
    _.extend(NetworkVMSList.prototype, bb.Events);
398
    
399
    // vm networks list helper
400
    var VMNetworksList = function() {
401
        this.initialize = function() {
402
            this.networks = {};
403
            this.network_ids = [];
404
        }
405

    
406
        this.add = function(net_id, data) {
407
            if (!this.networks[net_id]) {
408
                this.networks[net_id] = data || {};
409
                this.network_ids[this.network_ids.length] = net_id;
410
                this.trigger("network:connect", net_id);
411
                return true;
412
            }
413
        }
414

    
415
        this.remove = function(net_id) {
416
            if (this.networks[net_id]) {
417
                delete this.networks[net_id];
418
                this.network_ids = _.without(this.network_ids, net_id);
419
                this.trigger("network:disconnect", net_id);
420
                return true;
421
            }
422
            return false;
423
        }
424

    
425
        this.get = function() {
426
            return this.networks;
427
        }
428

    
429
        this.list = function() {
430
            return storage.networks.filter(_.bind(function(net){
431
                return this.network_ids.indexOf(net.id) > -1;
432
            }, this))
433
        }
434

    
435
        this.initialize();
436
    };
437
    _.extend(VMNetworksList.prototype, bb.Events);
438
        
439
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
440
    _.extend(models.ParamsList.prototype, bb.Events, {
441

    
442
        initialize: function(parent, param_name) {
443
            this.parent = parent;
444
            this.actions = {};
445
            this.param_name = param_name;
446
            this.length = 0;
447
        },
448
        
449
        has_action: function(action) {
450
            return this.actions[action] ? true : false;
451
        },
452
            
453
        _parse_params: function(arguments) {
454
            if (arguments.length <= 1) {
455
                return [];
456
            }
457

    
458
            var args = _.toArray(arguments);
459
            return args.splice(1);
460
        },
461

    
462
        contains: function(action, params) {
463
            params = this._parse_params(arguments);
464
            var has_action = this.has_action(action);
465
            if (!has_action) { return false };
466

    
467
            var paramsEqual = false;
468
            _.each(this.actions[action], function(action_params) {
469
                if (_.isEqual(action_params, params)) {
470
                    paramsEqual = true;
471
                }
472
            });
473
                
474
            return paramsEqual;
475
        },
476
        
477
        is_empty: function() {
478
            return _.isEmpty(this.actions);
479
        },
480

    
481
        add: function(action, params) {
482
            params = this._parse_params(arguments);
483
            if (this.contains.apply(this, arguments)) { return this };
484
            var isnew = false
485
            if (!this.has_action(action)) {
486
                this.actions[action] = [];
487
                isnew = true;
488
            };
489

    
490
            this.actions[action].push(params);
491
            this.parent.trigger("change:" + this.param_name, this.parent, this);
492
            if (isnew) {
493
                this.trigger("add", action, params);
494
            } else {
495
                this.trigger("change", action, params);
496
            }
497
            return this;
498
        },
499
        
500
        remove_all: function(action) {
501
            if (this.has_action(action)) {
502
                delete this.actions[action];
503
                this.parent.trigger("change:" + this.param_name, this.parent, this);
504
                this.trigger("remove", action);
505
            }
506
            return this;
507
        },
508

    
509
        reset: function() {
510
            this.actions = {};
511
            this.parent.trigger("change:" + this.param_name, this.parent, this);
512
            this.trigger("reset");
513
            this.trigger("remove");
514
        },
515

    
516
        remove: function(action, params) {
517
            params = this._parse_params(arguments);
518
            if (!this.has_action(action)) { return this };
519
            var index = -1;
520
            _.each(this.actions[action], _.bind(function(action_params) {
521
                if (_.isEqual(action_params, params)) {
522
                    index = this.actions[action].indexOf(action_params);
523
                }
524
            }, this));
525
            
526
            if (index > -1) {
527
                this.actions[action].splice(index, 1);
528
                if (_.isEmpty(this.actions[action])) {
529
                    delete this.actions[action];
530
                }
531
                this.parent.trigger("change:" + this.param_name, this.parent, this);
532
                this.trigger("remove", action, params);
533
            }
534
        }
535

    
536
    });
537

    
538
    // Image model
539
    models.Network = models.Model.extend({
540
        path: 'networks',
541
        has_status: true,
542
        
543
        initialize: function() {
544
            this.vms = new NetworkVMSList();
545
            this.vms.bind("pending:add", _.bind(this.handle_pending_connections, this, "add"));
546
            this.vms.bind("pending:clear", _.bind(this.handle_pending_connections, this, "clear"));
547
            this.vms.bind("pending:remove:add", _.bind(this.handle_pending_connections, this, "add"));
548
            this.vms.bind("pending:remove:clear", _.bind(this.handle_pending_connections, this, "clear"));
549

    
550
            var ret = models.Network.__super__.initialize.apply(this, arguments);
551

    
552
            storage.vms.bind("change:linked_to_nets", _.bind(this.update_connections, this, "vm:change"));
553
            storage.vms.bind("add", _.bind(this.update_connections, this, "add"));
554
            storage.vms.bind("remove", _.bind(this.update_connections, this, "remove"));
555
            storage.vms.bind("reset", _.bind(this.update_connections, this, "reset"));
556

    
557
            this.bind("change:linked_to", _.bind(this.update_connections, this, "net:change"));
558
            this.update_connections();
559
            this.update_state();
560
            
561
            this.set({"actions": new models.ParamsList(this, "actions")});
562

    
563
            return ret;
564
        },
565

    
566
        toJSON: function() {
567
            var attrs = _.clone(this.attributes);
568
            attrs.actions = _.clone(this.get("actions").actions);
569
            return attrs;
570
        },
571

    
572
        update_state: function() {
573
            if (this.vms.pending.length) {
574
                this.set({state: "CONNECTING"});
575
                return
576
            }
577

    
578
            if (this.vms.pending_for_removal.length) {
579
                this.set({state: "DISCONNECTING"});
580
                return
581
            }   
582
            
583
            var firewalling = false;
584
            _.each(this.vms.get(), _.bind(function(vm_id){
585
                var vm = storage.vms.get(vm_id);
586
                if (!vm) { return };
587
                if (!_.isEmpty(vm.pending_firewalls)) {
588
                    this.set({state:"FIREWALLING"});
589
                    firewalling = true;
590
                    return false;
591
                }
592
            },this));
593
            if (firewalling) { return };
594

    
595
            this.set({state:"NORMAL"});
596
        },
597

    
598
        handle_pending_connections: function(action) {
599
            this.update_state();
600
        },
601

    
602
        // handle vm/network connections
603
        update_connections: function(action, model) {
604
            
605
            // vm removed disconnect vm from network
606
            if (action == "remove") {
607
                var removed_from_net = this.vms.remove(model.id);
608
                var removed_from_vm = model.networks.remove(this.id);
609
                if (removed_from_net) {this.trigger("vm:disconnect", model, this); this.change()};
610
                if (removed_from_vm) {model.trigger("network:disconnect", this, model); this.change()};
611
                return;
612
            }
613
            
614
            // update links for all vms
615
            var links = this.get("linked_to");
616
            storage.vms.each(_.bind(function(vm) {
617
                var vm_links = vm.get("linked_to") || [];
618
                if (vm_links.indexOf(this.id) > -1) {
619
                    // vm has connection to current network
620
                    if (links.indexOf(vm.id) > -1) {
621
                        // and network has connection to vm, so try
622
                        // to append it
623
                        var add_to_net = this.vms.add(vm.id);
624
                        var index = _.indexOf(vm_links, this.id);
625
                        var add_to_vm = vm.networks.add(this.id, vm.get("linked_to_nets")[index]);
626
                        
627
                        // call only if connection did not existed
628
                        if (add_to_net) {this.trigger("vm:connect", vm, this); this.change()};
629
                        if (add_to_vm) {vm.trigger("network:connect", this, vm); vm.change()};
630
                    } else {
631
                        // no connection, try to remove it
632
                        var removed_from_net = this.vms.remove(vm.id);
633
                        var removed_from_vm = vm.networks.remove(this.id);
634
                        if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
635
                        if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
636
                    }
637
                } else {
638
                    // vm has no connection to current network, try to remove it
639
                    var removed_from_net = this.vms.remove(vm.id);
640
                    var removed_from_vm = vm.networks.remove(this.id);
641
                    if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
642
                    if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
643
                }
644
            },this));
645
        },
646

    
647
        is_public: function() {
648
            return this.id == "public";
649
        },
650

    
651
        contains_vm: function(vm) {
652
            var net_vm_exists = this.vms.get().indexOf(vm.id) > -1;
653
            var vm_net_exists = vm.is_connected_to(this);
654
            return net_vm_exists && vm_net_exists;
655
        },
656
        
657
        call: function(action, params, success, error) {
658
            if (action == "destroy") {
659
                this.set({state:"DESTROY"});
660
                this.get("actions").remove("destroy");
661
                this.remove(_.bind(function(){
662
                    success();
663
                }, this), error);
664
            }
665
            
666
            if (action == "disconnect") {
667
                _.each(params, _.bind(function(vm_id) {
668
                    var vm = snf.storage.vms.get(vm_id);
669
                    this.get("actions").remove("disconnect", vm_id);
670
                    if (vm) {
671
                        this.remove_vm(vm, success, error);
672
                    }
673
                }, this));
674
            }
675
        },
676

    
677
        add_vm: function (vm, callback, error, options) {
678
            var payload = {add:{serverRef:"" + vm.id}};
679
            payload._options = options || {};
680
            return this.api_call(this.api_path() + "/action", "create", 
681
                                 payload,
682
                                 _.bind(function(){
683
                                     this.vms.add_pending(vm.id);
684
                                     if (callback) {callback()}
685
                                 },this), error);
686
        },
687

    
688
        remove_vm: function (vm, callback, error, options) {
689
            var payload = {remove:{serverRef:"" + vm.id}};
690
            payload._options = options || {};
691
            return this.api_call(this.api_path() + "/action", "create", 
692
                                 {remove:{serverRef:"" + vm.id}},
693
                                 _.bind(function(){
694
                                     this.vms.add_pending_for_remove(vm.id);
695
                                     if (callback) {callback()}
696
                                 },this), error);
697
        },
698

    
699
        rename: function(name, callback) {
700
            return this.api_call(this.api_path(), "update", {
701
                network:{name:name}, 
702
                _options:{
703
                    critical: false, 
704
                    error_params:{
705
                        title: "Network action failed",
706
                        ns: "Networks",
707
                        extra_details: {"Network id": this.id}
708
                    }
709
                }}, callback);
710
        },
711

    
712
        get_connectable_vms: function() {
713
            var servers = this.vms.list();
714
            return storage.vms.filter(function(vm){
715
                return servers.indexOf(vm) == -1 && !vm.in_error_state();
716
            })
717
        },
718

    
719
        state_message: function() {
720
            if (this.get("state") == "NORMAL" && this.is_public()) {
721
                return "Public network";
722
            }
723

    
724
            return models.Network.STATES[this.get("state")];
725
        },
726

    
727
        in_progress: function() {
728
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
729
        },
730

    
731
        do_all_pending_actions: function(success, error) {
732
            var destroy = this.get("actions").has_action("destroy");
733
            _.each(this.get("actions").actions, _.bind(function(params, action) {
734
                _.each(params, _.bind(function(with_params) {
735
                    this.call(action, with_params, success, error);
736
                }, this));
737
            }, this));
738
        }
739
    });
740
    
741
    models.Network.STATES = {
742
        'NORMAL': 'Private network',
743
        'CONNECTING': 'Connecting...',
744
        'DISCONNECTING': 'Disconnecting...',
745
        'FIREWALLING': 'Firewall update...',
746
        'DESTROY': 'Destroying...'
747
    }
748

    
749
    models.Network.STATES_TRANSITIONS = {
750
        'CONNECTING': ['NORMAL'],
751
        'DISCONNECTING': ['NORMAL'],
752
        'FIREWALLING': ['NORMAL']
753
    }
754

    
755
    // Virtualmachine model
756
    models.VM = models.Model.extend({
757

    
758
        path: 'servers',
759
        has_status: true,
760
        initialize: function(params) {
761
            this.networks = new VMNetworksList();
762
            
763
            this.pending_firewalls = {};
764
            
765
            models.VM.__super__.initialize.apply(this, arguments);
766

    
767
            this.set({state: params.status || "ERROR"});
768
            this.log = new snf.logging.logger("VM " + this.id);
769
            this.pending_action = undefined;
770
            
771
            // init stats parameter
772
            this.set({'stats': undefined}, {silent: true});
773
            // defaults to not update the stats
774
            // each view should handle this vm attribute 
775
            // depending on if it displays stat images or not
776
            this.do_update_stats = false;
777
            
778
            // interval time
779
            // this will dynamicaly change if the server responds that
780
            // images get refreshed on different intervals
781
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
782
            this.stats_available = false;
783

    
784
            // initialize interval
785
            this.init_stats_intervals(this.stats_update_interval);
786
            
787
            this.bind("change:progress", _.bind(this.update_building_progress, this));
788
            this.update_building_progress();
789

    
790
            this.bind("change:firewalls", _.bind(this.handle_firewall_change, this));
791
            
792
            // default values
793
            this.set({linked_to_nets:this.get("linked_to_nets") || []});
794
            this.set({firewalls:this.get("firewalls") || []});
795

    
796
            this.bind("change:state", _.bind(function(){if (this.state() == "DESTROY") { this.handle_destroy() }}, this))
797
        },
798

    
799
        handle_firewall_change: function() {
800

    
801
        },
802
        
803
        set_linked_to_nets: function(data) {
804
            this.set({"linked_to":_.map(data, function(n){ return n.id})});
805
            return data;
806
        },
807

    
808
        is_connected_to: function(net) {
809
            return _.filter(this.networks.list(), function(n){return n.id == net.id}).length > 0;
810
        },
811
        
812
        status: function(st) {
813
            if (!st) { return this.get("status")}
814
            return this.set({status:st});
815
        },
816

    
817
        set_status: function(st) {
818
            var new_state = this.state_for_api_status(st);
819
            var transition = false;
820

    
821
            if (this.state() != new_state) {
822
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
823
                    transition = this.state();
824
                }
825
            }
826
            
827
            // call it silently to avoid double change trigger
828
            this.set({'state': this.state_for_api_status(st)}, {silent: true});
829
            
830
            // trigger transition
831
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
832
                this.trigger("transition", {from:transition, to:new_state}) 
833
            };
834
            return st;
835
        },
836

    
837
        update_building_progress: function() {
838
            if (this.is_building()) {
839
                var progress = this.get("progress");
840
                if (progress == 0) {
841
                    this.state("BUILD_INIT");
842
                    this.set({progress_message: BUILDING_MESSAGES['INIT']});
843
                }
844
                if (progress > 0 && progress < 99) {
845
                    this.state("BUILD_COPY");
846
                    this.get_copy_details(true, undefined, _.bind(function(details){
847
                        this.set({
848
                            progress_message: BUILDING_MESSAGES['COPY'].format(details.copy, 
849
                                                                               details.size, 
850
                                                                               details.progress)
851
                        });
852
                    }, this));
853
                }
854
                if (progress == 100) {
855
                    this.state("BUILD_FINAL");
856
                    this.set({progress_message: BUILDING_MESSAGES['FINAL']});
857
                }
858
            } else {
859
            }
860
        },
861

    
862
        get_copy_details: function(human, image, callback) {
863
            var human = human || false;
864
            var image = image || this.get_image(_.bind(function(image){
865
                var progress = this.get('progress');
866
                var size = image.get_size();
867
                var size_copied = (size * progress / 100).toFixed(2);
868
                
869
                if (human) {
870
                    size = util.readablizeBytes(size*1024*1024);
871
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
872
                }
873

    
874
                callback({'progress': progress, 'size': size, 'copy': size_copied})
875
            }, this));
876
        },
877

    
878
        start_stats_update: function(force_if_empty) {
879
            var prev_state = this.do_update_stats;
880

    
881
            this.do_update_stats = true;
882
            
883
            // fetcher initialized ??
884
            if (!this.stats_fetcher) {
885
                this.init_stats_intervals();
886
            }
887

    
888

    
889
            // fetcher running ???
890
            if (!this.stats_fetcher.running || !prev_state) {
891
                this.stats_fetcher.start();
892
            }
893

    
894
            if (force_if_empty && this.get("stats") == undefined) {
895
                this.update_stats(true);
896
            }
897
        },
898

    
899
        stop_stats_update: function(stop_calls) {
900
            this.do_update_stats = false;
901

    
902
            if (stop_calls) {
903
                this.stats_fetcher.stop();
904
            }
905
        },
906

    
907
        // clear and reinitialize update interval
908
        init_stats_intervals: function (interval) {
909
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
910
            this.stats_fetcher.start();
911
        },
912
        
913
        get_stats_fetcher: function(timeout) {
914
            var cb = _.bind(function(data){
915
                this.update_stats();
916
            }, this);
917
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
918
            return fetcher;
919
        },
920

    
921
        // do the api call
922
        update_stats: function(force) {
923
            // do not update stats if flag not set
924
            if ((!this.do_update_stats && !force) || this.updating_stats) {
925
                return;
926
            }
927

    
928
            // make the api call, execute handle_stats_update on sucess
929
            // TODO: onError handler ???
930
            stats_url = this.url() + "/stats";
931
            this.updating_stats = true;
932
            this.sync("read", this, {
933
                handles_error:true, 
934
                url: stats_url, 
935
                refresh:true, 
936
                success: _.bind(this.handle_stats_update, this),
937
                error: _.bind(this.handle_stats_error, this),
938
                complete: _.bind(function(){this.updating_stats = false;}, this),
939
                critical: false,
940
                log_error: false
941
            });
942
        },
943

    
944
        get_stats_image: function(stat, type) {
945
        },
946
        
947
        _set_stats: function(stats) {
948
            var silent = silent === undefined ? false : silent;
949
            // unavailable stats while building
950
            if (this.get("status") == "BUILD") { 
951
                this.stats_available = false;
952
            } else { this.stats_available = true; }
953

    
954
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
955
            
956
            this.set({stats: stats}, {silent:true});
957
            this.trigger("stats:update", stats);
958
        },
959

    
960
        unbind: function() {
961
            models.VM.__super__.unbind.apply(this, arguments);
962
        },
963

    
964
        handle_stats_error: function() {
965
            stats = {};
966
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
967
                stats[k] = false;
968
            });
969

    
970
            this.set({'stats': stats});
971
        },
972

    
973
        // this method gets executed after a successful vm stats api call
974
        handle_stats_update: function(data) {
975
            var self = this;
976
            // avoid browser caching
977
            
978
            if (data.stats && _.size(data.stats) > 0) {
979
                var ts = $.now();
980
                var stats = data.stats;
981
                var images_loaded = 0;
982
                var images = {};
983

    
984
                function check_images_loaded() {
985
                    images_loaded++;
986

    
987
                    if (images_loaded == 4) {
988
                        self._set_stats(images);
989
                    }
990
                }
991
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
992
                    
993
                    stats[k] = stats[k] + "?_=" + ts;
994
                    
995
                    var stat = k.slice(0,3);
996
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
997
                    var img = $("<img />");
998
                    var val = stats[k];
999
                    
1000
                    // load stat image to a temporary dom element
1001
                    // update model stats on image load/error events
1002
                    img.load(function() {
1003
                        images[k] = val;
1004
                        check_images_loaded();
1005
                    });
1006

    
1007
                    img.error(function() {
1008
                        images[stat + type] = false;
1009
                        check_images_loaded();
1010
                    });
1011

    
1012
                    img.attr({'src': stats[k]});
1013
                })
1014
                data.stats = stats;
1015
            }
1016

    
1017
            // do we need to change the interval ??
1018
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
1019
                this.stats_update_interval = data.stats.refresh * 1000;
1020
                this.stats_fetcher.interval = this.stats_update_interval;
1021
                this.stats_fetcher.maximum_interval = this.stats_update_interval;
1022
                this.stats_fetcher.stop();
1023
                this.stats_fetcher.start(false);
1024
            }
1025
        },
1026

    
1027
        // helper method that sets the do_update_stats
1028
        // in the future this method could also make an api call
1029
        // immediaetly if needed
1030
        enable_stats_update: function() {
1031
            this.do_update_stats = true;
1032
        },
1033
        
1034
        handle_destroy: function() {
1035
            this.stats_fetcher.stop();
1036
        },
1037

    
1038
        require_reboot: function() {
1039
            if (this.is_active()) {
1040
                this.set({'reboot_required': true});
1041
            }
1042
        },
1043
        
1044
        set_pending_action: function(data) {
1045
            this.pending_action = data;
1046
            return data;
1047
        },
1048

    
1049
        // machine has pending action
1050
        update_pending_action: function(action, force) {
1051
            this.set({pending_action: action});
1052
        },
1053

    
1054
        clear_pending_action: function() {
1055
            this.set({pending_action: undefined});
1056
        },
1057

    
1058
        has_pending_action: function() {
1059
            return this.get("pending_action") ? this.get("pending_action") : false;
1060
        },
1061
        
1062
        // machine is active
1063
        is_active: function() {
1064
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
1065
        },
1066
        
1067
        // machine is building 
1068
        is_building: function() {
1069
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
1070
        },
1071
        
1072
        in_error_state: function() {
1073
            return this.state() === "ERROR"
1074
        },
1075

    
1076
        // user can connect to machine
1077
        is_connectable: function() {
1078
            // check if ips exist
1079
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
1080
                return false;
1081
            }
1082
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1083
        },
1084
        
1085
        set_firewalls: function(data) {
1086
            _.each(data, _.bind(function(val, key){
1087
                if (this.pending_firewalls && this.pending_firewalls[key] && this.pending_firewalls[key] == val) {
1088
                        this.require_reboot();
1089
                        this.remove_pending_firewall(key, val);
1090
                }
1091
            }, this));
1092
            return data;
1093
        },
1094

    
1095
        remove_pending_firewall: function(net_id, value) {
1096
            if (this.pending_firewalls[net_id] == value) {
1097
                delete this.pending_firewalls[net_id];
1098
                storage.networks.get(net_id).update_state();
1099
            }
1100
        },
1101
            
1102
        remove_meta: function(key, complete, error) {
1103
            var url = this.api_path() + "/meta/" + key;
1104
            this.api_call(url, "delete", undefined, complete, error);
1105
        },
1106

    
1107
        save_meta: function(meta, complete, error) {
1108
            var url = this.api_path() + "/meta/" + meta.key;
1109
            var payload = {meta:{}};
1110
            payload.meta[meta.key] = meta.value;
1111
            payload._options = {
1112
                critical:false, 
1113
                error_params: {
1114
                    title: "Machine metadata error",
1115
                    extra_details: {"Machine id": this.id}
1116
            }};
1117

    
1118
            this.api_call(url, "update", payload, complete, error);
1119
        },
1120

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

    
1124
            this.pending_firewalls[net_id] = value;
1125
            this.trigger("change", this, this);
1126
            var payload = {"firewallProfile":{"profile":value}};
1127
            payload._options = _.extend({critical: false}, options);
1128
            
1129
            // reset firewall state on error
1130
            var error_cb = _.bind(function() {
1131
                thi
1132
            }, this);
1133

    
1134
            this.api_call(this.api_path() + "/action", "create", payload, callback, error);
1135
            storage.networks.get(net_id).update_state();
1136
        },
1137

    
1138
        firewall_pending: function(net_id) {
1139
            return this.pending_firewalls[net_id] != undefined;
1140
        },
1141
        
1142
        // update/get the state of the machine
1143
        state: function() {
1144
            var args = slice.call(arguments);
1145
                
1146
            // TODO: it might not be a good idea to set the state in set_state method
1147
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1148
                this.set({'state': args[0]});
1149
            }
1150

    
1151
            return this.get('state');
1152
        },
1153
        
1154
        // get the state that the api status corresponds to
1155
        state_for_api_status: function(status) {
1156
            return this.state_transition(this.state(), status);
1157
        },
1158
        
1159
        // vm state equals vm api status
1160
        state_is_status: function(state) {
1161
            return models.VM.STATUSES.indexOf(state) != -1;
1162
        },
1163
        
1164
        // get transition state for the corresponging api status
1165
        state_transition: function(state, new_status) {
1166
            var statuses = models.VM.STATES_TRANSITIONS[state];
1167
            if (statuses) {
1168
                if (statuses.indexOf(new_status) > -1) {
1169
                    return new_status;
1170
                } else {
1171
                    return state;
1172
                }
1173
            } else {
1174
                return new_status;
1175
            }
1176
        },
1177
        
1178
        // the current vm state is a transition state
1179
        in_transition: function() {
1180
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
1181
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
1182
        },
1183
        
1184
        // get image object
1185
        get_image: function(callback) {
1186
            var image = storage.images.get(this.get('imageRef'));
1187
            if (!image) {
1188
                storage.images.update_unknown_id(this.get('imageRef'), callback);
1189
                return;
1190
            }
1191
            callback(image);
1192
            return image;
1193
        },
1194
        
1195
        // get flavor object
1196
        get_flavor: function() {
1197
            var flv = storage.flavors.get(this.get('flavorRef'));
1198
            if (!flv) {
1199
                storage.flavors.update_unknown_id(this.get('flavorRef'));
1200
                flv = storage.flavors.get(this.get('flavorRef'));
1201
            }
1202
            return flv;
1203
        },
1204

    
1205
        // retrieve the metadata object
1206
        get_meta: function() {
1207
            try {
1208
                return this.get('metadata').values
1209
            } catch (err) {
1210
                return {};
1211
            }
1212
        },
1213
        
1214
        // get metadata OS value
1215
        get_os: function() {
1216
            return this.get_meta().OS || (this.get_image(function(){}) ? 
1217
                                          this.get_image(function(){}).get_os() || "okeanos" : "okeanos");
1218
        },
1219

    
1220
        // get public ip addresses
1221
        // TODO: public network is always the 0 index ???
1222
        get_addresses: function(net_id) {
1223
            var net_id = net_id || "public";
1224
            
1225
            var info = this.get_network_info(net_id);
1226
            if (!info) { return {} };
1227
            addrs = {};
1228
            _.each(info.values, function(addr) {
1229
                addrs["ip" + addr.version] = addr.addr;
1230
            });
1231
            return addrs
1232
        },
1233

    
1234
        get_network_info: function(net_id) {
1235
            var net_id = net_id || "public";
1236
            
1237
            if (!this.networks.network_ids.length) { return {} };
1238

    
1239
            var addresses = this.networks.get();
1240
            try {
1241
                return _.select(addresses, function(net, key){return key == net_id })[0];
1242
            } catch (err) {
1243
                //this.log.debug("Cannot find network {0}".format(net_id))
1244
            }
1245
        },
1246

    
1247
        firewall_profile: function(net_id) {
1248
            var net_id = net_id || "public";
1249
            var firewalls = this.get("firewalls");
1250
            return firewalls[net_id];
1251
        },
1252

    
1253
        has_firewall: function(net_id) {
1254
            var net_id = net_id || "public";
1255
            return ["ENABLED","PROTECTED"].indexOf(this.firewall_profile()) > -1;
1256
        },
1257
    
1258
        // get actions that the user can execute
1259
        // depending on the vm state/status
1260
        get_available_actions: function() {
1261
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1262
        },
1263

    
1264
        set_profile: function(profile, net_id) {
1265
        },
1266
        
1267
        // call rename api
1268
        rename: function(new_name) {
1269
            //this.set({'name': new_name});
1270
            this.sync("update", this, {
1271
                critical: true,
1272
                data: {
1273
                    'server': {
1274
                        'name': new_name
1275
                    }
1276
                }, 
1277
                // do the rename after the method succeeds
1278
                success: _.bind(function(){
1279
                    //this.set({name: new_name});
1280
                    snf.api.trigger("call");
1281
                }, this)
1282
            });
1283
        },
1284
        
1285
        get_console_url: function(data) {
1286
            var url_params = {
1287
                machine: this.get("name"),
1288
                host_ip: this.get_addresses().ip4,
1289
                host_ip_v6: this.get_addresses().ip6,
1290
                host: data.host,
1291
                port: data.port,
1292
                password: data.password
1293
            }
1294
            return '/machines/console?' + $.param(url_params);
1295
        },
1296

    
1297
        // action helper
1298
        call: function(action_name, success, error) {
1299
            var id_param = [this.id];
1300

    
1301
            success = success || function() {};
1302
            error = error || function() {};
1303

    
1304
            var self = this;
1305

    
1306
            switch(action_name) {
1307
                case 'start':
1308
                    this.__make_api_call(this.get_action_url(), // vm actions url
1309
                                         "create", // create so that sync later uses POST to make the call
1310
                                         {start:{}}, // payload
1311
                                         function() {
1312
                                             // set state after successful call
1313
                                             self.state("START"); 
1314
                                             success.apply(this, arguments);
1315
                                             snf.api.trigger("call");
1316
                                         },  
1317
                                         error, 'start');
1318
                    break;
1319
                case 'reboot':
1320
                    this.__make_api_call(this.get_action_url(), // vm actions url
1321
                                         "create", // create so that sync later uses POST to make the call
1322
                                         {reboot:{type:"HARD"}}, // payload
1323
                                         function() {
1324
                                             // set state after successful call
1325
                                             self.state("REBOOT"); 
1326
                                             success.apply(this, arguments)
1327
                                             snf.api.trigger("call");
1328
                                             self.set({'reboot_required': false});
1329
                                         },
1330
                                         error, 'reboot');
1331
                    break;
1332
                case 'shutdown':
1333
                    this.__make_api_call(this.get_action_url(), // vm actions url
1334
                                         "create", // create so that sync later uses POST to make the call
1335
                                         {shutdown:{}}, // payload
1336
                                         function() {
1337
                                             // set state after successful call
1338
                                             self.state("SHUTDOWN"); 
1339
                                             success.apply(this, arguments)
1340
                                             snf.api.trigger("call");
1341
                                         },  
1342
                                         error, 'shutdown');
1343
                    break;
1344
                case 'console':
1345
                    this.__make_api_call(this.url() + "/action", "create", {'console': {'type':'vnc'}}, function(data) {
1346
                        var cons_data = data.console;
1347
                        success.apply(this, [cons_data]);
1348
                    }, undefined, 'console')
1349
                    break;
1350
                case 'destroy':
1351
                    this.__make_api_call(this.url(), // vm actions url
1352
                                         "delete", // create so that sync later uses POST to make the call
1353
                                         undefined, // payload
1354
                                         function() {
1355
                                             // set state after successful call
1356
                                             self.state('DESTROY');
1357
                                             success.apply(this, arguments)
1358
                                         },  
1359
                                         error, 'destroy');
1360
                    break;
1361
                default:
1362
                    throw "Invalid VM action ("+action_name+")";
1363
            }
1364
        },
1365
        
1366
        __make_api_call: function(url, method, data, success, error, action) {
1367
            var self = this;
1368
            error = error || function(){};
1369
            success = success || function(){};
1370

    
1371
            var params = {
1372
                url: url,
1373
                data: data,
1374
                success: function(){ self.handle_action_succeed.apply(self, arguments); success.apply(this, arguments)},
1375
                error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)},
1376
                error_params: { ns: "Machines actions", 
1377
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1378
                                extra_details: { 'Machine ID': this.id, 'URL': url, 'Action': action || "undefined" },
1379
                                allow_reload: false
1380
                              },
1381
                display: false,
1382
                critical: false
1383
            }
1384
            this.sync(method, this, params);
1385
        },
1386

    
1387
        handle_action_succeed: function() {
1388
            this.trigger("action:success", arguments);
1389
        },
1390
        
1391
        reset_action_error: function() {
1392
            this.action_error = false;
1393
            this.trigger("action:fail:reset", this.action_error);
1394
        },
1395

    
1396
        handle_action_fail: function() {
1397
            this.action_error = arguments;
1398
            this.trigger("action:fail", arguments);
1399
        },
1400

    
1401
        get_action_url: function(name) {
1402
            return this.url() + "/action";
1403
        },
1404

    
1405
        get_connection_info: function(host_os, success, error) {
1406
            var url = "/machines/connect";
1407
            params = {
1408
                ip_address: this.get_addresses().ip4,
1409
                os: this.get_os(),
1410
                host_os: host_os,
1411
                srv: this.id
1412
            }
1413

    
1414
            url = url + "?" + $.param(params);
1415

    
1416
            var ajax = snf.api.sync("read", undefined, { url: url, 
1417
                                                         error:error, 
1418
                                                         success:success, 
1419
                                                         handles_error:1});
1420
        }
1421
    })
1422
    
1423
    models.VM.ACTIONS = [
1424
        'start',
1425
        'shutdown',
1426
        'reboot',
1427
        'console',
1428
        'destroy'
1429
    ]
1430

    
1431
    models.VM.AVAILABLE_ACTIONS = {
1432
        'UNKNWON'       : ['destroy'],
1433
        'BUILD'         : ['destroy'],
1434
        'REBOOT'        : ['shutdown', 'destroy', 'console'],
1435
        'STOPPED'       : ['start', 'destroy'],
1436
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1437
        'ERROR'         : ['destroy'],
1438
        'DELETED'        : [],
1439
        'DESTROY'       : [],
1440
        'BUILD_INIT'    : ['destroy'],
1441
        'BUILD_COPY'    : ['destroy'],
1442
        'BUILD_FINAL'   : ['destroy'],
1443
        'SHUTDOWN'      : ['destroy'],
1444
        'START'         : [],
1445
        'CONNECT'       : [],
1446
        'DISCONNECT'    : []
1447
    }
1448

    
1449
    // api status values
1450
    models.VM.STATUSES = [
1451
        'UNKNWON',
1452
        'BUILD',
1453
        'REBOOT',
1454
        'STOPPED',
1455
        'ACTIVE',
1456
        'ERROR',
1457
        'DELETED'
1458
    ]
1459

    
1460
    // api status values
1461
    models.VM.CONNECT_STATES = [
1462
        'ACTIVE',
1463
        'REBOOT',
1464
        'SHUTDOWN'
1465
    ]
1466

    
1467
    // vm states
1468
    models.VM.STATES = models.VM.STATUSES.concat([
1469
        'DESTROY',
1470
        'BUILD_INIT',
1471
        'BUILD_COPY',
1472
        'BUILD_FINAL',
1473
        'SHUTDOWN',
1474
        'START',
1475
        'CONNECT',
1476
        'DISCONNECT',
1477
        'FIREWALL'
1478
    ]);
1479
    
1480
    models.VM.STATES_TRANSITIONS = {
1481
        'DESTROY' : ['DELETED'],
1482
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1483
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1484
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1485
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1486
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1487
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1488
        'BUILD_COPY': ['ERROR', 'ACTIVE', 'BUILD_FINAL', 'DESTROY'],
1489
        'BUILD_FINAL': ['ERROR', 'ACTIVE', 'DESTROY'],
1490
        'BUILD_INIT': ['ERROR', 'ACTIVE', 'BUILD_COPY', 'BUILD_FINAL', 'DESTROY']
1491
    }
1492

    
1493
    models.VM.TRANSITION_STATES = [
1494
        'DESTROY',
1495
        'SHUTDOWN',
1496
        'START',
1497
        'REBOOT',
1498
        'BUILD'
1499
    ]
1500

    
1501
    models.VM.ACTIVE_STATES = [
1502
        'BUILD', 'REBOOT', 'ACTIVE',
1503
        'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL',
1504
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1505
    ]
1506

    
1507
    models.VM.BUILDING_STATES = [
1508
        'BUILD', 'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL'
1509
    ]
1510

    
1511
    models.Networks = models.Collection.extend({
1512
        model: models.Network,
1513
        path: 'networks',
1514
        details: true,
1515
        //noUpdate: true,
1516
        defaults: {'linked_to':[]},
1517

    
1518
        parse: function (resp, xhr) {
1519
            // FIXME: depricated global var
1520
            if (!resp) { return []};
1521
               
1522
            var data = _.map(resp.networks.values, _.bind(this.parse_net_api_data, this));
1523
            return data;
1524
        },
1525

    
1526
        reset_pending_actions: function() {
1527
            this.each(function(net) {
1528
                net.get("actions").reset();
1529
            })
1530
        },
1531

    
1532
        do_all_pending_actions: function() {
1533
            this.each(function(net) {
1534
                net.do_all_pending_actions();
1535
            })
1536
        },
1537

    
1538
        parse_net_api_data: function(data) {
1539
            if (data.servers && data.servers.values) {
1540
                data['linked_to'] = data.servers.values;
1541
            }
1542
            return data;
1543
        },
1544

    
1545
        create: function (name, callback) {
1546
            return this.api_call(this.path, "create", {network:{name:name}}, callback);
1547
        }
1548
    })
1549

    
1550
    models.Images = models.Collection.extend({
1551
        model: models.Image,
1552
        path: 'images',
1553
        details: true,
1554
        noUpdate: true,
1555
        supportIncUpdates: false,
1556
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1557
        read_method: 'read',
1558

    
1559
        // update collection model with id passed
1560
        // making a direct call to the image
1561
        // api url
1562
        update_unknown_id: function(id, callback) {
1563
            var url = getUrl.call(this) + "/" + id;
1564
            this.api_call(this.path + "/" + id, this.read_method, {_options:{async:true, skip_api_error:true}}, undefined, 
1565
            _.bind(function() {
1566
                this.add({id:id, name:"Unknown image", size:-1, progress:100, status:"DELETED"});
1567
                callback(this.get(id));
1568
            }, this), _.bind(function(image, msg, xhr) {
1569
                var img_data = this._read_image_from_request(image, msg, xhr);
1570
                this.add(img_data);
1571
                callback(this.get(id));
1572
            }, this));
1573
        },
1574

    
1575
        _read_image_from_request: function(image, msg, xhr) {
1576
            return image.image;
1577
        },
1578

    
1579
        parse: function (resp, xhr) {
1580
            // FIXME: depricated global var
1581
            var data = _.map(resp.images.values, _.bind(this.parse_meta, this));
1582
            return resp.images.values;
1583
        },
1584

    
1585
        get_meta_key: function(img, key) {
1586
            if (img.metadata && img.metadata.values && img.metadata.values[key]) {
1587
                return img.metadata.values[key];
1588
            }
1589
            return undefined;
1590
        },
1591

    
1592
        comparator: function(img) {
1593
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1594
        },
1595

    
1596
        parse_meta: function(img) {
1597
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1598
                if (img[key]) { return };
1599
                img[key] = this.get_meta_key(img, key) || "";
1600
            }, this));
1601
            return img;
1602
        },
1603

    
1604
        active: function() {
1605
            return this.filter(function(img){return img.get('status') != "DELETED"});
1606
        },
1607

    
1608
        predefined: function() {
1609
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
1610
        },
1611
        
1612
        fetch_for_type: function(type, complete, error) {
1613
            this.fetch({update:true, 
1614
                        success: complete, 
1615
                        error: error, 
1616
                        skip_api_error: true });
1617
        },
1618
        
1619
        get_images_for_type: function(type) {
1620
            if (this['get_{0}_images'.format(type)]) {
1621
                return this['get_{0}_images'.format(type)]();
1622
            }
1623

    
1624
            return this.active();
1625
        },
1626

    
1627
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
1628
            var load = false;
1629
            error = onError || function() {};
1630
            function complete(collection) { 
1631
                onComplete(collection.get_images_for_type(type)); 
1632
            }
1633
            
1634
            // do we need to fetch/update current collection entries
1635
            if (load) {
1636
                onStart();
1637
                this.fetch_for_type(type, complete, error);
1638
            } else {
1639
                // fallback to complete
1640
                complete(this);
1641
            }
1642
        }
1643
    })
1644

    
1645
    models.Flavors = models.Collection.extend({
1646
        model: models.Flavor,
1647
        path: 'flavors',
1648
        details: true,
1649
        noUpdate: true,
1650
        supportIncUpdates: false,
1651
        // update collection model with id passed
1652
        // making a direct call to the flavor
1653
        // api url
1654
        update_unknown_id: function(id, callback) {
1655
            var url = getUrl.call(this) + "/" + id;
1656
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
1657
            _.bind(function() {
1658
                this.add({id:id, cpu:"", ram:"", disk:"", name: "", status:"DELETED"})
1659
            }, this), _.bind(function(flv) {
1660
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1661
                this.add(flv.flavor);
1662
            }, this));
1663
        },
1664

    
1665
        parse: function (resp, xhr) {
1666
            // FIXME: depricated global var
1667
            return _.map(resp.flavors.values, function(o) { o.disk_template = o['SNF:disk_template']; return o});
1668
        },
1669

    
1670
        comparator: function(flv) {
1671
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1672
        },
1673

    
1674
        unavailable_values_for_image: function(img, flavors) {
1675
            var flavors = flavors || this.active();
1676
            var size = img.get_size();
1677
            
1678
            var index = {cpu:[], disk:[], ram:[]};
1679

    
1680
            _.each(this.active(), function(el) {
1681
                var img_size = size;
1682
                var flv_size = el.get_disk_size();
1683
                if (flv_size < img_size) {
1684
                    if (index.disk.indexOf(flv_size) == -1) {
1685
                        index.disk.push(flv_size);
1686
                    }
1687
                };
1688
            });
1689
            
1690
            return index;
1691
        },
1692

    
1693
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
1694
            if (!filter_list) { filter_list = this.models };
1695
            
1696
            return this.select(function(flv){
1697
                if (flv.get("cpu") == cpu + "" &&
1698
                   flv.get("ram") == mem + "" &&
1699
                   flv.get("disk") == disk + "" &&
1700
                   flv.get("disk_template") == disk_template &&
1701
                   filter_list.indexOf(flv) > -1) { return true; }
1702
            })[0];
1703
        },
1704
        
1705
        get_data: function(lst) {
1706
            var data = {'cpu': [], 'mem':[], 'disk':[]};
1707

    
1708
            _.each(lst, function(flv) {
1709
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1710
                    data.cpu.push(flv.get("cpu"));
1711
                }
1712
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1713
                    data.mem.push(flv.get("ram"));
1714
                }
1715
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1716
                    data.disk.push(flv.get("disk"));
1717
                }
1718
            })
1719
            
1720
            return data;
1721
        },
1722

    
1723
        active: function() {
1724
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1725
        }
1726
            
1727
    })
1728

    
1729
    models.VMS = models.Collection.extend({
1730
        model: models.VM,
1731
        path: 'servers',
1732
        details: true,
1733
        copy_image_meta: true,
1734
        
1735
        parse: function (resp, xhr) {
1736
            // FIXME: depricated after refactoring
1737
            var data = resp;
1738
            if (!resp) { return [] };
1739
            data = _.filter(_.map(resp.servers.values, _.bind(this.parse_vm_api_data, this)), function(v){return v});
1740
            return data;
1741
        },
1742
        
1743
        get_reboot_required: function() {
1744
            return this.filter(function(vm){return vm.get("reboot_required") == true})
1745
        },
1746

    
1747
        has_pending_actions: function() {
1748
            return this.filter(function(vm){return vm.pending_action}).length > 0;
1749
        },
1750

    
1751
        reset_pending_actions: function() {
1752
            this.each(function(vm) {
1753
                vm.clear_pending_action();
1754
            })
1755
        },
1756

    
1757
        do_all_pending_actions: function(success, error) {
1758
            this.each(function(vm) {
1759
                if (vm.has_pending_action()) {
1760
                    vm.call(vm.pending_action, success, error);
1761
                    vm.clear_pending_action();
1762
                }
1763
            })
1764
        },
1765
        
1766
        do_all_reboots: function(success, error) {
1767
            this.each(function(vm) {
1768
                if (vm.get("reboot_required")) {
1769
                    vm.call("reboot", success, error);
1770
                }
1771
            });
1772
        },
1773

    
1774
        reset_reboot_required: function() {
1775
            this.each(function(vm) {
1776
                vm.set({'reboot_required': undefined});
1777
            })
1778
        },
1779
        
1780
        stop_stats_update: function(exclude) {
1781
            var exclude = exclude || [];
1782
            this.each(function(vm) {
1783
                if (exclude.indexOf(vm) > -1) {
1784
                    return;
1785
                }
1786
                vm.stop_stats_update();
1787
            })
1788
        },
1789
        
1790
        has_meta: function(vm_data) {
1791
            return vm_data.metadata && vm_data.metadata.values
1792
        },
1793

    
1794
        has_addresses: function(vm_data) {
1795
            return vm_data.metadata && vm_data.metadata.values
1796
        },
1797

    
1798
        parse_vm_api_data: function(data) {
1799
            // do not add non existing DELETED entries
1800
            if (data.status && data.status == "DELETED") {
1801
                if (!this.get(data.id)) {
1802
                    return false;
1803
                }
1804
            }
1805

    
1806
            // OS attribute
1807
            if (this.has_meta(data)) {
1808
                data['OS'] = data.metadata.values.OS || "okeanos";
1809
            }
1810
            
1811
            data['firewalls'] = {};
1812
            if (data['addresses'] && data['addresses'].values) {
1813
                data['linked_to_nets'] = data['addresses'].values;
1814
                _.each(data['addresses'].values, function(f){
1815
                    if (f['firewallProfile']) {
1816
                        data['firewalls'][f['id']] = f['firewallProfile']
1817
                    }
1818
                });
1819
            }
1820
            
1821
            // if vm has no metadata, no metadata object
1822
            // is in json response, reset it to force
1823
            // value update
1824
            if (!data['metadata']) {
1825
                data['metadata'] = {values:{}};
1826
            }
1827

    
1828
            return data;
1829
        },
1830

    
1831
        create: function (name, image, flavor, meta, extra, callback) {
1832
            if (this.copy_image_meta) {
1833
                if (image.get("OS")) {
1834
                    meta['OS'] = image.get("OS");
1835
                }
1836
           }
1837
            
1838
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta}
1839
            opts = _.extend(opts, extra);
1840

    
1841
            this.api_call(this.path, "create", {'server': opts}, undefined, undefined, callback, {critical: false});
1842
        }
1843

    
1844
    })
1845

    
1846
    models.PublicKey = models.Model.extend({
1847
        path: 'keys',
1848
        base_url: '/ui/userdata',
1849
        details: false,
1850
        noUpdate: true,
1851

    
1852

    
1853
        get_public_key: function() {
1854
            return cryptico.publicKeyFromString(this.get("content"));
1855
        },
1856

    
1857
        get_filename: function() {
1858
            return "{0}.pub".format(this.get("name"));
1859
        },
1860

    
1861
        identify_type: function() {
1862
            try {
1863
                var cont = snf.util.validatePublicKey(this.get("content"));
1864
                var type = cont.split(" ")[0];
1865
                return synnefo.util.publicKeyTypesMap[type];
1866
            } catch (err) { return false };
1867
        }
1868

    
1869
    })
1870
    
1871
    models.PublicKeys = models.Collection.extend({
1872
        model: models.PublicKey,
1873
        details: false,
1874
        path: 'keys',
1875
        base_url: '/ui/userdata',
1876
        noUpdate: true,
1877

    
1878
        generate_new: function(success, error) {
1879
            snf.api.sync('create', undefined, {
1880
                url: getUrl.call(this, this.base_url) + "/generate", 
1881
                success: success, 
1882
                error: error,
1883
                skip_api_error: true
1884
            });
1885
        },
1886

    
1887
        add_crypto_key: function(key, success, error, options) {
1888
            var options = options || {};
1889
            var m = new models.PublicKey();
1890

    
1891
            // guess a name
1892
            var name_tpl = "public key";
1893
            var name = name_tpl;
1894
            var name_count = 1;
1895
            
1896
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
1897
                name = name_tpl + " " + name_count;
1898
                name_count++;
1899
            }
1900
            
1901
            m.set({name: name});
1902
            m.set({content: key});
1903
            
1904
            options.success = function () { return success(m) };
1905
            options.errror = error;
1906
            options.skip_api_error = true;
1907
            
1908
            this.create(m.attributes, options);
1909
        }
1910
    })
1911
    
1912
    // storage initialization
1913
    snf.storage.images = new models.Images();
1914
    snf.storage.flavors = new models.Flavors();
1915
    snf.storage.networks = new models.Networks();
1916
    snf.storage.vms = new models.VMS();
1917
    snf.storage.keys = new models.PublicKeys();
1918

    
1919
    //snf.storage.vms.fetch({update:true});
1920
    //snf.storage.images.fetch({update:true});
1921
    //snf.storage.flavors.fetch({update:true});
1922

    
1923
})(this);