Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / ui / static / snf / js / models.js @ 47c66641

History | View | Annotate | Download (69.2 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
    var NIC_REGEX = /^nic-([0-9]+)-([0-9]+)$/
61
    
62
    // i18n
63
    BUILDING_MESSAGES = window.BUILDING_MESSAGES || {'INIT': 'init', 'COPY': '{0}, {1}, {2}', 'FINAL': 'final'};
64

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
340
    });
341
    
342
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
343
    _.extend(models.ParamsList.prototype, bb.Events, {
344

    
345
        initialize: function(parent, param_name) {
346
            this.parent = parent;
347
            this.actions = {};
348
            this.param_name = param_name;
349
            this.length = 0;
350
        },
351
        
352
        has_action: function(action) {
353
            return this.actions[action] ? true : false;
354
        },
355
            
356
        _parse_params: function(arguments) {
357
            if (arguments.length <= 1) {
358
                return [];
359
            }
360

    
361
            var args = _.toArray(arguments);
362
            return args.splice(1);
363
        },
364

    
365
        contains: function(action, params) {
366
            params = this._parse_params(arguments);
367
            var has_action = this.has_action(action);
368
            if (!has_action) { return false };
369

    
370
            var paramsEqual = false;
371
            _.each(this.actions[action], function(action_params) {
372
                if (_.isEqual(action_params, params)) {
373
                    paramsEqual = true;
374
                }
375
            });
376
                
377
            return paramsEqual;
378
        },
379
        
380
        is_empty: function() {
381
            return _.isEmpty(this.actions);
382
        },
383

    
384
        add: function(action, params) {
385
            params = this._parse_params(arguments);
386
            if (this.contains.apply(this, arguments)) { return this };
387
            var isnew = false
388
            if (!this.has_action(action)) {
389
                this.actions[action] = [];
390
                isnew = true;
391
            };
392

    
393
            this.actions[action].push(params);
394
            this.parent.trigger("change:" + this.param_name, this.parent, this);
395
            if (isnew) {
396
                this.trigger("add", action, params);
397
            } else {
398
                this.trigger("change", action, params);
399
            }
400
            return this;
401
        },
402
        
403
        remove_all: function(action) {
404
            if (this.has_action(action)) {
405
                delete this.actions[action];
406
                this.parent.trigger("change:" + this.param_name, this.parent, this);
407
                this.trigger("remove", action);
408
            }
409
            return this;
410
        },
411

    
412
        reset: function() {
413
            this.actions = {};
414
            this.parent.trigger("change:" + this.param_name, this.parent, this);
415
            this.trigger("reset");
416
            this.trigger("remove");
417
        },
418

    
419
        remove: function(action, params) {
420
            params = this._parse_params(arguments);
421
            if (!this.has_action(action)) { return this };
422
            var index = -1;
423
            _.each(this.actions[action], _.bind(function(action_params) {
424
                if (_.isEqual(action_params, params)) {
425
                    index = this.actions[action].indexOf(action_params);
426
                }
427
            }, this));
428
            
429
            if (index > -1) {
430
                this.actions[action].splice(index, 1);
431
                if (_.isEmpty(this.actions[action])) {
432
                    delete this.actions[action];
433
                }
434
                this.parent.trigger("change:" + this.param_name, this.parent, this);
435
                this.trigger("remove", action, params);
436
            }
437
        }
438

    
439
    });
440

    
441
    // Image model
442
    models.Network = models.Model.extend({
443
        path: 'networks',
444
        has_status: true,
445
        defaults: {'connecting':0},
446
        
447
        initialize: function() {
448
            var ret = models.Network.__super__.initialize.apply(this, arguments);
449
            this.set({"actions": new models.ParamsList(this, "actions")});
450
            this.update_state();
451
            this.bind("change:nics", _.bind(synnefo.storage.nics.update_net_nics, synnefo.storage.nics));
452
            this.bind("change:status", _.bind(this.update_state, this));
453
            return ret;
454
        },
455

    
456
        toJSON: function() {
457
            var attrs = _.clone(this.attributes);
458
            attrs.actions = _.clone(this.get("actions").actions);
459
            return attrs;
460
        },
461
        
462
        set_state: function(val) {
463
            if (val == "PENDING" && this.get("state") == "DESTORY") {
464
                return "DESTROY";
465
            }
466
            return val;
467
        },
468

    
469
        update_state: function() {
470
            if (this.get("connecting") > 0) {
471
                this.set({state: "CONNECTING"});
472
                return
473
            }
474
            
475
            if (this.get_nics(function(nic){ return nic.get("removing") == 1}).length > 0) {
476
                this.set({state: "DISCONNECTING"});
477
                return
478
            }   
479
            
480
            if (this.contains_firewalling_nics() > 0) {
481
                this.set({state: "FIREWALLING"});
482
                return
483
            }   
484
            
485
            if (this.get("state") == "DESTROY") { 
486
                this.set({"destroyed":1});
487
            }
488
            
489
            this.set({state:this.get('status')});
490
        },
491

    
492
        is_public: function() {
493
            return this.get("public");
494
        },
495

    
496
        decrease_connecting: function() {
497
            var conn = this.get("connecting");
498
            if (!conn) { conn = 0 };
499
            if (conn > 0) {
500
                conn--;
501
            }
502
            this.set({"connecting": conn});
503
            this.update_state();
504
        },
505

    
506
        increase_connecting: function() {
507
            var conn = this.get("connecting");
508
            if (!conn) { conn = 0 };
509
            conn++;
510
            this.set({"connecting": conn});
511
            this.update_state();
512
        },
513

    
514
        connected_to: function(vm) {
515
            return this.get('linked_to').indexOf(""+vm.id) > -1;
516
        },
517

    
518
        connected_with_nic_id: function(nic_id) {
519
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
520
        },
521

    
522
        get_nics: function(filter) {
523
            var nics = synnefo.storage.nics.filter(function(nic) {
524
                return nic.get('network_id') == this.id;
525
            }, this);
526

    
527
            if (filter) {
528
                return _.filter(nics, filter);
529
            }
530
            return nics;
531
        },
532

    
533
        contains_firewalling_nics: function() {
534
            return this.get_nics(function(n){return n.get('pending_firewall')}).length
535
        },
536

    
537
        call: function(action, params, success, error) {
538
            if (action == "destroy") {
539
                this.set({state:"DESTROY"});
540
                this.get("actions").remove("destroy", params);
541
                this.remove(_.bind(function(){
542
                    success();
543
                }, this), error);
544
            }
545
            
546
            if (action == "disconnect") {
547
                if (this.get("state") == "DESTROY") {
548
                    return;
549
                }
550

    
551
                _.each(params, _.bind(function(nic_id) {
552
                    var nic = snf.storage.nics.get(nic_id);
553
                    this.get("actions").remove("disconnect", nic_id);
554
                    if (nic) {
555
                        this.remove_nic(nic, success, error);
556
                    }
557
                }, this));
558
            }
559
        },
560

    
561
        add_vm: function (vm, callback, error, options) {
562
            var payload = {add:{serverRef:"" + vm.id}};
563
            payload._options = options || {};
564
            return this.api_call(this.api_path() + "/action", "create", 
565
                                 payload,
566
                                 _.bind(function(){
567
                                     //this.vms.add_pending(vm.id);
568
                                     this.increase_connecting();
569
                                     if (callback) {callback()}
570
                                 },this), error);
571
        },
572

    
573
        remove_nic: function (nic, callback, error, options) {
574
            var payload = {remove:{attachment:"" + nic.get("attachment_id")}};
575
            payload._options = options || {};
576
            return this.api_call(this.api_path() + "/action", "create", 
577
                                 payload,
578
                                 _.bind(function(){
579
                                     nic.set({"removing": 1});
580
                                     nic.get_network().update_state();
581
                                     //this.vms.add_pending_for_remove(vm.id);
582
                                     if (callback) {callback()}
583
                                 },this), error);
584
        },
585

    
586
        rename: function(name, callback) {
587
            return this.api_call(this.api_path(), "update", {
588
                network:{name:name}, 
589
                _options:{
590
                    critical: false, 
591
                    error_params:{
592
                        title: "Network action failed",
593
                        ns: "Networks",
594
                        extra_details: {"Network id": this.id}
595
                    }
596
                }}, callback);
597
        },
598

    
599
        get_connectable_vms: function() {
600
            return storage.vms.filter(function(vm){
601
                return !vm.in_error_state();
602
            })
603
        },
604

    
605
        state_message: function() {
606
            if (this.get("state") == "ACTIVE" && !this.is_public()) {
607
                if (this.get("cidr") && this.get("dhcp") == true) {
608
                    return this.get("cidr");
609
                } else {
610
                    return "Private network";
611
                }
612
            }
613
            if (this.get("state") == "ACTIVE" && this.is_public()) {
614
                  return "Public network";
615
            }
616

    
617
            return models.Network.STATES[this.get("state")];
618
        },
619

    
620
        in_progress: function() {
621
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
622
        },
623

    
624
        do_all_pending_actions: function(success, error) {
625
            var destroy = this.get("actions").has_action("destroy");
626
            _.each(this.get("actions").actions, _.bind(function(params, action) {
627
                _.each(params, _.bind(function(with_params) {
628
                    this.call(action, with_params, success, error);
629
                }, this));
630
            }, this));
631
            this.get("actions").reset();
632
        }
633
    });
