Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / ui / static / snf / js / models.js @ 738a9b18

History | View | Annotate | Download (68.6 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 _.escape(this.get('metadata').values[key]);
227
            }
228
            return undefined;
229
        },
230

    
231
        get_owner: function() {
232
            return this.get('owner') || _.keys(synnefo.config.system_images_owners)[0];
233
        },
234

    
235
        display_owner: function() {
236
            var owner = this.get_owner();
237
            if (_.include(_.keys(synnefo.config.system_images_owners), owner)) {
238
                return synnefo.config.system_images_owners[owner];
239
            } else {
240
                return owner;
241
            }
242
        },
243
    
244
        get_readable_size: function() {
245
            if (this.is_deleted()) {
246
                return synnefo.config.image_deleted_size_title || '(none)';
247
            }
248
            return this.get_size() > 0 ? util.readablizeBytes(this.get_size() * 1024 * 1024) : '(none)';
249
        },
250

    
251
        get_os: function() {
252
            return this.get("OS");
253
        },
254

    
255
        get_gui: function() {
256
            return this.get_meta('GUI');
257
        },
258

    
259
        get_created_user: function() {
260
            return synnefo.config.os_created_users[this.get_os()] || "root";
261
        },
262

    
263
        get_sort_order: function() {
264
            return parseInt(this.get('metadata') ? this.get('metadata').values.sortorder : -1)
265
        },
266

    
267
        get_vm: function() {
268
            var vm_id = this.get("serverRef");
269
            var vm = undefined;
270
            vm = storage.vms.get(vm_id);
271
            return vm;
272
        },
273

    
274
        is_public: function() {
275
            return this.get('is_public') || true;
276
        },
277

    
278
        is_deleted: function() {
279
            return this.get('status') == "DELETED"
280
        },
281
        
282
        ssh_keys_path: function() {
283
            prepend = '';
284
            if (this.get_created_user() != 'root') {
285
                prepend = '/home'
286
            }
287
            return '{1}/{0}/.ssh/authorized_keys'.format(this.get_created_user(), prepend);
288
        },
289

    
290
        _supports_ssh: function() {
291
            if (synnefo.config.support_ssh_os_list.indexOf(this.get_os()) > -1) {
292
                return true;
293
            }
294
            return false;
295
        },
296

    
297
        supports: function(feature) {
298
            if (feature == "ssh") {
299
                return this._supports_ssh()
300
            }
301
            return false;
302
        },
303

    
304
        personality_data_for_keys: function(keys) {
305
            contents = '';
306
            _.each(keys, function(key){
307
                contents = contents + key.get("content") + "\n"
308
            });
309
            contents = $.base64.encode(contents);
310

    
311
            return {
312
                path: this.ssh_keys_path(),
313
                contents: contents
314
            }
315
        }
316
    });
317

    
318
    // Flavor model
319
    models.Flavor = models.Model.extend({
320
        path: 'flavors',
321

    
322
        details_string: function() {
323
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
324
        },
325

    
326
        get_disk_size: function() {
327
            return parseInt(this.get("disk") * 1000)
328
        },
329

    
330
        get_disk_template_info: function() {
331
            var info = snf.config.flavors_disk_templates_info[this.get("disk_template")];
332
            if (!info) {
333
                info = { name: this.get("disk_template"), description:'' };
334
            }
335
            return info
336
        }
337

    
338
    });
339
    
340
    //network vms list helper
341
    var NetworkVMSList = function() {
342
        this.initialize = function() {
343
            this.vms = [];
344
            this.pending = [];
345
            this.pending_for_removal = [];
346
        }
347
        
348
        this.add_pending_for_remove = function(vm_id) {
349
            if (this.pending_for_removal.indexOf(vm_id) == -1) {
350
                this.pending_for_removal.push(vm_id);
351
            }
352

    
353
            if (this.pending_for_removal.length) {
354
                this.trigger("pending:remove:add");
355
            }
356
        },
357

    
358
        this.add_pending = function(vm_id) {
359
            if (this.pending.indexOf(vm_id) == -1) {
360
                this.pending[this.pending.length] = vm_id;
361
            }
362

    
363
            if (this.pending.length) {
364
                this.trigger("pending:add");
365
            }
366
        }
367

    
368
        this.check_pending = function() {
369
            var len = this.pending.length;
370
            var args = [this.pending];
371
            this.pending = _.difference(this.pending, this.vms);
372
            if (len != this.pending.length) {
373
                if (this.pending.length == 0) {
374
                    this.trigger("pending:clear");
375
                }
376
            }
377

    
378
            var len = this.pending_for_removal.length;
379
            this.pending_for_removal = _.intersection(this.pending_for_removal, this.vms);
380
            if (this.pending_for_removal.length == 0) {
381
                this.trigger("pending:remove:clear");
382
            }
383

    
384
        }
385

    
386

    
387
        this.add = function(vm_id) {
388
            if (this.vms.indexOf(vm_id) == -1) {
389
                this.vms[this.vms.length] = vm_id;
390
                this.trigger("network:connect", vm_id);
391
                this.check_pending();
392
                return true;
393
            }
394
        }
395

    
396
        this.remove = function(vm_id) {
397
            if (this.vms.indexOf(vm_id) > -1) {
398
                this.vms = _.without(this.vms, vm_id);
399
                this.trigger("network:disconnect", vm_id);
400
                this.check_pending();
401
                return true;
402
            }
403
        }
404

    
405
        this.get = function() {
406
            return this.vms;
407
        }
408

    
409
        this.list = function() {
410
            return storage.vms.filter(_.bind(function(vm){
411
                return this.vms.indexOf(vm.id) > -1;
412
            }, this))
413
        }
414

    
415
        this.initialize();
416
    };
417
    _.extend(NetworkVMSList.prototype, bb.Events);
418
    
419
    // vm networks list helper
420
    var VMNetworksList = function() {
421
        this.initialize = function() {
422
            this.networks = {};
423
            this.network_ids = [];
424
        }
425

    
426
        this.add = function(net_id, data) {
427
            if (!this.networks[net_id]) {
428
                this.networks[net_id] = data || {};
429
                this.network_ids[this.network_ids.length] = net_id;
430
                this.trigger("network:connect", net_id);
431
                return true;
432
            }
433
        }
434

    
435
        this.remove = function(net_id) {
436
            if (this.networks[net_id]) {
437
                delete this.networks[net_id];
438
                this.network_ids = _.without(this.network_ids, net_id);
439
                this.trigger("network:disconnect", net_id);
440
                return true;
441
            }
442
            return false;
443
        }
444

    
445
        this.get = function() {
446
            return this.networks;
447
        }
448

    
449
        this.list = function() {
450
            return storage.networks.filter(_.bind(function(net){
451
                return this.network_ids.indexOf(net.id) > -1;
452
            }, this))
453
        }
454

    
455
        this.initialize();
456
    };
457
    _.extend(VMNetworksList.prototype, bb.Events);
458
        