634
    
635
    models.Network.STATES = {
636
        'ACTIVE': 'Private network',
637
        'CONNECTING': 'Connecting...',
638
        'DISCONNECTING': 'Disconnecting...',
639
        'FIREWALLING': 'Firewall update...',
640
        'DESTROY': 'Destroying...',
641
        'PENDING': 'Pending...',
642
        'ERROR': 'Error'
643
    }
644

    
645
    models.Network.STATES_TRANSITIONS = {
646
        'CONNECTING': ['ACTIVE'],
647
        'DISCONNECTING': ['ACTIVE'],
648
        'PENDING': ['ACTIVE'],
649
        'FIREWALLING': ['ACTIVE']
650
    }
651

    
652
    // Virtualmachine model
653
    models.VM = models.Model.extend({
654

    
655
        path: 'servers',
656
        has_status: true,
657
        initialize: function(params) {
658
            
659
            this.pending_firewalls = {};
660
            
661
            models.VM.__super__.initialize.apply(this, arguments);
662

    
663
            this.set({state: params.status || "ERROR"});
664
            this.log = new snf.logging.logger("VM " + this.id);
665
            this.pending_action = undefined;
666
            
667
            // init stats parameter
668
            this.set({'stats': undefined}, {silent: true});
669
            // defaults to not update the stats
670
            // each view should handle this vm attribute 
671
            // depending on if it displays stat images or not
672
            this.do_update_stats = false;
673
            
674
            // interval time
675
            // this will dynamicaly change if the server responds that
676
            // images get refreshed on different intervals
677
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
678
            this.stats_available = false;
679

    
680
            // initialize interval
681
            this.init_stats_intervals(this.stats_update_interval);
682
            
683
            this.bind("change:progress", _.bind(this.update_building_progress, this));
684
            this.update_building_progress();
685

    
686
            // default values
687
            this.bind("change:state", _.bind(function(){
688
                if (this.state() == "DESTROY") { 
689
                    this.handle_destroy() 
690
                }
691
            }, this));
692

    
693
            this.bind("change:nics", _.bind(synnefo.storage.nics.update_vm_nics, synnefo.storage.nics));
694
        },
695

    
696
        status: function(st) {
697
            if (!st) { return this.get("status")}
698
            return this.set({status:st});
699
        },
700

    
701
        set_status: function(st) {
702
            var new_state = this.state_for_api_status(st);
703
            var transition = false;
704

    
705
            if (this.state() != new_state) {
706
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
707
                    transition = this.state();
708
                }
709
            }
710
            
711
            // call it silently to avoid double change trigger
712
            this.set({'state': this.state_for_api_status(st)}, {silent: true});
713
            
714
            // trigger transition
715
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
716
                this.trigger("transition", {from:transition, to:new_state}) 
717
            };
718
            return st;
719
        },
720

    
721
        update_building_progress: function() {
722
            if (this.is_building()) {
723
                var progress = this.get("progress");
724
                if (progress == 0) {
725
                    this.state("BUILD_INIT");
726
                    this.set({progress_message: BUILDING_MESSAGES['INIT']});
727
                }
728
                if (progress > 0 && progress < 99) {
729
                    this.state("BUILD_COPY");
730
                    this.get_copy_details(true, undefined, _.bind(function(details){
731
                        this.set({
732
                            progress_message: BUILDING_MESSAGES['COPY'].format(details.copy, 
733
                                                                               details.size, 
734
                                                                               details.progress)
735
                        });
736
                    }, this));
737
                }
738
                if (progress == 100) {
739
                    this.state("BUILD_FINAL");
740
                    this.set({progress_message: BUILDING_MESSAGES['FINAL']});
741
                }
742
            } else {
743
            }
744
        },
745

    
746
        get_copy_details: function(human, image, callback) {
747
            var human = human || false;
748
            var image = image || this.get_image(_.bind(function(image){
749
                var progress = this.get('progress');
750
                var size = image.get_size();
751
                var size_copied = (size * progress / 100).toFixed(2);
752
                
753
                if (human) {
754
                    size = util.readablizeBytes(size*1024*1024);
755
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
756
                }
757

    
758
                callback({'progress': progress, 'size': size, 'copy': size_copied})
759
            }, this));
760
        },