459
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
460
    _.extend(models.ParamsList.prototype, bb.Events, {
461

    
462
        initialize: function(parent, param_name) {
463
            this.parent = parent;
464
            this.actions = {};
465
            this.param_name = param_name;
466
            this.length = 0;
467
        },
468
        
469
        has_action: function(action) {
470
            return this.actions[action] ? true : false;
471
        },
472
            
473
        _parse_params: function(arguments) {
474
            if (arguments.length <= 1) {
475
                return [];
476
            }
477

    
478
            var args = _.toArray(arguments);
479
            return args.splice(1);
480
        },
481

    
482
        contains: function(action, params) {
483
            params = this._parse_params(arguments);
484
            var has_action = this.has_action(action);
485
            if (!has_action) { return false };
486

    
487
            var paramsEqual = false;
488
            _.each(this.actions[action], function(action_params) {
489
                if (_.isEqual(action_params, params)) {
490
                    paramsEqual = true;
491
                }
492
            });
493
                
494
            return paramsEqual;
495
        },
496
        
497
        is_empty: function() {
498
            return _.isEmpty(this.actions);
499
        },
500

    
501
        add: function(action, params) {
502
            params = this._parse_params(arguments);
503
            if (this.contains.apply(this, arguments)) { return this };
504
            var isnew = false
505
            if (!this.has_action(action)) {
506
                this.actions[action] = [];
507
                isnew = true;
508
            };
509

    
510
            this.actions[action].push(params);
511
            this.parent.trigger("change:" + this.param_name, this.parent, this);
512
            if (isnew) {
513
                this.trigger("add", action, params);
514
            } else {
515
                this.trigger("change", action, params);
516
            }
517
            return this;
518
        },
519
        
520
        remove_all: function(action) {
521
            if (this.has_action(action)) {
522
                delete this.actions[action];
523
                this.parent.trigger("change:" + this.param_name, this.parent, this);
524
                this.trigger("remove", action);
525
            }
526
            return this;
527
        },
528

    
529
        reset: function() {
530
            this.actions = {};
531
            this.parent.trigger("change:" + this.param_name, this.parent, this);
532
            this.trigger("reset");
533
            this.trigger("remove");
534
        },
535

    
536
        remove: function(action, params) {
537
            params = this._parse_params(arguments);
538
            if (!this.has_action(action)) { return this };
539
            var index = -1;
540
            _.each(this.actions[action], _.bind(function(action_params) {
541
                if (_.isEqual(action_params, params)) {
542
                    index = this.actions[action].indexOf(action_params);
543
                }
544
            }, this));
545
            
546
            if (index > -1) {
547
                this.actions[action].splice(index, 1);
548
                if (_.isEmpty(this.actions[action])) {
549
                    delete this.actions[action];
550
                }
551
                this.parent.trigger("change:" + this.param_name, this.parent, this);
552
                this.trigger("remove", action, params);
553
            }
554
        }
555

    
556
    });
557

    
558
    // Image model
559
    models.Network = models.Model.extend({
560
        path: 'networks',
561
        has_status: true,
562
        
563
        initialize: function() {
564
            this.vms = new NetworkVMSList();
565
            this.vms.bind("pending:add", _.bind(this.handle_pending_connections, this, "add"));
566
            this.vms.bind("pending:clear", _.bind(this.handle_pending_connections, this, "clear"));
567
            this.vms.bind("pending:remove:add", _.bind(this.handle_pending_connections, this, "add"));
568
            this.vms.bind("pending:remove:clear", _.bind(this.handle_pending_connections, this, "clear"));
569

    
570
            var ret = models.Network.__super__.initialize.apply(this, arguments);
571

    
572
            storage.vms.bind("change:linked_to_nets", _.bind(this.update_connections, this, "vm:change"));
573
            storage.vms.bind("add", _.bind(this.update_connections, this, "add"));
574
            storage.vms.bind("remove", _.bind(this.update_connections, this, "remove"));
575
            storage.vms.bind("reset", _.bind(this.update_connections, this, "reset"));
576

    
577
            this.bind("change:linked_to", _.bind(this.update_connections, this, "net:change"));
578
            this.update_connections();
579
            this.update_state();
580
            
581
            this.set({"actions": new models.ParamsList(this, "actions")});
582

    
583
            return ret;
584
        },
585

    
586
        toJSON: function() {
587
            var attrs = _.clone(this.attributes);
588
            attrs.actions = _.clone(this.get("actions").actions);
589
            return attrs;
590
        },
591

    
592
        update_state: function() {
593
            if (this.vms.pending.length) {
594
                this.set({state: "CONNECTING"});
595
                return
596
            }
597

    
598
            if (this.vms.pending_for_removal.length) {
599
                this.set({state: "DISCONNECTING"});
600
                return
601
            }   
602
            
603
            var firewalling = false;
604
            _.each(this.vms.get(), _.bind(function(vm_id){
605
                var vm = storage.vms.get(vm_id);
606
                if (!vm) { return };
607
                if (!_.isEmpty(vm.pending_firewalls)) {
608
                    this.set({state:"FIREWALLING"});
609
                    firewalling = true;
610
                    return false;
611
                }
612
            },this));
613
            if (firewalling) { return };
614

    
615
            this.set({state:"NORMAL"});
616
        },
617

    
618
        handle_pending_connections: function(action) {
619
            this.update_state();
620
        },
621

    
622
        // handle vm/network connections
623
        update_connections: function(action, model) {
624
            
625
            // vm removed disconnect vm from network
626
            if (action == "remove") {
627
                var removed_from_net = this.vms.remove(model.id);
628
                var removed_from_vm = model.networks.remove(this.id);
629
                if (removed_from_net) {this.trigger("vm:disconnect", model, this); this.change()};
630
                if (removed_from_vm) {model.trigger("network:disconnect", this, model); this.change()};
631
                return;
632
            }
633
            
634
            // update links for all vms
635
            var links = this.get("linked_to");
636
            storage.vms.each(_.bind(function(vm) {
637
                var vm_links = vm.get("linked_to") || [];
638
                if (vm_links.indexOf(this.id) > -1) {
639
                    // vm has connection to current network
640
                    if (links.indexOf(vm.id) > -1) {
641
                        // and network has connection to vm, so try
642
                        // to append it
643
                        var add_to_net = this.vms.add(vm.id);
644
                        var index = _.indexOf(vm_links, this.id);
645
                        var add_to_vm = vm.networks.add(this.id, vm.get("linked_to_nets")[index]);
646
                        
647
                        // call only if connection did not existed
648
                        if (add_to_net) {this.trigger("vm:connect", vm, this); this.change()};
649
                        if (add_to_vm) {vm.trigger("network:connect", this, vm); vm.change()};
650
                    } else {
651
                        // no connection, try to remove it
652
                        var removed_from_net = this.vms.remove(vm.id);
653
                        var removed_from_vm = vm.networks.remove(this.id);
654
                        if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
655
                        if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
656
                    }
657
                } else {
658
                    // vm has no connection to current network, try to remove it
659
                    var removed_from_net = this.vms.remove(vm.id);
660
                    var removed_from_vm = vm.networks.remove(this.id);
661
                    if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()};
662
                    if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()};
663
                }
664
            },this));
665
        },
666

    
667
        is_public: function() {
668
            return this.id == "public";
669
        },
670

    
671
        contains_vm: function(vm) {
672
            var net_vm_exists = this.vms.get().indexOf(vm.id) > -1;
673
            var vm_net_exists = vm.is_connected_to(this);
674
            return net_vm_exists && vm_net_exists;
675
        },
676
        
677
        call: function(action, params, success, error) {
678
            if (action == "destroy") {
679
                this.set({state:"DESTROY"});
680
                this.get("actions").remove("destroy");
681
                this.remove(_.bind(function(){
682
                    success();
683
                }, this), error);
684
            }
685
            
686
            if (action == "disconnect") {
687
                _.each(params, _.bind(function(vm_id) {
688
                    var vm = snf.storage.vms.get(vm_id);
689
                    this.get("actions").remove("disconnect", vm_id);
690
                    if (vm) {
691
                        this.remove_vm(vm, success, error);
692
                    }
693
                }, this));
694
            }
695
        },
696

    
697
        add_vm: function (vm, callback, error, options) {
698
            var payload = {add:{serverRef:"" + vm.id}};
699
            payload._options = options || {};
700
            return this.api_call(this.api_path() + "/action", "create", 
701
                                 payload,
702
                                 _.bind(function(){
703
                                     this.vms.add_pending(vm.id);
704
                                     if (callback) {callback()}
705
                                 },this), error);
706
        },
707

    
708
        remove_vm: function (vm, callback, error, options) {
709
            var payload = {remove:{serverRef:"" + vm.id}};
710
            payload._options = options || {};
711
            return this.api_call(this.api_path() + "/action", "create", 
712
                                 {remove:{serverRef:"" + vm.id}},
713
                                 _.bind(function(){
714
                                     this.vms.add_pending_for_remove(vm.id);
715
                                     if (callback) {callback()}
716
                                 },this), error);
717
        },
718

    
719
        rename: function(name, callback) {
720
            return this.api_call(this.api_path(), "update", {
721
                network:{name:name}, 
722
                _options:{
723
                    critical: false, 
724
                    error_params:{
725
                        title: "Network action failed",
726
                        ns: "Networks",
727
                        extra_details: {"Network id": this.id}
728
                    }
729
                }}, callback);
730
        },
731

    
732
        get_connectable_vms: function() {
733
            var servers = this.vms.list();
734
            return storage.vms.filter(function(vm){
735
                return servers.indexOf(vm) == -1 && !vm.in_error_state();
736
            })
737
        },
738

    
739
        state_message: function() {
740
            if (this.get("state") == "NORMAL" && this.is_public()) {
741
                return "Public network";
742
            }
743

    
744
            return models.Network.STATES[this.get("state")];
745
        },
746

    
747
        in_progress: function() {
748
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
749
        },
750

    
751
        do_all_pending_actions: function(success, error) {
752
            var destroy = this.get("actions").has_action("destroy");
753
            _.each(this.get("actions").actions, _.bind(function(params, action) {
754
                _.each(params, _.bind(function(with_params) {
755
                    this.call(action, with_params, success, error);
756
                }, this));
757
            }, this));
758
        }
759
    });
760
    
761
    models.Network.STATES = {
762
        'NORMAL': 'Private network',
763
        'CONNECTING': 'Connecting...',
764
        'DISCONNECTING': 'Disconnecting...',
765
        'FIREWALLING': 'Firewall update...',
766
        'DESTROY': 'Destroying...'
767
    }
768

    
769
    models.Network.STATES_TRANSITIONS = {
770
        'CONNECTING': ['NORMAL'],
771
        'DISCONNECTING': ['NORMAL'],
772
        'FIREWALLING': ['NORMAL']
773
    }
774

    
775
    // Virtualmachine model
776
    models.VM = models.Model.extend({
777

    
778
        path: 'servers',
779
        has_status: true,
780
        initialize: function(params) {
781
            this.networks = new VMNetworksList();
782
            
783
            this.pending_firewalls = {};
784
            
785
            models.VM.__super__.initialize.apply(this, arguments);
786

    
787
            this.set({state: params.status || "ERROR"});
788
            this.log = new snf.logging.logger("VM " + this.id);
789
            this.pending_action = undefined;
790
            
791
            // init stats parameter
792
            this.set({'stats': undefined}, {silent: true});
793
            // defaults to not update the stats
794
            // each view should handle this vm attribute 
795
            // depending on if it displays stat images or not
796
            this.do_update_stats = false;
797
            
798
            // interval time
799
            // this will dynamicaly change if the server responds that
800
            // images get refreshed on different intervals
801
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
802
            this.stats_available = false;
803

    
804
            // initialize interval
805
            this.init_stats_intervals(this.stats_update_interval);
806
            
807
            this.bind("change:progress", _.bind(this.update_building_progress, this));
808
            this.update_building_progress();
809

    
810
            this.bind("change:firewalls", _.bind(this.handle_firewall_change, this));
811
            
812
            // default values
813
            this.set({linked_to_nets:this.get("linked_to_nets") || []});
814
            this.set({firewalls:this.get("firewalls") || []});
815

    
816
            this.bind("change:state", _.bind(function(){if (this.state() == "DESTROY") { this.handle_destroy() }}, this))
817
        },
818

    
819
        handle_firewall_change: function() {
820

    
821
        },
822
        
823
        set_linked_to_nets: function(data) {
824
            this.set({"linked_to":_.map(data, function(n){ return n.id})});
825
            return data;
826
        },
827

    
828
        is_connected_to: function(net) {
829
            return _.filter(this.networks.list(), function(n){return n.id == net.id}).length > 0;
830
        },
831
        
832
        status: function(st) {
833
            if (!st) { return this.get("status")}
834
            return this.set({status:st});
835
        },
836

    
837
        set_status: function(st) {
838
            var new_state = this.state_for_api_status(st);
839
            var transition = false;
840

    
841
            if (this.state() != new_state) {
842
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
843
                    transition = this.state();
844
                }
845
            }
846
            
847
            // call it silently to avoid double change trigger
848
            this.set({'state': this.state_for_api_status(st)}, {silent: true});
849
            
850
            // trigger transition
851
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
852
                this.trigger("transition", {from:transition, to:new_state}) 
853
            };
854
            return st;
855
        },
856

    
857
        update_building_progress: function() {
858
            if (this.is_building()) {
859
                var progress = this.get("progress");
860
                if (progress == 0) {
861
                    this.state("BUILD_INIT");
862
                    this.set({progress_message: BUILDING_MESSAGES['INIT']});
863
                }
864
                if (progress > 0 && progress < 99) {
865
                    this.state("BUILD_COPY");
866
                    this.get_copy_details(true, undefined, _.bind(function(details){
867
                        this.set({
868
                            progress_message: BUILDING_MESSAGES['COPY'].format(details.copy, 
869
                                                                               details.size, 
870
                                                                               details.progress)
871
                        });
872
                    }, this));
873
                }
874
                if (progress == 100) {
875
                    this.state("BUILD_FINAL");
876
                    this.set({progress_message: BUILDING_MESSAGES['FINAL']});
877
                }
878
            } else {
879
            }
880
        },