761

    
762
        start_stats_update: function(force_if_empty) {
763
            var prev_state = this.do_update_stats;
764

    
765
            this.do_update_stats = true;
766
            
767
            // fetcher initialized ??
768
            if (!this.stats_fetcher) {
769
                this.init_stats_intervals();
770
            }
771

    
772

    
773
            // fetcher running ???
774
            if (!this.stats_fetcher.running || !prev_state) {
775
                this.stats_fetcher.start();
776
            }
777

    
778
            if (force_if_empty && this.get("stats") == undefined) {
779
                this.update_stats(true);
780
            }
781
        },
782

    
783
        stop_stats_update: function(stop_calls) {
784
            this.do_update_stats = false;
785

    
786
            if (stop_calls) {
787
                this.stats_fetcher.stop();
788
            }
789
        },
790

    
791
        // clear and reinitialize update interval
792
        init_stats_intervals: function (interval) {
793
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
794
            this.stats_fetcher.start();
795
        },
796
        
797
        get_stats_fetcher: function(timeout) {
798
            var cb = _.bind(function(data){
799
                this.update_stats();
800
            }, this);
801
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
802
            return fetcher;
803
        },
804

    
805
        // do the api call
806
        update_stats: function(force) {
807
            // do not update stats if flag not set
808
            if ((!this.do_update_stats && !force) || this.updating_stats) {
809
                return;
810
            }
811

    
812
            // make the api call, execute handle_stats_update on sucess
813
            // TODO: onError handler ???
814
            stats_url = this.url() + "/stats";
815
            this.updating_stats = true;
816
            this.sync("read", this, {
817
                handles_error:true, 
818
                url: stats_url, 
819
                refresh:true, 
820
                success: _.bind(this.handle_stats_update, this),
821
                error: _.bind(this.handle_stats_error, this),
822
                complete: _.bind(function(){this.updating_stats = false;}, this),
823
                critical: false,
824
                log_error: false,
825
                skips_timeouts: true
826
            });
827
        },
828

    
829
        get_stats_image: function(stat, type) {
830
        },
831
        
832
        _set_stats: function(stats) {
833
            var silent = silent === undefined ? false : silent;
834
            // unavailable stats while building
835
            if (this.get("status") == "BUILD") { 
836
                this.stats_available = false;
837
            } else { this.stats_available = true; }
838

    
839
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
840
            
841
            this.set({stats: stats}, {silent:true});
842
            this.trigger("stats:update", stats);
843
        },
844

    
845
        unbind: function() {
846
            models.VM.__super__.unbind.apply(this, arguments);
847
        },
848

    
849
        handle_stats_error: function() {
850
            stats = {};
851
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
852
                stats[k] = false;
853
            });
854

    
855
            this.set({'stats': stats});
856
        },
857

    
858
        // this method gets executed after a successful vm stats api call
859
        handle_stats_update: function(data) {
860
            var self = this;
861
            // avoid browser caching
862
            
863
            if (data.stats && _.size(data.stats) > 0) {
864
                var ts = $.now();
865
                var stats = data.stats;
866
                var images_loaded = 0;
867
                var images = {};
868

    
869
                function check_images_loaded() {
870
                    images_loaded++;
871

    
872
                    if (images_loaded == 4) {
873
                        self._set_stats(images);
874
                    }
875
                }
876
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
877
                    
878
                    stats[k] = stats[k] + "?_=" + ts;
879
                    
880
                    var stat = k.slice(0,3);
881
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
882
                    var img = $("<img />");
883
                    var val = stats[k];
884
                    
885
                    // load stat image to a temporary dom element
886
                    // update model stats on image load/error events
887
                    img.load(function() {
888
                        images[k] = val;
889
                        check_images_loaded();
890
                    });
891

    
892
                    img.error(function() {
893
                        images[stat + type] = false;
894
                        check_images_loaded();
895
                    });
896

    
897
                    img.attr({'src': stats[k]});
898
                })
899
                data.stats = stats;
900
            }
901

    
902
            // do we need to change the interval ??
903
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
904
                this.stats_update_interval = data.stats.refresh * 1000;
905
                this.stats_fetcher.interval = this.stats_update_interval;
906
                this.stats_fetcher.maximum_interval = this.stats_update_interval;
907
                this.stats_fetcher.stop();
908
                this.stats_fetcher.start(false);
909
            }
910
        },
911

    
912
        // helper method that sets the do_update_stats
913
        // in the future this method could also make an api call
914
        // immediaetly if needed
915
        enable_stats_update: function() {
916
            this.do_update_stats = true;
917
        },
918
        
919
        handle_destroy: function() {
920
            this.stats_fetcher.stop();
921
        },
922

    
923
        require_reboot: function() {
924
            if (this.is_active()) {
925
                this.set({'reboot_required': true});
926
            }
927
        },
928
        
929
        set_pending_action: function(data) {
930
            this.pending_action = data;
931
            return data;
932
        },
933

    
934
        // machine has pending action
935
        update_pending_action: function(action, force) {
936
            this.set({pending_action: action});
937
        },
938

    
939
        clear_pending_action: function() {
940
            this.set({pending_action: undefined});
941
        },
942

    
943
        has_pending_action: function() {
944
            return this.get("pending_action") ? this.get("pending_action") : false;
945
        },
946
        
947
        // machine is active
948
        is_active: function() {
949
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
950
        },
951
        
952
        // machine is building 
953
        is_building: function() {
954
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
955
        },
956
        
957
        in_error_state: function() {
958
            return this.state() === "ERROR"
959
        },
960

    
961
        // user can connect to machine
962
        is_connectable: function() {
963
            // check if ips exist
964
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
965
                return false;
966
            }
967
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
968
        },
969
        
970
        remove_meta: function(key, complete, error) {
971
            var url = this.api_path() + "/meta/" + key;
972
            this.api_call(url, "delete", undefined, complete, error);
973
        },
974

    
975
        save_meta: function(meta, complete, error) {
976
            var url = this.api_path() + "/meta/" + meta.key;
977
            var payload = {meta:{}};
978
            payload.meta[meta.key] = meta.value;
979
            payload._options = {
980
                critical:false, 
981
                error_params: {
982
                    title: "Machine metadata error",
983
                    extra_details: {"Machine id": this.id}
984
            }};
985

    
986
            this.api_call(url, "update", payload, complete, error);
987
        },
988

    
989

    
990
        // update/get the state of the machine
991
        state: function() {
992
            var args = slice.call(arguments);
993
                
994
            // TODO: it might not be a good idea to set the state in set_state method
995
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
996
                this.set({'state': args[0]});
997
            }
998

    
999
            return this.get('state');
1000
        },
1001
        
1002
        // get the state that the api status corresponds to
1003
        state_for_api_status: function(status) {
1004
            return this.state_transition(this.state(), status);
1005
        },
1006
        
1007
        // vm state equals vm api status
1008
        state_is_status: function(state) {
1009
            return models.VM.STATUSES.indexOf(state) != -1;
1010
        },
1011
        
1012
        // get transition state for the corresponging api status
1013
        state_transition: function(state, new_status) {
1014
            var statuses = models.VM.STATES_TRANSITIONS[state];
1015
            if (statuses) {
1016
                if (statuses.indexOf(new_status) > -1) {
1017
                    return new_status;
1018
                } else {
1019
                    return state;
1020
                }
1021
            } else {
1022
                return new_status;
1023
            }
1024
        },
1025
        
1026
        // the current vm state is a transition state
1027
        in_transition: function() {
1028
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
1029
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
1030
        },
1031
        
1032
        // get image object
1033
        get_image: function(callback) {
1034
            var image = storage.images.get(this.get('imageRef'));
1035
            if (!image) {
1036
                storage.images.update_unknown_id(this.get('imageRef'), callback);
1037
                return;
1038
            }
1039
            callback(image);
1040
            return image;
1041
        },
1042
        
1043
        // get flavor object
1044
        get_flavor: function() {
1045
            var flv = storage.flavors.get(this.get('flavorRef'));
1046
            if (!flv) {
1047
                storage.flavors.update_unknown_id(this.get('flavorRef'));
1048
                flv = storage.flavors.get(this.get('flavorRef'));
1049
            }
1050
            return flv;
1051
        },
1052

    
1053
        // retrieve the metadata object
1054
        get_meta: function(key) {
1055
            try {
1056
                return _.escape(this.get('metadata').values[key]);
1057
            } catch (err) {
1058
                return {};
1059
            }
1060
        },
1061
        
1062
        // get metadata OS value
1063
        get_os: function() {
1064
            return this.get_meta('OS') || (this.get_image(function(){}) ? 
1065
                                          this.get_image(function(){}).get_os() || "okeanos" : "okeanos");
1066
        },
1067

    
1068
        get_gui: function() {
1069
            return this.get_meta('GUI');
1070
        },
1071
        
1072
        connected_to: function(net) {
1073
            return this.get('linked_to').indexOf(net.id) > -1;
1074
        },
1075

    
1076
        connected_with_nic_id: function(nic_id) {
1077
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
1078
        },
1079

    
1080
        get_nics: function(filter) {
1081
            ret = synnefo.storage.nics.filter(function(nic) {
1082
                return parseInt(nic.get('vm_id')) == this.id;
1083
            }, this);
1084

    
1085
            if (filter) {
1086
                return _.filter(ret, filter);
1087
            }
1088

    
1089
            return ret;
1090
        },
1091

    
1092
        get_net_nics: function(net_id) {
1093
            return this.get_nics(function(n){return n.get('network_id') == net_id});
1094
        },
1095

    
1096
        get_public_nic: function() {
1097
            return this.get_nics(function(n){ return n.get_network().is_public() === true })[0];
1098
        },
1099

    
1100
        get_nic: function(net_id) {
1101
        },
1102

    
1103
        has_firewall: function() {
1104
            var nic = this.get_public_nic();
1105
            if (nic) {
1106
                var profile = nic.get('firewallProfile'); 
1107
                return ['ENABLED', 'PROTECTED'].indexOf(profile) > -1;
1108
            }
1109
            return false;
1110
        },
1111

    
1112
        get_firewall_profile: function() {
1113
            var nic = this.get_public_nic();
1114
            if (nic) {
1115
                return nic.get('firewallProfile');
1116
            }
1117
            return null;
1118
        },
1119

    
1120
        get_addresses: function() {
1121
            var pnic = this.get_public_nic();
1122
            if (!pnic) { return {'ip4': undefined, 'ip6': undefined }};
1123
            return {'ip4': pnic.get('ipv4'), 'ip6': pnic.get('ipv6')};
1124
        },
1125
    
1126
        // get actions that the user can execute
1127
        // depending on the vm state/status
1128
        get_available_actions: function() {
1129
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1130
        },
1131

    
1132
        set_profile: function(profile, net_id) {
1133
        },
1134
        
1135
        // call rename api
1136
        rename: function(new_name) {
1137
            //this.set({'name': new_name});
1138
            this.sync("update", this, {
1139
                critical: true,
1140
                data: {
1141
                    'server': {
1142
                        'name': new_name
1143
                    }
1144
                }, 
1145
                // do the rename after the method succeeds
1146
                success: _.bind(function(){
1147
                    //this.set({name: new_name});
1148
                    snf.api.trigger("call");
1149
                }, this)
1150
            });
1151
        },
1152
        
1153
        get_console_url: function(data) {
1154
            var url_params = {
1155
                machine: this.get("name"),
1156
                host_ip: this.get_addresses().ip4,
1157
                host_ip_v6: this.get_addresses().ip6,
1158
                host: data.host,
1159
                port: data.port,
1160
                password: data.password
1161
            }
1162
            return '/machines/console?' + $.param(url_params);
1163
        },
1164

    
1165
        // action helper
1166
        call: function(action_name, success, error, params) {
1167
            var id_param = [this.id];
1168
            
1169
            params = params || {};
1170
            success = success || function() {};
1171
            error = error || function() {};
1172

    
1173
            var self = this;
1174

    
1175
            switch(action_name) {
1176
                case 'start':
1177
                    this.__make_api_call(this.get_action_url(), // vm actions url
1178
                                         "create", // create so that sync later uses POST to make the call
1179
                                         {start:{}}, // payload
1180
                                         function() {
1181
                                             // set state after successful call
1182
                                             self.state("START"); 
1183
                                             success.apply(this, arguments);
1184
                                             snf.api.trigger("call");
1185
                                         },  
1186
                                         error, 'start', params);
1187
                    break;
1188
                case 'reboot':
1189
                    this.__make_api_call(this.get_action_url(), // vm actions url
1190
                                         "create", // create so that sync later uses POST to make the call
1191
                                         {reboot:{type:"HARD"}}, // payload
1192
                                         function() {
1193
                                             // set state after successful call
1194
                                             self.state("REBOOT"); 
1195
                                             success.apply(this, arguments)
1196
                                             snf.api.trigger("call");
1197
                                             self.set({'reboot_required': false});
1198
                                         },
1199
                                         error, 'reboot', params);
1200
                    break;
1201
                case 'shutdown':
1202
                    this.__make_api_call(this.get_action_url(), // vm actions url
1203
                                         "create", // create so that sync later uses POST to make the call
1204
                                         {shutdown:{}}, // payload
1205
                                         function() {
1206
                                             // set state after successful call
1207
                                             self.state("SHUTDOWN"); 
1208
                                             success.apply(this, arguments)
1209
                                             snf.api.trigger("call");
1210
                                         },  
1211
                                         error, 'shutdown', params);
1212
                    break;
1213
                case 'console':
1214
                    this.__make_api_call(this.url() + "/action", "create", {'console': {'type':'vnc'}}, function(data) {
1215
                        var cons_data = data.console;
1216
                        success.apply(this, [cons_data]);
1217
                    }, undefined, 'console', params)
1218
                    break;
1219
                case 'destroy':
1220
                    this.__make_api_call(this.url(), // vm actions url
1221
                                         "delete", // create so that sync later uses POST to make the call
1222
                                         undefined, // payload
1223
                                         function() {
1224
                                             // set state after successful call
1225
                                             self.state('DESTROY');
1226
                                             success.apply(this, arguments)
1227
                                         },  
1228
                                         error, 'destroy', params);
1229
                    break;
1230
                default:
1231
                    throw "Invalid VM action ("+action_name+")";
1232
            }
1233
        },