881

    
882
        get_copy_details: function(human, image, callback) {
883
            var human = human || false;
884
            var image = image || this.get_image(_.bind(function(image){
885
                var progress = this.get('progress');
886
                var size = image.get_size();
887
                var size_copied = (size * progress / 100).toFixed(2);
888
                
889
                if (human) {
890
                    size = util.readablizeBytes(size*1024*1024);
891
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
892
                }
893

    
894
                callback({'progress': progress, 'size': size, 'copy': size_copied})
895
            }, this));
896
        },
897

    
898
        start_stats_update: function(force_if_empty) {
899
            var prev_state = this.do_update_stats;
900

    
901
            this.do_update_stats = true;
902
            
903
            // fetcher initialized ??
904
            if (!this.stats_fetcher) {
905
                this.init_stats_intervals();
906
            }
907

    
908

    
909
            // fetcher running ???
910
            if (!this.stats_fetcher.running || !prev_state) {
911
                this.stats_fetcher.start();
912
            }
913

    
914
            if (force_if_empty && this.get("stats") == undefined) {
915
                this.update_stats(true);
916
            }
917
        },
918

    
919
        stop_stats_update: function(stop_calls) {
920
            this.do_update_stats = false;
921

    
922
            if (stop_calls) {
923
                this.stats_fetcher.stop();
924
            }
925
        },
926

    
927
        // clear and reinitialize update interval
928
        init_stats_intervals: function (interval) {
929
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
930
            this.stats_fetcher.start();
931
        },
932
        
933
        get_stats_fetcher: function(timeout) {
934
            var cb = _.bind(function(data){
935
                this.update_stats();
936
            }, this);
937
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
938
            return fetcher;
939
        },
940

    
941
        // do the api call
942
        update_stats: function(force) {
943
            // do not update stats if flag not set
944
            if ((!this.do_update_stats && !force) || this.updating_stats) {
945
                return;
946
            }
947

    
948
            // make the api call, execute handle_stats_update on sucess
949
            // TODO: onError handler ???
950
            stats_url = this.url() + "/stats";
951
            this.updating_stats = true;
952
            this.sync("read", this, {
953
                handles_error:true, 
954
                url: stats_url, 
955
                refresh:true, 
956
                success: _.bind(this.handle_stats_update, this),
957
                error: _.bind(this.handle_stats_error, this),
958
                complete: _.bind(function(){this.updating_stats = false;}, this),
959
                critical: false,
960
                log_error: false,
961
                skips_timeouts: true
962
            });
963
        },
964

    
965
        get_stats_image: function(stat, type) {
966
        },
967
        
968
        _set_stats: function(stats) {
969
            var silent = silent === undefined ? false : silent;
970
            // unavailable stats while building
971
            if (this.get("status") == "BUILD") { 
972
                this.stats_available = false;
973
            } else { this.stats_available = true; }
974

    
975
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
976
            
977
            this.set({stats: stats}, {silent:true});
978
            this.trigger("stats:update", stats);
979
        },
980

    
981
        unbind: function() {
982
            models.VM.__super__.unbind.apply(this, arguments);
983
        },
984

    
985
        handle_stats_error: function() {
986
            stats = {};
987
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
988
                stats[k] = false;
989
            });
990

    
991
            this.set({'stats': stats});
992
        },
993

    
994
        // this method gets executed after a successful vm stats api call
995
        handle_stats_update: function(data) {
996
            var self = this;
997
            // avoid browser caching
998
            
999
            if (data.stats && _.size(data.stats) > 0) {
1000
                var ts = $.now();
1001
                var stats = data.stats;
1002
                var images_loaded = 0;
1003
                var images = {};
1004

    
1005
                function check_images_loaded() {
1006
                    images_loaded++;
1007

    
1008
                    if (images_loaded == 4) {
1009
                        self._set_stats(images);
1010
                    }
1011
                }
1012
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1013
                    
1014
                    stats[k] = stats[k] + "?_=" + ts;
1015
                    
1016
                    var stat = k.slice(0,3);
1017
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
1018
                    var img = $("<img />");
1019
                    var val = stats[k];
1020
                    
1021
                    // load stat image to a temporary dom element
1022
                    // update model stats on image load/error events
1023
                    img.load(function() {
1024
                        images[k] = val;
1025
                        check_images_loaded();
1026
                    });
1027

    
1028
                    img.error(function() {
1029
                        images[stat + type] = false;
1030
                        check_images_loaded();
1031
                    });
1032

    
1033
                    img.attr({'src': stats[k]});
1034
                })
1035
                data.stats = stats;
1036
            }
1037

    
1038
            // do we need to change the interval ??
1039
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
1040
                this.stats_update_interval = data.stats.refresh * 1000;
1041
                this.stats_fetcher.interval = this.stats_update_interval;
1042
                this.stats_fetcher.maximum_interval = this.stats_update_interval;
1043
                this.stats_fetcher.stop();
1044
                this.stats_fetcher.start(false);
1045
            }
1046
        },
1047

    
1048
        // helper method that sets the do_update_stats
1049
        // in the future this method could also make an api call
1050
        // immediaetly if needed
1051
        enable_stats_update: function() {
1052
            this.do_update_stats = true;
1053
        },
1054
        
1055
        handle_destroy: function() {
1056
            this.stats_fetcher.stop();
1057
        },
1058

    
1059
        require_reboot: function() {
1060
            if (this.is_active()) {
1061
                this.set({'reboot_required': true});
1062
            }
1063
        },
1064
        
1065
        set_pending_action: function(data) {
1066
            this.pending_action = data;
1067
            return data;
1068
        },
1069

    
1070
        // machine has pending action
1071
        update_pending_action: function(action, force) {
1072
            this.set({pending_action: action});
1073
        },
1074

    
1075
        clear_pending_action: function() {
1076
            this.set({pending_action: undefined});
1077
        },
1078

    
1079
        has_pending_action: function() {
1080
            return this.get("pending_action") ? this.get("pending_action") : false;
1081
        },
1082
        
1083
        // machine is active
1084
        is_active: function() {
1085
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
1086
        },
1087
        
1088
        // machine is building 
1089
        is_building: function() {
1090
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
1091
        },
1092
        
1093
        in_error_state: function() {
1094
            return this.state() === "ERROR"
1095
        },
1096

    
1097
        // user can connect to machine
1098
        is_connectable: function() {
1099
            // check if ips exist
1100
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
1101
                return false;
1102
            }
1103
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1104
        },
1105
        
1106
        set_firewalls: function(data) {
1107
            _.each(data, _.bind(function(val, key){
1108
                if (this.pending_firewalls && this.pending_firewalls[key] && this.pending_firewalls[key] == val) {
1109
                        this.require_reboot();
1110
                        this.remove_pending_firewall(key, val);
1111
                }
1112
            }, this));
1113
            return data;
1114
        },
1115

    
1116
        remove_pending_firewall: function(net_id, value) {
1117
            if (this.pending_firewalls[net_id] == value) {
1118
                delete this.pending_firewalls[net_id];
1119
                storage.networks.get(net_id).update_state();
1120
            }
1121
        },