1234
        
1235
        __make_api_call: function(url, method, data, success, error, action, extra_params) {
1236
            var self = this;
1237
            error = error || function(){};
1238
            success = success || function(){};
1239

    
1240
            var params = {
1241
                url: url,
1242
                data: data,
1243
                success: function(){ self.handle_action_succeed.apply(self, arguments); success.apply(this, arguments)},
1244
                error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)},
1245
                error_params: { ns: "Machines actions", 
1246
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1247
                                extra_details: { 'Machine ID': this.id, 'URL': url, 'Action': action || "undefined" },
1248
                                allow_reload: false
1249
                              },
1250
                display: false,
1251
                critical: false
1252
            }
1253
            _.extend(params, extra_params)
1254
            this.sync(method, this, params);
1255
        },
1256

    
1257
        handle_action_succeed: function() {
1258
            this.trigger("action:success", arguments);
1259
        },
1260
        
1261
        reset_action_error: function() {
1262
            this.action_error = false;
1263
            this.trigger("action:fail:reset", this.action_error);
1264
        },
1265

    
1266
        handle_action_fail: function() {
1267
            this.action_error = arguments;
1268
            this.trigger("action:fail", arguments);
1269
        },
1270

    
1271
        get_action_url: function(name) {
1272
            return this.url() + "/action";
1273
        },
1274

    
1275
        get_connection_info: function(host_os, success, error) {
1276
            var url = "/machines/connect";
1277
            params = {
1278
                ip_address: this.get_addresses().ip4,
1279
                os: this.get_os(),
1280
                host_os: host_os,
1281
                srv: this.id
1282
            }
1283

    
1284
            url = url + "?" + $.param(params);
1285

    
1286
            var ajax = snf.api.sync("read", undefined, { url: url, 
1287
                                                         error:error, 
1288
                                                         success:success, 
1289
                                                         handles_error:1});
1290
        }
1291
    })
1292
    
1293
    models.VM.ACTIONS = [
1294
        'start',
1295
        'shutdown',
1296
        'reboot',
1297
        'console',
1298
        'destroy'
1299
    ]
1300

    
1301
    models.VM.AVAILABLE_ACTIONS = {
1302
        'UNKNWON'       : ['destroy'],
1303
        'BUILD'         : ['destroy'],
1304
        'REBOOT'        : ['shutdown', 'destroy', 'console'],
1305
        'STOPPED'       : ['start', 'destroy'],
1306
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1307
        'ERROR'         : ['destroy'],
1308
        'DELETED'        : [],
1309
        'DESTROY'       : [],
1310
        'BUILD_INIT'    : ['destroy'],
1311
        'BUILD_COPY'    : ['destroy'],
1312
        'BUILD_FINAL'   : ['destroy'],
1313
        'SHUTDOWN'      : ['destroy'],
1314
        'START'         : [],
1315
        'CONNECT'       : [],
1316
        'DISCONNECT'    : []
1317
    }
1318

    
1319
    // api status values
1320
    models.VM.STATUSES = [
1321
        'UNKNWON',
1322
        'BUILD',
1323
        'REBOOT',
1324
        'STOPPED',
1325
        'ACTIVE',
1326
        'ERROR',
1327
        'DELETED'
1328
    ]
1329

    
1330
    // api status values
1331
    models.VM.CONNECT_STATES = [
1332
        'ACTIVE',
1333
        'REBOOT',
1334
        'SHUTDOWN'
1335
    ]
1336

    
1337
    // vm states
1338
    models.VM.STATES = models.VM.STATUSES.concat([
1339
        'DESTROY',
1340
        'BUILD_INIT',
1341
        'BUILD_COPY',
1342
        'BUILD_FINAL',
1343
        'SHUTDOWN',
1344
        'START',
1345
        'CONNECT',
1346
        'DISCONNECT',
1347
        'FIREWALL'
1348
    ]);
1349
    
1350
    models.VM.STATES_TRANSITIONS = {
1351
        'DESTROY' : ['DELETED'],
1352
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1353
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1354
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1355
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1356
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1357
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1358
        'BUILD_COPY': ['ERROR', 'ACTIVE', 'BUILD_FINAL', 'DESTROY'],
1359
        'BUILD_FINAL': ['ERROR', 'ACTIVE', 'DESTROY'],
1360
        'BUILD_INIT': ['ERROR', 'ACTIVE', 'BUILD_COPY', 'BUILD_FINAL', 'DESTROY']
1361
    }
1362

    
1363
    models.VM.TRANSITION_STATES = [
1364
        'DESTROY',
1365
        'SHUTDOWN',
1366
        'START',
1367
        'REBOOT',
1368
        'BUILD'
1369
    ]
1370

    
1371
    models.VM.ACTIVE_STATES = [
1372
        'BUILD', 'REBOOT', 'ACTIVE',
1373
        'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL',
1374
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1375
    ]
1376

    
1377
    models.VM.BUILDING_STATES = [
1378
        'BUILD', 'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL'
1379
    ]