1122
            
1123
        remove_meta: function(key, complete, error) {
1124
            var url = this.api_path() + "/meta/" + key;
1125
            this.api_call(url, "delete", undefined, complete, error);
1126
        },
1127

    
1128
        save_meta: function(meta, complete, error) {
1129
            var url = this.api_path() + "/meta/" + meta.key;
1130
            var payload = {meta:{}};
1131
            payload.meta[meta.key] = meta.value;
1132
            payload._options = {
1133
                critical:false, 
1134
                error_params: {
1135
                    title: "Machine metadata error",
1136
                    extra_details: {"Machine id": this.id}
1137
            }};
1138

    
1139
            this.api_call(url, "update", payload, complete, error);
1140
        },
1141

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

    
1145
            this.pending_firewalls[net_id] = value;
1146
            this.trigger("change", this, this);
1147
            var payload = {"firewallProfile":{"profile":value}};
1148
            payload._options = _.extend({critical: false}, options);
1149
            
1150
            // reset firewall state on error
1151
            var error_cb = _.bind(function() {
1152
                thi
1153
            }, this);
1154

    
1155
            this.api_call(this.api_path() + "/action", "create", payload, callback, error);
1156
            storage.networks.get(net_id).update_state();
1157
        },
1158

    
1159
        firewall_pending: function(net_id) {
1160
            return this.pending_firewalls[net_id] != undefined;
1161
        },
1162
        
1163
        // update/get the state of the machine
1164
        state: function() {
1165
            var args = slice.call(arguments);
1166
                
1167
            // TODO: it might not be a good idea to set the state in set_state method
1168
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1169
                this.set({'state': args[0]});
1170
            }
1171

    
1172
            return this.get('state');
1173
        },
1174
        
1175
        // get the state that the api status corresponds to
1176
        state_for_api_status: function(status) {
1177
            return this.state_transition(this.state(), status);
1178
        },
1179
        
1180
        // vm state equals vm api status
1181
        state_is_status: function(state) {
1182
            return models.VM.STATUSES.indexOf(state) != -1;
1183
        },
1184
        
1185
        // get transition state for the corresponging api status
1186
        state_transition: function(state, new_status) {
1187
            var statuses = models.VM.STATES_TRANSITIONS[state];
1188
            if (statuses) {
1189
                if (statuses.indexOf(new_status) > -1) {
1190
                    return new_status;
1191
                } else {
1192
                    return state;
1193
                }
1194
            } else {
1195
                return new_status;
1196
            }
1197
        },
1198
        
1199
        // the current vm state is a transition state
1200
        in_transition: function() {
1201
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
1202
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
1203
        },
1204
        
1205
        // get image object
1206
        get_image: function(callback) {
1207
            var image = storage.images.get(this.get('imageRef'));
1208
            if (!image) {
1209
                storage.images.update_unknown_id(this.get('imageRef'), callback);
1210
                return;
1211
            }
1212
            callback(image);
1213
            return image;
1214
        },
1215
        
1216
        // get flavor object
1217
        get_flavor: function() {
1218
            var flv = storage.flavors.get(this.get('flavorRef'));
1219
            if (!flv) {
1220
                storage.flavors.update_unknown_id(this.get('flavorRef'));
1221
                flv = storage.flavors.get(this.get('flavorRef'));
1222
            }
1223
            return flv;
1224
        },
1225

    
1226
        // retrieve the metadata object
1227
        get_meta: function(key) {
1228
            try {
1229
                return _.escape(this.get('metadata').values[key]);
1230
            } catch (err) {
1231
                return {};
1232
            }
1233
        },
1234
        
1235
        // get metadata OS value
1236
        get_os: function() {
1237
            return this.get_meta('OS') || (this.get_image(function(){}) ? 
1238
                                          this.get_image(function(){}).get_os() || "okeanos" : "okeanos");
1239
        },
1240

    
1241
        get_gui: function() {
1242
            return this.get_meta('GUI');
1243
        },
1244

    
1245
        // get public ip addresses
1246
        // TODO: public network is always the 0 index ???
1247
        get_addresses: function(net_id) {
1248
            var net_id = net_id || "public";
1249
            
1250
            var info = this.get_network_info(net_id);
1251
            if (!info) { return {} };
1252
            addrs = {};
1253
            _.each(info.values, function(addr) {
1254
                addrs["ip" + addr.version] = addr.addr;
1255
            });
1256
            return addrs
1257
        },
1258

    
1259
        get_network_info: function(net_id) {
1260
            var net_id = net_id || "public";
1261
            
1262
            if (!this.networks.network_ids.length) { return {} };
1263

    
1264
            var addresses = this.networks.get();
1265
            try {
1266
                return _.select(addresses, function(net, key){return key == net_id })[0];
1267
            } catch (err) {
1268
                //this.log.debug("Cannot find network {0}".format(net_id))
1269
            }
1270
        },
1271

    
1272
        firewall_profile: function(net_id) {
1273
            var net_id = net_id || "public";
1274
            var firewalls = this.get("firewalls");
1275
            return firewalls[net_id];
1276
        },
1277

    
1278
        has_firewall: function(net_id) {
1279
            var net_id = net_id || "public";
1280
            return ["ENABLED","PROTECTED"].indexOf(this.firewall_profile()) > -1;
1281
        },
1282
    
1283
        // get actions that the user can execute
1284
        // depending on the vm state/status
1285
        get_available_actions: function() {
1286
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1287
        },
1288

    
1289
        set_profile: function(profile, net_id) {
1290
        },
1291
        
1292
        // call rename api
1293
        rename: function(new_name) {
1294
            //this.set({'name': new_name});
1295
            this.sync("update", this, {
1296
                critical: true,
1297
                data: {
1298
                    'server': {
1299
                        'name': new_name
1300
                    }
1301
                }, 
1302
                // do the rename after the method succeeds
1303
                success: _.bind(function(){
1304
                    //this.set({name: new_name});
1305
                    snf.api.trigger("call");
1306
                }, this)
1307
            });
1308
        },
1309
        
1310
        get_console_url: function(data) {
1311
            var url_params = {
1312
                machine: this.get("name"),
1313
                host_ip: this.get_addresses().ip4,
1314
                host_ip_v6: this.get_addresses().ip6,
1315
                host: data.host,
1316
                port: data.port,
1317
                password: data.password
1318
            }
1319
            return '/machines/console?' + $.param(url_params);
1320
        },
1321

    
1322
        // action helper
1323
        call: function(action_name, success, error, params) {
1324
            var id_param = [this.id];
1325
            
1326
            params = params || {};
1327
            success = success || function() {};
1328
            error = error || function() {};
1329

    
1330
            var self = this;
1331

    
1332
            switch(action_name) {
1333
                case 'start':
1334
                    this.__make_api_call(this.get_action_url(), // vm actions url
1335
                                         "create", // create so that sync later uses POST to make the call
1336
                                         {start:{}}, // payload
1337
                                         function() {
1338
                                             // set state after successful call
1339
                                             self.state("START"); 
1340
                                             success.apply(this, arguments);
1341
                                             snf.api.trigger("call");
1342
                                         },  
1343
                                         error, 'start', params);
1344
                    break;
1345
                case 'reboot':
1346
                    this.__make_api_call(this.get_action_url(), // vm actions url
1347
                                         "create", // create so that sync later uses POST to make the call
1348
                                         {reboot:{type:"HARD"}}, // payload
1349
                                         function() {
1350
                                             // set state after successful call
1351
                                             self.state("REBOOT"); 
1352
                                             success.apply(this, arguments)
1353
                                             snf.api.trigger("call");
1354
                                             self.set({'reboot_required': false});
1355
                                         },
1356
                                         error, 'reboot', params);
1357
                    break;
1358
                case 'shutdown':
1359
                    this.__make_api_call(this.get_action_url(), // vm actions url
1360
                                         "create", // create so that sync later uses POST to make the call
1361
                                         {shutdown:{}}, // payload
1362
                                         function() {
1363
                                             // set state after successful call
1364
                                             self.state("SHUTDOWN"); 
1365
                                             success.apply(this, arguments)
1366
                                             snf.api.trigger("call");
1367
                                         },  
1368
                                         error, 'shutdown', params);
1369
                    break;
1370
                case 'console':
1371
                    this.__make_api_call(this.url() + "/action", "create", {'console': {'type':'vnc'}}, function(data) {
1372
                        var cons_data = data.console;
1373
                        success.apply(this, [cons_data]);
1374
                    }, undefined, 'console', params)
1375
                    break;
1376
                case 'destroy':
1377
                    this.__make_api_call(this.url(), // vm actions url
1378
                                         "delete", // create so that sync later uses POST to make the call
1379
                                         undefined, // payload
1380
                                         function() {
1381
                                             // set state after successful call
1382
                                             self.state('DESTROY');
1383
                                             success.apply(this, arguments)
1384
                                         },  
1385
                                         error, 'destroy', params);
1386
                    break;
1387
                default:
1388
                    throw "Invalid VM action ("+action_name+")";
1389
            }
1390
        },
1391
        
1392
        __make_api_call: function(url, method, data, success, error, action, extra_params) {
1393
            var self = this;
1394
            error = error || function(){};
1395
            success = success || function(){};
1396

    
1397
            var params = {
1398
                url: url,
1399
                data: data,
1400
                success: function(){ self.handle_action_succeed.apply(self, arguments); success.apply(this, arguments)},
1401
                error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)},
1402
                error_params: { ns: "Machines actions", 
1403
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1404
                                extra_details: { 'Machine ID': this.id, 'URL': url, 'Action': action || "undefined" },
1405
                                allow_reload: false
1406
                              },
1407
                display: false,
1408
                critical: false
1409
            }
1410
            _.extend(params, extra_params)
1411
            this.sync(method, this, params);
1412
        },
1413

    
1414
        handle_action_succeed: function() {
1415
            this.trigger("action:success", arguments);
1416
        },
1417
        
1418
        reset_action_error: function() {
1419
            this.action_error = false;
1420
            this.trigger("action:fail:reset", this.action_error);
1421
        },
1422

    
1423
        handle_action_fail: function() {
1424
            this.action_error = arguments;
1425
            this.trigger("action:fail", arguments);
1426
        },
1427

    
1428
        get_action_url: function(name) {
1429
            return this.url() + "/action";
1430
        },
1431

    
1432
        get_connection_info: function(host_os, success, error) {
1433
            var url = "/machines/connect";
1434
            params = {
1435
                ip_address: this.get_addresses().ip4,
1436
                os: this.get_os(),
1437
                host_os: host_os,
1438
                srv: this.id
1439
            }
1440

    
1441
            url = url + "?" + $.param(params);
1442

    
1443
            var ajax = snf.api.sync("read", undefined, { url: url, 
1444
                                                         error:error, 
1445
                                                         success:success, 
1446
                                                         handles_error:1});
1447
        }
1448
    })
1449
    
1450
    models.VM.ACTIONS = [
1451
        'start',
1452
        'shutdown',
1453
        'reboot',
1454
        'console',
1455
        'destroy'
1456
    ]
1457

    
1458
    models.VM.AVAILABLE_ACTIONS = {
1459
        'UNKNWON'       : ['destroy'],
1460
        'BUILD'         : ['destroy'],
1461
        'REBOOT'        : ['shutdown', 'destroy', 'console'],
1462
        'STOPPED'       : ['start', 'destroy'],
1463
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1464
        'ERROR'         : ['destroy'],
1465
        'DELETED'        : [],
1466
        'DESTROY'       : [],
1467
        'BUILD_INIT'    : ['destroy'],
1468
        'BUILD_COPY'    : ['destroy'],
1469
        'BUILD_FINAL'   : ['destroy'],
1470
        'SHUTDOWN'      : ['destroy'],
1471
        'START'         : [],
1472
        'CONNECT'       : [],
1473
        'DISCONNECT'    : []
1474
    }
1475

    
1476
    // api status values
1477
    models.VM.STATUSES = [
1478
        'UNKNWON',
1479
        'BUILD',
1480
        'REBOOT',
1481
        'STOPPED',
1482
        'ACTIVE',
1483
        'ERROR',
1484
        'DELETED'
1485
    ]
1486

    
1487
    // api status values
1488
    models.VM.CONNECT_STATES = [
1489
        'ACTIVE',
1490
        'REBOOT',
1491
        'SHUTDOWN'
1492
    ]
1493

    
1494
    // vm states
1495
    models.VM.STATES = models.VM.STATUSES.concat([
1496
        'DESTROY',
1497
        'BUILD_INIT',
1498
        'BUILD_COPY',
1499
        'BUILD_FINAL',
1500
        'SHUTDOWN',
1501
        'START',
1502
        'CONNECT',
1503
        'DISCONNECT',
1504
        'FIREWALL'
1505
    ]);
1506
    
1507
    models.VM.STATES_TRANSITIONS = {
1508
        'DESTROY' : ['DELETED'],
1509
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1510
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1511
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1512
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1513
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1514
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1515
        'BUILD_COPY': ['ERROR', 'ACTIVE', 'BUILD_FINAL', 'DESTROY'],
1516
        'BUILD_FINAL': ['ERROR', 'ACTIVE', 'DESTROY'],
1517
        'BUILD_INIT': ['ERROR', 'ACTIVE', 'BUILD_COPY', 'BUILD_FINAL', 'DESTROY']
1518
    }
1519

    
1520
    models.VM.TRANSITION_STATES = [
1521
        'DESTROY',
1522
        'SHUTDOWN',
1523
        'START',
1524
        'REBOOT',
1525
        'BUILD'
1526
    ]
1527

    
1528
    models.VM.ACTIVE_STATES = [
1529
        'BUILD', 'REBOOT', 'ACTIVE',
1530
        'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL',
1531
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1532
    ]
1533

    
1534
    models.VM.BUILDING_STATES = [
1535
        'BUILD', 'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL'
1536
    ]