1380

    
1381
    models.Networks = models.Collection.extend({
1382
        model: models.Network,
1383
        path: 'networks',
1384
        details: true,
1385
        //noUpdate: true,
1386
        defaults: {'nics':[],'linked_to':[]},
1387
        
1388
        parse: function (resp, xhr) {
1389
            // FIXME: depricated global var
1390
            if (!resp) { return []};
1391
               
1392
            var data = _.map(resp.networks.values, _.bind(this.parse_net_api_data, this));
1393
            return data;
1394
        },
1395

    
1396
        add: function() {
1397
            ret = models.Networks.__super__.add.apply(this, arguments);
1398
            // update nics after each network addition
1399
            ret.each(function(r){
1400
                synnefo.storage.nics.update_net_nics(r);
1401
            });
1402
        },
1403

    
1404
        reset_pending_actions: function() {
1405
            this.each(function(net) {
1406
                net.get("actions").reset();
1407
            });
1408
        },
1409

    
1410
        do_all_pending_actions: function() {
1411
            this.each(function(net) {
1412
                net.do_all_pending_actions();
1413
            })
1414
        },
1415

    
1416
        parse_net_api_data: function(data) {
1417
            // append nic metadata
1418
            // net.get('nics') contains a list of vm/index objects 
1419
            // e.g. {'vm_id':12231, 'index':1}
1420
            // net.get('linked_to') contains a list of vms the network is 
1421
            // connected to e.g. [1001, 1002]
1422
            if (data.attachments && data.attachments.values) {
1423
                data['nics'] = {};
1424
                data['linked_to'] = [];
1425
                _.each(data.attachments.values, function(nic_id){
1426
                  
1427
                  var vm_id = NIC_REGEX.exec(nic_id)[1];
1428
                  var nic_index = parseInt(NIC_REGEX.exec(nic_id)[2]);
1429

    
1430
                  if (vm_id !== undefined && nic_index !== undefined) {
1431
                      data['nics'][nic_id] = {
1432
                          'vm_id': vm_id, 
1433
                          'index': nic_index, 
1434
                          'id': nic_id
1435
                      };
1436
                      if (data['linked_to'].indexOf(vm_id) == -1) {
1437
                        data['linked_to'].push(vm_id);
1438
                      }
1439
                  }
1440
                });
1441
            }
1442
            return data;
1443
        },
1444

    
1445
        create: function (name, type, cidr, dhcp, callback) {
1446
            var params = {
1447
                network:{
1448
                    name:name
1449
                }
1450
            };
1451

    
1452
            if (type) {
1453
                params.network.type = type;
1454
            }
1455
            if (cidr) {
1456
                params.network.cidr = cidr;
1457
            }
1458
            if (dhcp) {
1459
                params.network.dhcp = dhcp;
1460
            }
1461

    
1462
            if (dhcp === false) {
1463
                params.network.dhcp = false;
1464
            }
1465
            
1466
            return this.api_call(this.path, "create", params, callback);
1467
        }
1468
    })
1469

    
1470
    models.Images = models.Collection.extend({
1471
        model: models.Image,
1472
        path: 'images',
1473
        details: true,
1474
        noUpdate: true,
1475
        supportIncUpdates: false,
1476
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1477
        read_method: 'read',
1478

    
1479
        // update collection model with id passed
1480
        // making a direct call to the image
1481
        // api url
1482
        update_unknown_id: function(id, callback) {
1483
            var url = getUrl.call(this) + "/" + id;
1484
            this.api_call(this.path + "/" + id, this.read_method, {
1485
              _options:{
1486
                async:true, 
1487
                skip_api_error:true}
1488
              }, undefined, 
1489
            _.bind(function() {
1490
                if (!this.get(id)) {
1491
                            if (this.fallback_service) {
1492
                        // if current service has fallback_service attribute set
1493
                        // use this service to retrieve the missing image model
1494
                        var tmpservice = new this.fallback_service();
1495
                        tmpservice.update_unknown_id(id, _.bind(function(img){
1496
                            img.attributes.status = "DELETED";
1497
                            this.add(img.attributes);
1498
                            callback(this.get(id));
1499
                        }, this));
1500
                    } else {
1501
                        var title = synnefo.config.image_deleted_title || 'Deleted';
1502
                        // else add a dummy DELETED state image entry
1503
                        this.add({id:id, name:title, size:-1, 
1504
                                  progress:100, status:"DELETED"});
1505
                        callback(this.get(id));
1506
                    }   
1507
                } else {
1508
                    callback(this.get(id));
1509
                }
1510
            }, this), _.bind(function(image, msg, xhr) {
1511
                if (!image) {
1512
                    var title = synnefo.config.image_deleted_title || 'Deleted';
1513
                    this.add({id:id, name:title, size:-1, 
1514
                              progress:100, status:"DELETED"});
1515
                    callback(this.get(id));
1516
                    return;
1517
                }
1518
                var img_data = this._read_image_from_request(image, msg, xhr);
1519
                this.add(img_data);
1520
                callback(this.get(id));
1521
            }, this));
1522
        },
1523

    
1524
        _read_image_from_request: function(image, msg, xhr) {
1525
            return image.image;
1526
        },
1527

    
1528
        parse: function (resp, xhr) {
1529
            // FIXME: depricated global var
1530
            var data = _.map(resp.images.values, _.bind(this.parse_meta, this));
1531
            return resp.images.values;
1532
        },
1533

    
1534
        get_meta_key: function(img, key) {
1535
            if (img.metadata && img.metadata.values && img.metadata.values[key]) {
1536
                return _.escape(img.metadata.values[key]);
1537
            }
1538
            return undefined;
1539
        },
1540

    
1541
        comparator: function(img) {
1542
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1543
        },
1544

    
1545
        parse_meta: function(img) {
1546
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1547
                if (img[key]) { return };
1548
                img[key] = this.get_meta_key(img, key) || "";
1549
            }, this));
1550
            return img;
1551
        },
1552

    
1553
        active: function() {
1554
            return this.filter(function(img){return img.get('status') != "DELETED"});
1555
        },
1556

    
1557
        predefined: function() {
1558
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
1559
        },
1560
        
1561
        fetch_for_type: function(type, complete, error) {
1562
            this.fetch({update:true, 
1563
                        success: complete, 
1564
                        error: error, 
1565
                        skip_api_error: true });
1566
        },
1567
        
1568
        get_images_for_type: function(type) {
1569
            if (this['get_{0}_images'.format(type)]) {
1570
                return this['get_{0}_images'.format(type)]();
1571
            }
1572

    
1573
            return this.active();
1574
        },
1575

    
1576
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
1577
            var load = false;
1578
            error = onError || function() {};
1579
            function complete(collection) { 
1580
                onComplete(collection.get_images_for_type(type)); 
1581
            }
1582
            
1583
            // do we need to fetch/update current collection entries
1584
            if (load) {
1585
                onStart();
1586
                this.fetch_for_type(type, complete, error);
1587
            } else {
1588
                // fallback to complete
1589
                complete(this);
1590
            }
1591
        }
1592
    })
1593

    
1594
    models.Flavors = models.Collection.extend({
1595
        model: models.Flavor,
1596
        path: 'flavors',
1597
        details: true,
1598
        noUpdate: true,
1599
        supportIncUpdates: false,
1600
        // update collection model with id passed
1601
        // making a direct call to the flavor
1602
        // api url
1603
        update_unknown_id: function(id, callback) {
1604
            var url = getUrl.call(this) + "/" + id;
1605
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
1606
            _.bind(function() {
1607
                this.add({id:id, cpu:"", ram:"", disk:"", name: "", status:"DELETED"})
1608
            }, this), _.bind(function(flv) {
1609
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1610
                this.add(flv.flavor);
1611
            }, this));
1612
        },
1613

    
1614
        parse: function (resp, xhr) {
1615
            // FIXME: depricated global var
1616
            return _.map(resp.flavors.values, function(o) { o.disk_template = o['SNF:disk_template']; return o});
1617
        },
1618

    
1619
        comparator: function(flv) {
1620
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1621
        },
1622

    
1623
        unavailable_values_for_image: function(img, flavors) {
1624
            var flavors = flavors || this.active();
1625
            var size = img.get_size();
1626
            
1627
            var index = {cpu:[], disk:[], ram:[]};
1628

    
1629
            _.each(this.active(), function(el) {
1630
                var img_size = size;
1631
                var flv_size = el.get_disk_size();
1632
                if (flv_size < img_size) {
1633
                    if (index.disk.indexOf(flv_size) == -1) {
1634
                        index.disk.push(flv_size);
1635
                    }
1636
                };
1637
            });
1638
            
1639
            return index;
1640
        },
1641

    
1642
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
1643
            if (!filter_list) { filter_list = this.models };
1644
            
1645
            return this.select(function(flv){
1646
                if (flv.get("cpu") == cpu + "" &&
1647
                   flv.get("ram") == mem + "" &&
1648
                   flv.get("disk") == disk + "" &&
1649
                   flv.get("disk_template") == disk_template &&
1650
                   filter_list.indexOf(flv) > -1) { return true; }
1651
            })[0];
1652
        },
1653
        
1654
        get_data: function(lst) {
1655
            var data = {'cpu': [], 'mem':[], 'disk':[]};
1656

    
1657
            _.each(lst, function(flv) {
1658
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1659
                    data.cpu.push(flv.get("cpu"));
1660
                }
1661
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1662
                    data.mem.push(flv.get("ram"));
1663
                }
1664
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1665
                    data.disk.push(flv.get("disk"));
1666
                }
1667
            })
1668
            
1669
            return data;
1670
        },
1671

    
1672
        active: function() {
1673
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1674
        }
1675
            
1676
    })
1677

    
1678
    models.VMS = models.Collection.extend({
1679
        model: models.VM,
1680
        path: 'servers',
1681
        details: true,
1682
        copy_image_meta: true,
1683

    
1684
        parse: function (resp, xhr) {
1685
            // FIXME: depricated after refactoring
1686
            var data = resp;
1687
            if (!resp) { return [] };
1688
            data = _.filter(_.map(resp.servers.values, _.bind(this.parse_vm_api_data, this)), function(v){return v});
1689
            return data;
1690
        },
1691

    
1692
        add: function() {
1693
            ret = models.VMS.__super__.add.apply(this, arguments);
1694
            ret.each(function(r){
1695
                synnefo.storage.nics.update_vm_nics(r);
1696
            });
1697
        },
1698
        
1699
        get_reboot_required: function() {
1700
            return this.filter(function(vm){return vm.get("reboot_required") == true})
1701
        },
1702

    
1703
        has_pending_actions: function() {
1704
            return this.filter(function(vm){return vm.pending_action}).length > 0;
1705
        },
1706

    
1707
        reset_pending_actions: function() {
1708
            this.each(function(vm) {
1709
                vm.clear_pending_action();
1710
            })
1711
        },
1712

    
1713
        do_all_pending_actions: function(success, error) {
1714
            this.each(function(vm) {
1715
                if (vm.has_pending_action()) {
1716
                    vm.call(vm.pending_action, success, error);
1717
                    vm.clear_pending_action();
1718
                }
1719
            })
1720
        },
1721
        
1722
        do_all_reboots: function(success, error) {
1723
            this.each(function(vm) {
1724
                if (vm.get("reboot_required")) {
1725
                    vm.call("reboot", success, error);
1726
                }
1727
            });
1728
        },
1729

    
1730
        reset_reboot_required: function() {
1731
            this.each(function(vm) {
1732
                vm.set({'reboot_required': undefined});
1733
            })
1734
        },
1735
        
1736
        stop_stats_update: function(exclude) {
1737
            var exclude = exclude || [];
1738
            this.each(function(vm) {
1739
                if (exclude.indexOf(vm) > -1) {
1740
                    return;
1741
                }
1742
                vm.stop_stats_update();
1743
            })
1744
        },
1745
        
1746
        has_meta: function(vm_data) {
1747
            return vm_data.metadata && vm_data.metadata.values
1748
        },
1749

    
1750
        has_addresses: function(vm_data) {
1751
            return vm_data.metadata && vm_data.metadata.values
1752
        },
1753

    
1754
        parse_vm_api_data: function(data) {
1755
            // do not add non existing DELETED entries
1756
            if (data.status && data.status == "DELETED") {
1757
                if (!this.get(data.id)) {
1758
                    return false;
1759
                }
1760
            }
1761

    
1762
            // OS attribute
1763
            if (this.has_meta(data)) {
1764
                data['OS'] = data.metadata.values.OS || "okeanos";
1765
            }
1766
            
1767

    
1768
            // network metadata
1769
            data['firewalls'] = {};
1770
            data['nics'] = {};
1771
            data['linked_to'] = [];
1772

    
1773
            if (data['attachments'] && data['attachments'].values) {
1774
                var nics = data['attachments'].values;
1775
                _.each(nics, function(nic) {
1776
                    var net_id = nic.network_id;
1777
                    var index = parseInt(NIC_REGEX.exec(nic.id)[2]);
1778
                    if (data['linked_to'].indexOf(net_id) == -1) {
1779
                        data['linked_to'].push(net_id);
1780
                    }
1781

    
1782
                    data['nics'][nic.id] = nic;
1783
                })
1784
            }
1785
            
1786
            // if vm has no metadata, no metadata object
1787
            // is in json response, reset it to force
1788
            // value update
1789
            if (!data['metadata']) {
1790
                data['metadata'] = {values:{}};
1791
            }
1792

    
1793
            return data;
1794
        },
1795

    
1796
        create: function (name, image, flavor, meta, extra, callback) {
1797
            if (this.copy_image_meta) {
1798
                if (image.get("OS")) {
1799
                    meta['OS'] = image.get("OS");
1800
                }
1801
           }
1802
            
1803
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta}
1804
            opts = _.extend(opts, extra);
1805

    
1806
            this.api_call(this.path, "create", {'server': opts}, undefined, undefined, callback, {critical: true});
1807
        }
1808

    
1809
    })
1810
    