1537

    
1538
    models.Networks = models.Collection.extend({
1539
        model: models.Network,
1540
        path: 'networks',
1541
        details: true,
1542
        //noUpdate: true,
1543
        defaults: {'linked_to':[]},
1544

    
1545
        parse: function (resp, xhr) {
1546
            // FIXME: depricated global var
1547
            if (!resp) { return []};
1548
               
1549
            var data = _.map(resp.networks.values, _.bind(this.parse_net_api_data, this));
1550
            return data;
1551
        },
1552

    
1553
        reset_pending_actions: function() {
1554
            this.each(function(net) {
1555
                net.get("actions").reset();
1556
            })
1557
        },
1558

    
1559
        do_all_pending_actions: function() {
1560
            this.each(function(net) {
1561
                net.do_all_pending_actions();
1562
            })
1563
        },
1564

    
1565
        parse_net_api_data: function(data) {
1566
            if (data.servers && data.servers.values) {
1567
                data['linked_to'] = data.servers.values;
1568
            }
1569
            return data;
1570
        },
1571

    
1572
        create: function (name, callback) {
1573
            return this.api_call(this.path, "create", {network:{name:name}}, callback);
1574
        }
1575
    })
1576

    
1577
    models.Images = models.Collection.extend({
1578
        model: models.Image,
1579
        path: 'images',
1580
        details: true,
1581
        noUpdate: true,
1582
        supportIncUpdates: false,
1583
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1584
        read_method: 'read',
1585

    
1586
        // update collection model with id passed
1587
        // making a direct call to the image
1588
        // api url
1589
        update_unknown_id: function(id, callback) {
1590
            var url = getUrl.call(this) + "/" + id;
1591
            this.api_call(this.path + "/" + id, this.read_method, {_options:{async:true, skip_api_error:true}}, undefined, 
1592
            _.bind(function() {
1593
                if (!this.get(id)) {
1594
                            if (this.fallback_service) {
1595
                        // if current service has fallback_service attribute set
1596
                        // use this service to retrieve the missing image model
1597
                        var tmpservice = new this.fallback_service();
1598
                        tmpservice.update_unknown_id(id, _.bind(function(img){
1599
                            img.attributes.status = "DELETED";
1600
                            this.add(img.attributes);
1601
                            callback(this.get(id));
1602
                        }, this));
1603
                    } else {
1604
                        var title = synnefo.config.image_deleted_title || 'Deleted';
1605
                        // else add a dummy DELETED state image entry
1606
                        this.add({id:id, name:title, size:-1, 
1607
                                  progress:100, status:"DELETED"});
1608
                        callback(this.get(id));
1609
                    }   
1610
                } else {
1611
                    callback(this.get(id));
1612
                }
1613
            }, this), _.bind(function(image, msg, xhr) {
1614
                if (!image) {
1615
                    var title = synnefo.config.image_deleted_title || 'Deleted';
1616
                    this.add({id:id, name:title, size:-1, 
1617
                              progress:100, status:"DELETED"});
1618
                    callback(this.get(id));
1619
                    return;
1620
                }
1621
                var img_data = this._read_image_from_request(image, msg, xhr);
1622
                this.add(img_data);
1623
                callback(this.get(id));
1624
            }, this));
1625
        },
1626

    
1627
        _read_image_from_request: function(image, msg, xhr) {
1628
            return image.image;
1629
        },
1630

    
1631
        parse: function (resp, xhr) {
1632
            // FIXME: depricated global var
1633
            var data = _.map(resp.images.values, _.bind(this.parse_meta, this));
1634
            return resp.images.values;
1635
        },
1636

    
1637
        get_meta_key: function(img, key) {
1638
            if (img.metadata && img.metadata.values && img.metadata.values[key]) {
1639
                return _.escape(img.metadata.values[key]);
1640
            }
1641
            return undefined;
1642
        },
1643

    
1644
        comparator: function(img) {
1645
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1646
        },
1647

    
1648
        parse_meta: function(img) {
1649
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1650
                if (img[key]) { return };
1651
                img[key] = this.get_meta_key(img, key) || "";
1652
            }, this));
1653
            return img;
1654
        },
1655

    
1656
        active: function() {
1657
            return this.filter(function(img){return img.get('status') != "DELETED"});
1658
        },
1659

    
1660
        predefined: function() {
1661
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
1662
        },
1663
        
1664
        fetch_for_type: function(type, complete, error) {
1665
            this.fetch({update:true, 
1666
                        success: complete, 
1667
                        error: error, 
1668
                        skip_api_error: true });
1669
        },
1670
        
1671
        get_images_for_type: function(type) {
1672
            if (this['get_{0}_images'.format(type)]) {
1673
                return this['get_{0}_images'.format(type)]();
1674
            }
1675

    
1676
            return this.active();
1677
        },
1678

    
1679
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
1680
            var load = false;
1681
            error = onError || function() {};
1682
            function complete(collection) { 
1683
                onComplete(collection.get_images_for_type(type)); 
1684
            }
1685
            
1686
            // do we need to fetch/update current collection entries
1687
            if (load) {
1688
                onStart();
1689
                this.fetch_for_type(type, complete, error);
1690
            } else {
1691
                // fallback to complete
1692
                complete(this);
1693
            }
1694
        }
1695
    })
1696

    
1697
    models.Flavors = models.Collection.extend({
1698
        model: models.Flavor,
1699
        path: 'flavors',
1700
        details: true,
1701
        noUpdate: true,
1702
        supportIncUpdates: false,
1703
        // update collection model with id passed
1704
        // making a direct call to the flavor
1705
        // api url
1706
        update_unknown_id: function(id, callback) {
1707
            var url = getUrl.call(this) + "/" + id;
1708
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
1709
            _.bind(function() {
1710
                this.add({id:id, cpu:"", ram:"", disk:"", name: "", status:"DELETED"})
1711
            }, this), _.bind(function(flv) {
1712
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1713
                this.add(flv.flavor);
1714
            }, this));
1715
        },
1716

    
1717
        parse: function (resp, xhr) {
1718
            // FIXME: depricated global var
1719
            return _.map(resp.flavors.values, function(o) { o.disk_template = o['SNF:disk_template']; return o});
1720
        },
1721

    
1722
        comparator: function(flv) {
1723
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1724
        },
1725

    
1726
        unavailable_values_for_image: function(img, flavors) {
1727
            var flavors = flavors || this.active();
1728
            var size = img.get_size();
1729
            
1730
            var index = {cpu:[], disk:[], ram:[]};
1731

    
1732
            _.each(this.active(), function(el) {
1733
                var img_size = size;
1734
                var flv_size = el.get_disk_size();
1735
                if (flv_size < img_size) {
1736
                    if (index.disk.indexOf(flv_size) == -1) {
1737
                        index.disk.push(flv_size);
1738
                    }
1739
                };
1740
            });
1741
            
1742
            return index;
1743
        },
1744

    
1745
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
1746
            if (!filter_list) { filter_list = this.models };
1747
            
1748
            return this.select(function(flv){
1749
                if (flv.get("cpu") == cpu + "" &&
1750
                   flv.get("ram") == mem + "" &&
1751
                   flv.get("disk") == disk + "" &&
1752
                   flv.get("disk_template") == disk_template &&
1753
                   filter_list.indexOf(flv) > -1) { return true; }
1754
            })[0];
1755
        },