1811
    models.NIC = models.Model.extend({
1812
        
1813
        initialize: function() {
1814
            models.NIC.__super__.initialize.apply(this, arguments);
1815
            this.pending_for_firewall = false;
1816
            this.bind("change:firewallProfile", _.bind(this.check_firewall, this));
1817
            this.bind("change:pending_firewall", function(nic) {
1818
                nic.get_network().update_state();
1819
            });
1820
            this.get_vm().bind("remove", function(){
1821
                try {
1822
                    this.collection.remove(this);
1823
                } catch (err) {};
1824
            }, this);
1825
            this.get_network().bind("remove", function(){
1826
                try {
1827
                    this.collection.remove(this);
1828
                } catch (err) {};
1829
            }, this);
1830

    
1831
        },
1832

    
1833
        get_vm: function() {
1834
            return synnefo.storage.vms.get(parseInt(this.get('vm_id')));
1835
        },
1836

    
1837
        get_network: function() {
1838
            return synnefo.storage.networks.get(this.get('network_id'));
1839
        },
1840

    
1841
        get_v6_address: function() {
1842
            return this.get("ipv6");
1843
        },
1844

    
1845
        get_v4_address: function() {
1846
            return this.get("ipv4");
1847
        },
1848

    
1849
        set_firewall: function(value, callback, error, options) {
1850
            var net_id = this.get('network_id');
1851
            var self = this;
1852

    
1853
            // api call data
1854
            var payload = {"firewallProfile":{"profile":value}};
1855
            payload._options = _.extend({critical: false}, options);
1856
            
1857
            this.set({'pending_firewall': value});
1858
            this.set({'pending_firewall_sending': true});
1859

    
1860
            var success_cb = function() {
1861
                if (callback) {
1862
                    callback();
1863
                }
1864
                self.set({'pending_firewall_sending': false});
1865
            };
1866

    
1867
            var error_cb = function() {
1868
                self.reset_pending_firewall();
1869
            }
1870
            
1871
            this.get_vm().api_call(this.get_vm().api_path() + "/action", "create", payload, success_cb, error_cb);
1872
        },
1873

    
1874
        reset_pending_firewall: function() {
1875
            this.set({'pending_firewall': false});
1876
        },
1877

    
1878
        check_firewall: function() {
1879
            var firewall = this.get('firewallProfile');
1880
            var pending = this.get('pending_firewall');
1881
            this.reset_pending_firewall();
1882
        }
1883
        
1884
    });
1885

    
1886
    models.NICs = models.Collection.extend({
1887
        model: models.NIC,
1888
        
1889
        add_or_update: function(nic_id, data, vm) {
1890
            var params = _.clone(data);
1891
            var vm;
1892
            params.attachment_id = params.id;
1893
            params.id = params.id + '-' + params.network_id;
1894
            params.vm_id = parseInt(NIC_REGEX.exec(nic_id)[1]);
1895

    
1896
            if (!this.get(params.id)) {
1897
                this.add(params);
1898
                var nic = this.get(params.id);
1899
                vm = nic.get_vm();
1900
                nic.get_network().decrease_connecting();
1901
                nic.bind("remove", function() {
1902
                    nic.set({"removing": 0});
1903
                    if (this.get_network()) {
1904
                        // network might got removed before nic
1905
                        nic.get_network().update_state();
1906
                    }
1907
                });
1908

    
1909
            } else {
1910
                this.get(params.id).set(params);
1911
                vm = this.get(params.id).get_vm();
1912
            }
1913
            
1914
            // vm nics changed, trigger vm update
1915
            if (vm) { vm.trigger("change", vm)};
1916
        },
1917
        
1918
        reset_nics: function(nics, filter_attr, filter_val) {
1919
            var nics_to_check = this.filter(function(nic) {
1920
                return nic.get(filter_attr) == filter_val;
1921
            });
1922
            
1923
            _.each(nics_to_check, function(nic) {
1924
                if (nics.indexOf(nic.get('id')) == -1) {
1925
                    this.remove(nic);
1926
                } else {
1927
                }
1928
            }, this);
1929
        },
1930

    
1931
        update_vm_nics: function(vm) {
1932
            var nics = vm.get('nics');
1933
            this.reset_nics(_.map(nics, function(nic, key){
1934
                return key + "-" + nic.network_id;
1935
            }), 'vm_id', vm.id);
1936

    
1937
            _.each(nics, function(val, key) {
1938
                var net = synnefo.storage.networks.get(val.network_id);
1939
                if (net && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
1940
                    this.add_or_update(key, vm.get('nics')[key], vm);
1941
                }
1942
            }, this);
1943
        },
1944

    
1945
        update_net_nics: function(net) {
1946
            var nics = net.get('nics');
1947
            this.reset_nics(_.map(nics, function(nic, key){
1948
                return key + "-" + net.get('id');
1949
            }), 'network_id', net.id);
1950

    
1951
            _.each(nics, function(val, key) {
1952
                var vm = synnefo.storage.vms.get(val.vm_id);
1953
                if (vm && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
1954
                    this.add_or_update(key, vm.get('nics')[key], vm);
1955
                }
1956
            }, this);
1957
        }
1958
    });
1959

    
1960
    models.PublicKey = models.Model.extend({
1961
        path: 'keys',
1962
        base_url: '/ui/userdata',
1963
        details: false,
1964
        noUpdate: true,
1965

    
1966

    
1967
        get_public_key: function() {
1968
            return cryptico.publicKeyFromString(this.get("content"));
1969
        },
1970

    
1971
        get_filename: function() {
1972
            return "{0}.pub".format(this.get("name"));
1973
        },
1974

    
1975
        identify_type: function() {
1976
            try {
1977
                var cont = snf.util.validatePublicKey(this.get("content"));
1978
                var type = cont.split(" ")[0];
1979
                return synnefo.util.publicKeyTypesMap[type];
1980
            } catch (err) { return false };
1981
        }
1982

    
1983
    })
1984
    
1985
    models.PublicKeys = models.Collection.extend({
1986
        model: models.PublicKey,
1987
        details: false,
1988
        path: 'keys',
1989
        base_url: '/ui/userdata',
1990
        noUpdate: true,
1991

    
1992
        generate_new: function(success, error) {
1993
            snf.api.sync('create', undefined, {
1994
                url: getUrl.call(this, this.base_url) + "/generate", 
1995
                success: success, 
1996
                error: error,
1997
                skip_api_error: true
1998
            });
1999
        },
2000

    
2001
        add_crypto_key: function(key, success, error, options) {
2002
            var options = options || {};
2003
            var m = new models.PublicKey();
2004

    
2005
            // guess a name
2006
            var name_tpl = "my generated public key";
2007
            var name = name_tpl;
2008
            var name_count = 1;
2009
            
2010
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
2011
                name = name_tpl + " " + name_count;
2012
                name_count++;
2013
            }
2014
            
2015
            m.set({name: name});
2016
            m.set({content: key});
2017
            
2018
            options.success = function () { return success(m) };
2019
            options.errror = error;
2020
            options.skip_api_error = true;
2021
            
2022
            this.create(m.attributes, options);
2023
        }
2024
    })
2025
    
2026
    // storage initialization
2027
    snf.storage.images = new models.Images();
2028
    snf.storage.flavors = new models.Flavors();
2029
    snf.storage.networks = new models.Networks();
2030
    snf.storage.vms = new models.VMS();
2031
    snf.storage.keys = new models.PublicKeys();
2032
    snf.storage.nics = new models.NICs();
2033

    
2034
    //snf.storage.vms.fetch({update:true});
2035
    //snf.storage.images.fetch({update:true});
2036
    //snf.storage.flavors.fetch({update:true});
2037

    
2038
})(this);