1756
        
1757
        get_data: function(lst) {
1758
            var data = {'cpu': [], 'mem':[], 'disk':[]};
1759

    
1760
            _.each(lst, function(flv) {
1761
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1762
                    data.cpu.push(flv.get("cpu"));
1763
                }
1764
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1765
                    data.mem.push(flv.get("ram"));
1766
                }
1767
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1768
                    data.disk.push(flv.get("disk"));
1769
                }
1770
            })
1771
            
1772
            return data;
1773
        },
1774

    
1775
        active: function() {
1776
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1777
        }
1778
            
1779
    })
1780

    
1781
    models.VMS = models.Collection.extend({
1782
        model: models.VM,
1783
        path: 'servers',
1784
        details: true,
1785
        copy_image_meta: true,
1786
        
1787
        parse: function (resp, xhr) {
1788
            // FIXME: depricated after refactoring
1789
            var data = resp;
1790
            if (!resp) { return [] };
1791
            data = _.filter(_.map(resp.servers.values, _.bind(this.parse_vm_api_data, this)), function(v){return v});
1792
            return data;
1793
        },
1794
        
1795
        get_reboot_required: function() {
1796
            return this.filter(function(vm){return vm.get("reboot_required") == true})
1797
        },
1798

    
1799
        has_pending_actions: function() {
1800
            return this.filter(function(vm){return vm.pending_action}).length > 0;
1801
        },
1802

    
1803
        reset_pending_actions: function() {
1804
            this.each(function(vm) {
1805
                vm.clear_pending_action();
1806
            })
1807
        },
1808

    
1809
        do_all_pending_actions: function(success, error) {
1810
            this.each(function(vm) {
1811
                if (vm.has_pending_action()) {
1812
                    vm.call(vm.pending_action, success, error);
1813
                    vm.clear_pending_action();
1814
                }
1815
            })
1816
        },
1817
        
1818
        do_all_reboots: function(success, error) {
1819
            this.each(function(vm) {
1820
                if (vm.get("reboot_required")) {
1821
                    vm.call("reboot", success, error);
1822
                }
1823
            });
1824
        },
1825

    
1826
        reset_reboot_required: function() {
1827
            this.each(function(vm) {
1828
                vm.set({'reboot_required': undefined});
1829
            })
1830
        },
1831
        
1832
        stop_stats_update: function(exclude) {
1833
            var exclude = exclude || [];
1834
            this.each(function(vm) {
1835
                if (exclude.indexOf(vm) > -1) {
1836
                    return;
1837
                }
1838
                vm.stop_stats_update();
1839
            })
1840
        },
1841
        
1842
        has_meta: function(vm_data) {
1843
            return vm_data.metadata && vm_data.metadata.values
1844
        },
1845

    
1846
        has_addresses: function(vm_data) {
1847
            return vm_data.metadata && vm_data.metadata.values
1848
        },
1849

    
1850
        parse_vm_api_data: function(data) {
1851
            // do not add non existing DELETED entries
1852
            if (data.status && data.status == "DELETED") {
1853
                if (!this.get(data.id)) {
1854
                    return false;
1855
                }
1856
            }
1857

    
1858
            // OS attribute
1859
            if (this.has_meta(data)) {
1860
                data['OS'] = data.metadata.values.OS || "okeanos";
1861
            }
1862
            
1863
            data['firewalls'] = {};
1864
            if (data['addresses'] && data['addresses'].values) {
1865
                data['linked_to_nets'] = data['addresses'].values;
1866
                _.each(data['addresses'].values, function(f){
1867
                    if (f['firewallProfile']) {
1868
                        data['firewalls'][f['id']] = f['firewallProfile']
1869
                    }
1870
                });
1871
            }
1872
            
1873
            // if vm has no metadata, no metadata object
1874
            // is in json response, reset it to force
1875
            // value update
1876
            if (!data['metadata']) {
1877
                data['metadata'] = {values:{}};
1878
            }
1879

    
1880
            return data;
1881
        },
1882

    
1883
        create: function (name, image, flavor, meta, extra, callback) {
1884
            if (this.copy_image_meta) {
1885
                if (image.get("OS")) {
1886
                    meta['OS'] = image.get("OS");
1887
                }
1888
           }
1889
            
1890
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta}
1891
            opts = _.extend(opts, extra);
1892

    
1893
            this.api_call(this.path, "create", {'server': opts}, undefined, undefined, callback, {critical: true});
1894
        }
1895

    
1896
    })
1897

    
1898
    models.PublicKey = models.Model.extend({
1899
        path: 'keys',
1900
        base_url: '/ui/userdata',
1901
        details: false,
1902
        noUpdate: true,
1903

    
1904

    
1905
        get_public_key: function() {
1906
            return cryptico.publicKeyFromString(this.get("content"));
1907
        },
1908

    
1909
        get_filename: function() {
1910
            return "{0}.pub".format(this.get("name"));
1911
        },
1912

    
1913
        identify_type: function() {
1914
            try {
1915
                var cont = snf.util.validatePublicKey(this.get("content"));
1916
                var type = cont.split(" ")[0];
1917
                return synnefo.util.publicKeyTypesMap[type];
1918
            } catch (err) { return false };
1919
        }
1920

    
1921
    })
1922
    
1923
    models.PublicKeys = models.Collection.extend({
1924
        model: models.PublicKey,
1925
        details: false,
1926
        path: 'keys',
1927
        base_url: '/ui/userdata',
1928
        noUpdate: true,
1929

    
1930
        generate_new: function(success, error) {
1931
            snf.api.sync('create', undefined, {
1932
                url: getUrl.call(this, this.base_url) + "/generate", 
1933
                success: success, 
1934
                error: error,
1935
                skip_api_error: true
1936
            });
1937
        },
1938

    
1939
        add_crypto_key: function(key, success, error, options) {
1940
            var options = options || {};
1941
            var m = new models.PublicKey();
1942

    
1943
            // guess a name
1944
            var name_tpl = "public key";
1945
            var name = name_tpl;
1946
            var name_count = 1;
1947
            
1948
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
1949
                name = name_tpl + " " + name_count;
1950
                name_count++;
1951
            }
1952
            
1953
            m.set({name: name});
1954
            m.set({content: key});
1955
            
1956
            options.success = function () { return success(m) };
1957
            options.errror = error;
1958
            options.skip_api_error = true;
1959
            
1960
            this.create(m.attributes, options);
1961
        }
1962
    })
1963
    
1964
    // storage initialization
1965
    snf.storage.images = new models.Images();
1966
    snf.storage.flavors = new models.Flavors();
1967
    snf.storage.networks = new models.Networks();
1968
    snf.storage.vms = new models.VMS();
1969
    snf.storage.keys = new models.PublicKeys();
1970

    
1971
    //snf.storage.vms.fetch({update:true});
1972
    //snf.storage.images.fetch({update:true});
1973
    //snf.storage.flavors.fetch({update:true});
1974

    
1975
})(this);