Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / ui / static / snf / js / models.js @ 1e882dd7

History | View | Annotate | Download (73.5 kB)

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

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

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

    
50
    // logging
51
    var logger = new snf.logging.logger("SNF-MODELS");
52
    var debug = _.bind(logger.debug, logger);
53
    
54
    // get url helper
55
    var getUrl = function(baseurl) {
56
        var baseurl = baseurl || snf.config.api_urls[this.api_type];
57
        return baseurl + "/" + this.path;
58
    }
59

    
60
    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
        // return list of changed attributes that included in passed list
128
        // argument
129
        getKeysChanged: function(keys) {
130
            return _.intersection(keys, this.changedKeys());
131
        },
132
        
133
        // boolean check of keys changed
134
        keysChanged: function(keys) {
135
            return this.getKeysChanged(keys).length > 0;
136
        },
137

    
138
        // check if any of the passed attribues has changed
139
        hasOnlyChange: function(keys) {
140
            var ret = false;
141
            _.each(keys, _.bind(function(key) {
142
                if (this.changedKeys().length == 1 && this.changedKeys().indexOf(key) > -1) { ret = true};
143
            }, this));
144
            return ret;
145
        }
146

    
147
    })
148
    
149
    // Base object for all our model collections
150
    models.Collection = bb.Collection.extend({
151
        sync: snf.api.sync,
152
        api: snf.api,
153
        api_type: 'compute',
154
        supportIncUpdates: true,
155

    
156
        initialize: function() {
157
            models.Collection.__super__.initialize.apply(this, arguments);
158
            this.api_call = _.bind(this.api.call, this);
159
        },
160

    
161
        url: function(options, method) {
162
            return getUrl.call(this, this.base_url) + (
163
                    options.details || this.details && method != 'create' ? '/detail' : '');
164
        },
165

    
166
        fetch: function(options) {
167
            if (!options) { options = {} };
168
            // default to update
169
            if (!this.noUpdate) {
170
                if (options.update === undefined) { options.update = true };
171
                if (!options.removeMissing && options.refresh) { options.removeMissing = true };
172
            } else {
173
                if (options.refresh === undefined) {
174
                    options.refresh = true;
175
                }
176
            }
177
            // custom event foreach fetch
178
            return bb.Collection.prototype.fetch.call(this, options)
179
        },
180

    
181
        create: function(model, options) {
182
            var coll = this;
183
            options || (options = {});
184
            model = this._prepareModel(model, options);
185
            if (!model) return false;
186
            var success = options.success;
187
            options.success = function(nextModel, resp, xhr) {
188
                if (success) success(nextModel, resp, xhr);
189
            };
190
            model.save(null, options);
191
            return model;
192
        },
193

    
194
        get_fetcher: function(interval, increase, fast, increase_after_calls, max, initial_call, params) {
195
            var fetch_params = params || {};
196
            var handler_options = {};
197

    
198
            fetch_params.skips_timeouts = true;
199
            handler_options.interval = interval;
200
            handler_options.increase = increase;
201
            handler_options.fast = fast;
202
            handler_options.increase_after_calls = increase_after_calls;
203
            handler_options.max= max;
204
            handler_options.id = "collection id";
205

    
206
            var last_ajax = undefined;
207
            var callback = _.bind(function() {
208
                // clone to avoid referenced objects
209
                var params = _.clone(fetch_params);
210
                updater._ajax = last_ajax;
211
                
212
                // wait for previous request to finish
213
                if (last_ajax && last_ajax.readyState < 4 && last_ajax.statusText != "timeout") {
214
                    // opera readystate for 304 responses is 0
215
                    if (!($.browser.opera && last_ajax.readyState == 0 && last_ajax.status == 304)) {
216
                        return;
217
                    }
218
                }
219
                
220
                last_ajax = this.fetch(params);
221
            }, this);
222
            handler_options.callback = callback;
223

    
224
            var updater = new snf.api.updateHandler(_.clone(_.extend(handler_options, fetch_params)));
225
            snf.api.bind("call", _.throttle(_.bind(function(){ updater.faster(true)}, this)), 1000);
226
            return updater;
227
        }
228
    });
229
    
230
    // Image model
231
    models.Image = models.Model.extend({
232
        path: 'images',
233

    
234
        get_size: function() {
235
            return parseInt(this.get('metadata') ? this.get('metadata').values.size : -1)
236
        },
237

    
238
        get_meta: function(key) {
239
            if (this.get('metadata') && this.get('metadata').values && this.get('metadata').values[key]) {
240
                return _.escape(this.get('metadata').values[key]);
241
            }
242
            return undefined;
243
        },
244

    
245
        get_owner: function() {
246
            return this.get('owner') || _.keys(synnefo.config.system_images_owners)[0];
247
        },
248

    
249
        display_owner: function() {
250
            var owner = this.get_owner();
251
            if (_.include(_.keys(synnefo.config.system_images_owners), owner)) {
252
                return synnefo.config.system_images_owners[owner];
253
            } else {
254
                return owner;
255
            }
256
        },
257
    
258
        get_readable_size: function() {
259
            if (this.is_deleted()) {
260
                return synnefo.config.image_deleted_size_title || '(none)';
261
            }
262
            return this.get_size() > 0 ? util.readablizeBytes(this.get_size() * 1024 * 1024) : '(none)';
263
        },
264

    
265
        get_os: function() {
266
            return this.get("OS");
267
        },
268

    
269
        get_gui: function() {
270
            return this.get_meta('GUI');
271
        },
272

    
273
        get_created_user: function() {
274
            return synnefo.config.os_created_users[this.get_os()] || "root";
275
        },
276

    
277
        get_sort_order: function() {
278
            return parseInt(this.get('metadata') ? this.get('metadata').values.sortorder : -1)
279
        },
280

    
281
        get_vm: function() {
282
            var vm_id = this.get("serverRef");
283
            var vm = undefined;
284
            vm = storage.vms.get(vm_id);
285
            return vm;
286
        },
287

    
288
        is_public: function() {
289
            return this.get('is_public') || true;
290
        },
291

    
292
        is_deleted: function() {
293
            return this.get('status') == "DELETED"
294
        },
295
        
296
        ssh_keys_path: function() {
297
            prepend = '';
298
            if (this.get_created_user() != 'root') {
299
                prepend = '/home'
300
            }
301
            return '{1}/{0}/.ssh/authorized_keys'.format(this.get_created_user(), prepend);
302
        },
303

    
304
        _supports_ssh: function() {
305
            if (synnefo.config.support_ssh_os_list.indexOf(this.get_os()) > -1) {
306
                return true;
307
            }
308
            return false;
309
        },
310

    
311
        supports: function(feature) {
312
            if (feature == "ssh") {
313
                return this._supports_ssh()
314
            }
315
            return false;
316
        },
317

    
318
        personality_data_for_keys: function(keys) {
319
            contents = '';
320
            _.each(keys, function(key){
321
                contents = contents + key.get("content") + "\n"
322
            });
323
            contents = $.base64.encode(contents);
324

    
325
            return {
326
                path: this.ssh_keys_path(),
327
                contents: contents
328
            }
329
        }
330
    });
331

    
332
    // Flavor model
333
    models.Flavor = models.Model.extend({
334
        path: 'flavors',
335

    
336
        details_string: function() {
337
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
338
        },
339

    
340
        get_disk_size: function() {
341
            return parseInt(this.get("disk") * 1000)
342
        },
343

    
344
        get_disk_template_info: function() {
345
            var info = snf.config.flavors_disk_templates_info[this.get("disk_template")];
346
            if (!info) {
347
                info = { name: this.get("disk_template"), description:'' };
348
            }
349
            return info
350
        }
351

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

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

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

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

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

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

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

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

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

    
451
    });
452

    
453
    // Image model
454
    models.Network = models.Model.extend({
455
        path: 'networks',
456
        has_status: true,
457
        defaults: {'connecting':0},
458
        
459
        initialize: function() {
460
            var ret = models.Network.__super__.initialize.apply(this, arguments);
461
            this.set({"actions": new models.ParamsList(this, "actions")});
462
            this.update_state();
463
            this.bind("change:nics", _.bind(synnefo.storage.nics.update_net_nics, synnefo.storage.nics));
464
            this.bind("change:status", _.bind(this.update_state, this));
465
            return ret;
466
        },
467

    
468
        toJSON: function() {
469
            var attrs = _.clone(this.attributes);
470
            attrs.actions = _.clone(this.get("actions").actions);
471
            return attrs;
472
        },
473
        
474
        set_state: function(val) {
475
            if (val == "PENDING" && this.get("state") == "DESTORY") {
476
                return "DESTROY";
477
            }
478
            return val;
479
        },
480

    
481
        update_state: function() {
482
            if (this.get("connecting") > 0) {
483
                this.set({state: "CONNECTING"});
484
                return
485
            }
486
            
487
            if (this.get_nics(function(nic){ return nic.get("removing") == 1}).length > 0) {
488
                this.set({state: "DISCONNECTING"});
489
                return
490
            }   
491
            
492
            if (this.contains_firewalling_nics() > 0) {
493
                this.set({state: "FIREWALLING"});
494
                return
495
            }   
496
            
497
            if (this.get("state") == "DESTROY") { 
498
                this.set({"destroyed":1});
499
            }
500
            
501
            this.set({state:this.get('status')});
502
        },
503

    
504
        is_public: function() {
505
            return this.id === "public";
506
        },
507

    
508
        decrease_connecting: function() {
509
            var conn = this.get("connecting");
510
            if (!conn) { conn = 0 };
511
            if (conn > 0) {
512
                conn--;
513
            }
514
            this.set({"connecting": conn});
515
            this.update_state();
516
        },
517

    
518
        increase_connecting: function() {
519
            var conn = this.get("connecting");
520
            if (!conn) { conn = 0 };
521
            conn++;
522
            this.set({"connecting": conn});
523
            this.update_state();
524
        },
525

    
526
        connected_to: function(vm) {
527
            return this.get('linked_to').indexOf(""+vm.id) > -1;
528
        },
529

    
530
        connected_with_nic_id: function(nic_id) {
531
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
532
        },
533

    
534
        get_nics: function(filter) {
535
            var nics = synnefo.storage.nics.filter(function(nic) {
536
                return nic.get('network_id') == this.id;
537
            }, this);
538

    
539
            if (filter) {
540
                return _.filter(nics, filter);
541
            }
542
            return nics;
543
        },
544

    
545
        contains_firewalling_nics: function() {
546
            return this.get_nics(function(n){return n.get('pending_firewall')}).length
547
        },
548

    
549
        call: function(action, params, success, error) {
550
            if (action == "destroy") {
551
                this.set({state:"DESTROY"});
552
                this.get("actions").remove("destroy", params);
553
                this.remove(_.bind(function(){
554
                    success();
555
                }, this), error);
556
            }
557
            
558
            if (action == "disconnect") {
559
                if (this.get("state") == "DESTROY") {
560
                    return;
561
                }
562

    
563
                _.each(params, _.bind(function(nic_id) {
564
                    var nic = snf.storage.nics.get(nic_id);
565
                    this.get("actions").remove("disconnect", nic_id);
566
                    if (nic) {
567
                        this.remove_nic(nic, success, error);
568
                    }
569
                }, this));
570
            }
571
        },
572

    
573
        add_vm: function (vm, callback, error, options) {
574
            var payload = {add:{serverRef:"" + vm.id}};
575
            payload._options = options || {};
576
            return this.api_call(this.api_path() + "/action", "create", 
577
                                 payload,
578
                                 _.bind(function(){
579
                                     //this.vms.add_pending(vm.id);
580
                                     this.increase_connecting();
581
                                     if (callback) {callback()}
582
                                 },this), error);
583
        },
584

    
585
        remove_nic: function (nic, callback, error, options) {
586
            var payload = {remove:{attachment:"" + nic.get("attachment_id")}};
587
            payload._options = options || {};
588
            return this.api_call(this.api_path() + "/action", "create", 
589
                                 payload,
590
                                 _.bind(function(){
591
                                     nic.set({"removing": 1});
592
                                     nic.get_network().update_state();
593
                                     //this.vms.add_pending_for_remove(vm.id);
594
                                     if (callback) {callback()}
595
                                 },this), error);
596
        },
597

    
598
        rename: function(name, callback) {
599
            return this.api_call(this.api_path(), "update", {
600
                network:{name:name}, 
601
                _options:{
602
                    critical: false, 
603
                    error_params:{
604
                        title: "Network action failed",
605
                        ns: "Networks",
606
                        extra_details: {"Network id": this.id}
607
                    }
608
                }}, callback);
609
        },
610

    
611
        get_connectable_vms: function() {
612
            return storage.vms.filter(function(vm){
613
                return !vm.in_error_state();
614
            })
615
        },
616

    
617
        state_message: function() {
618
            if (this.get("state") == "ACTIVE" && !this.is_public()) {
619
                if (this.get("cidr") && this.get("dhcp") == true) {
620
                    return this.get("cidr");
621
                } else {
622
                    return "Private network";
623
                }
624
            }
625
            if (this.get("state") == "ACTIVE" && this.is_public()) {
626
                  return "Public network";
627
            }
628

    
629
            return models.Network.STATES[this.get("state")];
630
        },
631

    
632
        in_progress: function() {
633
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
634
        },
635

    
636
        do_all_pending_actions: function(success, error) {
637
            var destroy = this.get("actions").has_action("destroy");
638
            _.each(this.get("actions").actions, _.bind(function(params, action) {
639
                _.each(params, _.bind(function(with_params) {
640
                    this.call(action, with_params, success, error);
641
                }, this));
642
            }, this));
643
            this.get("actions").reset();
644
        }
645
    });
646
    
647
    models.Network.STATES = {
648
        'ACTIVE': 'Private network',
649
        'CONNECTING': 'Connecting...',
650
        'DISCONNECTING': 'Disconnecting...',
651
        'FIREWALLING': 'Firewall update...',
652
        'DESTROY': 'Destroying...',
653
        'PENDING': 'Pending...',
654
        'ERROR': 'Error'
655
    }
656

    
657
    models.Network.STATES_TRANSITIONS = {
658
        'CONNECTING': ['ACTIVE'],
659
        'DISCONNECTING': ['ACTIVE'],
660
        'PENDING': ['ACTIVE'],
661
        'FIREWALLING': ['ACTIVE']
662
    }
663

    
664
    // Virtualmachine model
665
    models.VM = models.Model.extend({
666

    
667
        path: 'servers',
668
        has_status: true,
669
        initialize: function(params) {
670
            
671
            this.pending_firewalls = {};
672
            
673
            models.VM.__super__.initialize.apply(this, arguments);
674

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

    
692
            // initialize interval
693
            this.init_stats_intervals(this.stats_update_interval);
694
            
695
            // handle progress message on instance change
696
            this.bind("change", _.bind(this.update_status_message, this));
697
            // force update of progress message
698
            this.update_status_message(true);
699
            
700
            // default values
701
            this.bind("change:state", _.bind(function(){
702
                if (this.state() == "DESTROY") { 
703
                    this.handle_destroy() 
704
                }
705
            }, this));
706

    
707
            this.bind("change:nics", _.bind(synnefo.storage.nics.update_vm_nics, synnefo.storage.nics));
708
        },
709

    
710
        status: function(st) {
711
            if (!st) { return this.get("status")}
712
            return this.set({status:st});
713
        },
714

    
715
        set_status: function(st) {
716
            var new_state = this.state_for_api_status(st);
717
            var transition = false;
718

    
719
            if (this.state() != new_state) {
720
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
721
                    transition = this.state();
722
                }
723
            }
724
            
725
            // call it silently to avoid double change trigger
726
            this.set({'state': this.state_for_api_status(st)}, {silent: true});
727
            
728
            // trigger transition
729
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
730
                this.trigger("transition", {from:transition, to:new_state}) 
731
            };
732
            return st;
733
        },
734
            
735
        get_diagnostics: function(success) {
736
            this.__make_api_call(this.get_diagnostics_url(),
737
                                 "read", // create so that sync later uses POST to make the call
738
                                 null, // payload
739
                                 function(data) {
740
                                     success(data);
741
                                 },  
742
                                 null, 'diagnostics');
743
        },
744

    
745
        has_diagnostics: function() {
746
            return this.get("diagnostics") && this.get("diagnostics").length;
747
        },
748

    
749
        get_progress_info: function() {
750
            // details about progress message
751
            // contains a list of diagnostic messages
752
            return this.get("status_messages");
753
        },
754

    
755
        get_status_message: function() {
756
            return this.get('status_message');
757
        },
758
        
759
        // extract status message from diagnostics
760
        status_message_from_diagnostics: function(diagnostics) {
761
            var valid_sources_map = synnefo.config.diagnostics_status_messages_map;
762
            var valid_sources = valid_sources_map[this.get('status')];
763
            if (!valid_sources) { return null };
764
            
765
            // filter messsages based on diagnostic source
766
            var messages = _.filter(diagnostics, function(diag) {
767
                return valid_sources.indexOf(diag.source) > -1;
768
            });
769

    
770
            var msg = messages[0];
771
            if (msg) {
772
              var message = msg.message;
773
              var message_tpl = snf.config.diagnostic_messages_tpls[msg.source];
774

    
775
              if (message_tpl) {
776
                  message = message_tpl.replace('MESSAGE', msg.message);
777
              }
778
              return message;
779
            }
780
            
781
            // no message to display, but vm in build state, display
782
            // finalizing message.
783
            if (this.is_building() == 'BUILD') {
784
                return synnefo.config.BUILDING_MESSAGES['FINAL'];
785
            }
786
            return null;
787
        },
788

    
789
        update_status_message: function(force) {
790
            // update only if one of the specified attributes has changed
791
            if (
792
              !this.keysChanged(['diagnostics', 'progress', 'status', 'state'])
793
                && !force
794
            ) { return };
795
            
796
            // if user requested to destroy the vm set the appropriate 
797
            // message.
798
            if (this.get('state') == "DESTROY") { 
799
                message = "Terminating..."
800
                this.set({status_message: message})
801
                return;
802
            }
803
            
804
            // set error message, if vm has diagnostic message display it as
805
            // progress message
806
            if (this.in_error_state()) {
807
                var d = this.get('diagnostics');
808
                if (d && d.length) {
809
                    var message = this.status_message_from_diagnostics(d);
810
                    this.set({status_message: message});
811
                } else {
812
                    this.set({status_message: null});
813
                }
814
                return;
815
            }
816
            
817
            // identify building status message
818
            if (this.is_building()) {
819
                var self = this;
820
                var success = function(msg) {
821
                    self.set({status_message: msg});
822
                }
823
                this.get_building_status_message(success);
824
                return;
825
            }
826

    
827
            this.set({status_message:null});
828
        },
829
            
830
        // get building status message. Asynchronous function since it requires
831
        // access to vm image.
832
        get_building_status_message: function(callback) {
833
            // no progress is set, vm is in initial build status
834
            var progress = this.get("progress");
835
            if (progress == 0 || !progress) {
836
                return callback(BUILDING_MESSAGES['INIT']);
837
            }
838
            
839
            // vm has copy progress, display copy percentage
840
            if (progress > 0 && progress <= 99) {
841
                this.get_copy_details(true, undefined, _.bind(
842
                    function(details){
843
                        callback(BUILDING_MESSAGES['COPY'].format(details.copy, 
844
                                                           details.size, 
845
                                                           details.progress));
846
                }, this));
847
                return;
848
            }
849

    
850
            // copy finished display FINAL message or identify status message
851
            // from diagnostics.
852
            if (progress >= 100) {
853
                if (!this.has_diagnostics()) {
854
                        callback(BUILDING_MESSAGES['FINAL']);
855
                } else {
856
                        var d = this.get("diagnostics");
857
                        var msg = this.status_message_from_diagnostics(d);
858
                        if (msg) {
859
                              callback(msg);
860
                        }
861
                }
862
            }
863
        },
864

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

    
877
                callback({'progress': progress, 'size': size, 'copy': size_copied})
878
            }, this));
879
        },
880

    
881
        start_stats_update: function(force_if_empty) {
882
            var prev_state = this.do_update_stats;
883

    
884
            this.do_update_stats = true;
885
            
886
            // fetcher initialized ??
887
            if (!this.stats_fetcher) {
888
                this.init_stats_intervals();
889
            }
890

    
891

    
892
            // fetcher running ???
893
            if (!this.stats_fetcher.running || !prev_state) {
894
                this.stats_fetcher.start();
895
            }
896

    
897
            if (force_if_empty && this.get("stats") == undefined) {
898
                this.update_stats(true);
899
            }
900
        },
901

    
902
        stop_stats_update: function(stop_calls) {
903
            this.do_update_stats = false;
904

    
905
            if (stop_calls) {
906
                this.stats_fetcher.stop();
907
            }
908
        },
909

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

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

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

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

    
958
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
959
            
960
            this.set({stats: stats}, {silent:true});
961
            this.trigger("stats:update", stats);
962
        },
963

    
964
        unbind: function() {
965
            models.VM.__super__.unbind.apply(this, arguments);
966
        },
967

    
968
        handle_stats_error: function() {
969
            stats = {};
970
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
971
                stats[k] = false;
972
            });
973

    
974
            this.set({'stats': stats});
975
        },
976

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

    
988
                function check_images_loaded() {
989
                    images_loaded++;
990

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

    
1011
                    img.error(function() {
1012
                        images[stat + type] = false;
1013
                        check_images_loaded();
1014
                    });
1015

    
1016
                    img.attr({'src': stats[k]});
1017
                })
1018
                data.stats = stats;
1019
            }
1020

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

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

    
1042
        require_reboot: function() {
1043
            if (this.is_active()) {
1044
                this.set({'reboot_required': true});
1045
            }
1046
        },
1047
        
1048
        set_pending_action: function(data) {
1049
            this.pending_action = data;
1050
            return data;
1051
        },
1052

    
1053
        // machine has pending action
1054
        update_pending_action: function(action, force) {
1055
            this.set({pending_action: action});
1056
        },
1057

    
1058
        clear_pending_action: function() {
1059
            this.set({pending_action: undefined});
1060
        },
1061

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

    
1080
        // user can connect to machine
1081
        is_connectable: function() {
1082
            // check if ips exist
1083
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
1084
                return false;
1085
            }
1086
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1087
        },
1088
        
1089
        remove_meta: function(key, complete, error) {
1090
            var url = this.api_path() + "/meta/" + key;
1091
            this.api_call(url, "delete", undefined, complete, error);
1092
        },
1093

    
1094
        save_meta: function(meta, complete, error) {
1095
            var url = this.api_path() + "/meta/" + meta.key;
1096
            var payload = {meta:{}};
1097
            payload.meta[meta.key] = meta.value;
1098
            payload._options = {
1099
                critical:false, 
1100
                error_params: {
1101
                    title: "Machine metadata error",
1102
                    extra_details: {"Machine id": this.id}
1103
            }};
1104

    
1105
            this.api_call(url, "update", payload, complete, error);
1106
        },
1107

    
1108

    
1109
        // update/get the state of the machine
1110
        state: function() {
1111
            var args = slice.call(arguments);
1112
                
1113
            // TODO: it might not be a good idea to set the state in set_state method
1114
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1115
                this.set({'state': args[0]});
1116
            }
1117

    
1118
            return this.get('state');
1119
        },
1120
        
1121
        // get the state that the api status corresponds to
1122
        state_for_api_status: function(status) {
1123
            return this.state_transition(this.state(), status);
1124
        },
1125
        
1126
        // vm state equals vm api status
1127
        state_is_status: function(state) {
1128
            return models.VM.STATUSES.indexOf(state) != -1;
1129
        },
1130
        
1131
        // get transition state for the corresponging api status
1132
        state_transition: function(state, new_status) {
1133
            var statuses = models.VM.STATES_TRANSITIONS[state];
1134
            if (statuses) {
1135
                if (statuses.indexOf(new_status) > -1) {
1136
                    return new_status;
1137
                } else {
1138
                    return state;
1139
                }
1140
            } else {
1141
                return new_status;
1142
            }
1143
        },
1144
        
1145
        // the current vm state is a transition state
1146
        in_transition: function() {
1147
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
1148
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
1149
        },
1150
        
1151
        // get image object
1152
        get_image: function(callback) {
1153
            var image = storage.images.get(this.get('imageRef'));
1154
            if (!image) {
1155
                storage.images.update_unknown_id(this.get('imageRef'), callback);
1156
                return;
1157
            }
1158
            callback(image);
1159
            return image;
1160
        },
1161
        
1162
        // get flavor object
1163
        get_flavor: function() {
1164
            var flv = storage.flavors.get(this.get('flavorRef'));
1165
            if (!flv) {
1166
                storage.flavors.update_unknown_id(this.get('flavorRef'));
1167
                flv = storage.flavors.get(this.get('flavorRef'));
1168
            }
1169
            return flv;
1170
        },
1171

    
1172
        // retrieve the metadata object
1173
        get_meta: function(key) {
1174
            try {
1175
                return _.escape(this.get('metadata').values[key]);
1176
            } catch (err) {
1177
                return {};
1178
            }
1179
        },
1180
        
1181
        // get metadata OS value
1182
        get_os: function() {
1183
            return this.get_meta('OS') || (this.get_image(function(){}) ? 
1184
                                          this.get_image(function(){}).get_os() || "okeanos" : "okeanos");
1185
        },
1186

    
1187
        get_gui: function() {
1188
            return this.get_meta('GUI');
1189
        },
1190
        
1191
        connected_to: function(net) {
1192
            return this.get('linked_to').indexOf(net.id) > -1;
1193
        },
1194

    
1195
        connected_with_nic_id: function(nic_id) {
1196
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
1197
        },
1198

    
1199
        get_nics: function(filter) {
1200
            ret = synnefo.storage.nics.filter(function(nic) {
1201
                return parseInt(nic.get('vm_id')) == this.id;
1202
            }, this);
1203

    
1204
            if (filter) {
1205
                return _.filter(ret, filter);
1206
            }
1207

    
1208
            return ret;
1209
        },
1210

    
1211
        get_net_nics: function(net_id) {
1212
            return this.get_nics(function(n){return n.get('network_id') == net_id});
1213
        },
1214

    
1215
        get_public_nic: function() {
1216
            return this.get_nics(function(n){ return n.get_network().is_public() })[0];
1217
        },
1218

    
1219
        get_nic: function(net_id) {
1220
        },
1221

    
1222
        has_firewall: function() {
1223
            var nic = this.get_public_nic();
1224
            if (nic) {
1225
                var profile = nic.get('firewallProfile'); 
1226
                return ['ENABLED', 'PROTECTED'].indexOf(profile) > -1;
1227
            }
1228
            return false;
1229
        },
1230

    
1231
        get_firewall_profile: function() {
1232
            var nic = this.get_public_nic();
1233
            if (nic) {
1234
                return nic.get('firewallProfile');
1235
            }
1236
            return null;
1237
        },
1238

    
1239
        get_addresses: function() {
1240
            var pnic = this.get_public_nic();
1241
            if (!pnic) { return {'ip4': undefined, 'ip6': undefined }};
1242
            return {'ip4': pnic.get('ipv4'), 'ip6': pnic.get('ipv6')};
1243
        },
1244
    
1245
        // get actions that the user can execute
1246
        // depending on the vm state/status
1247
        get_available_actions: function() {
1248
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1249
        },
1250

    
1251
        set_profile: function(profile, net_id) {
1252
        },
1253
        
1254
        // call rename api
1255
        rename: function(new_name) {
1256
            //this.set({'name': new_name});
1257
            this.sync("update", this, {
1258
                critical: true,
1259
                data: {
1260
                    'server': {
1261
                        'name': new_name
1262
                    }
1263
                }, 
1264
                // do the rename after the method succeeds
1265
                success: _.bind(function(){
1266
                    //this.set({name: new_name});
1267
                    snf.api.trigger("call");
1268
                }, this)
1269
            });
1270
        },
1271
        
1272
        get_console_url: function(data) {
1273
            var url_params = {
1274
                machine: this.get("name"),
1275
                host_ip: this.get_addresses().ip4,
1276
                host_ip_v6: this.get_addresses().ip6,
1277
                host: data.host,
1278
                port: data.port,
1279
                password: data.password
1280
            }
1281
            return '/machines/console?' + $.param(url_params);
1282
        },
1283

    
1284
        // action helper
1285
        call: function(action_name, success, error, params) {
1286
            var id_param = [this.id];
1287
            
1288
            params = params || {};
1289
            success = success || function() {};
1290
            error = error || function() {};
1291

    
1292
            var self = this;
1293

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

    
1359
            var params = {
1360
                url: url,
1361
                data: data,
1362
                success: function(){ self.handle_action_succeed.apply(self, arguments); success.apply(this, arguments)},
1363
                error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)},
1364
                error_params: { ns: "Machines actions", 
1365
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1366
                                extra_details: { 'Machine ID': this.id, 'URL': url, 'Action': action || "undefined" },
1367
                                allow_reload: false
1368
                              },
1369
                display: false,
1370
                critical: false
1371
            }
1372
            _.extend(params, extra_params)
1373
            this.sync(method, this, params);
1374
        },
1375

    
1376
        handle_action_succeed: function() {
1377
            this.trigger("action:success", arguments);
1378
        },
1379
        
1380
        reset_action_error: function() {
1381
            this.action_error = false;
1382
            this.trigger("action:fail:reset", this.action_error);
1383
        },
1384

    
1385
        handle_action_fail: function() {
1386
            this.action_error = arguments;
1387
            this.trigger("action:fail", arguments);
1388
        },
1389

    
1390
        get_action_url: function(name) {
1391
            return this.url() + "/action";
1392
        },
1393

    
1394
        get_diagnostics_url: function() {
1395
            return this.url() + "/diagnostics";
1396
        },
1397

    
1398
        get_connection_info: function(host_os, success, error) {
1399
            var url = "/machines/connect";
1400
            params = {
1401
                ip_address: this.get_addresses().ip4,
1402
                os: this.get_os(),
1403
                host_os: host_os,
1404
                srv: this.id
1405
            }
1406

    
1407
            url = url + "?" + $.param(params);
1408

    
1409
            var ajax = snf.api.sync("read", undefined, { url: url, 
1410
                                                         error:error, 
1411
                                                         success:success, 
1412
                                                         handles_error:1});
1413
        }
1414
    })
1415
    
1416
    models.VM.ACTIONS = [
1417
        'start',
1418
        'shutdown',
1419
        'reboot',
1420
        'console',
1421
        'destroy'
1422
    ]
1423

    
1424
    models.VM.AVAILABLE_ACTIONS = {
1425
        'UNKNWON'       : ['destroy'],
1426
        'BUILD'         : ['destroy'],
1427
        'REBOOT'        : ['shutdown', 'destroy', 'console'],
1428
        'STOPPED'       : ['start', 'destroy'],
1429
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1430
        'ERROR'         : ['destroy'],
1431
        'DELETED'        : [],
1432
        'DESTROY'       : [],
1433
        'BUILD_INIT'    : ['destroy'],
1434
        'BUILD_COPY'    : ['destroy'],
1435
        'BUILD_FINAL'   : ['destroy'],
1436
        'SHUTDOWN'      : ['destroy'],
1437
        'START'         : [],
1438
        'CONNECT'       : [],
1439
        'DISCONNECT'    : []
1440
    }
1441

    
1442
    // api status values
1443
    models.VM.STATUSES = [
1444
        'UNKNWON',
1445
        'BUILD',
1446
        'REBOOT',
1447
        'STOPPED',
1448
        'ACTIVE',
1449
        'ERROR',
1450
        'DELETED'
1451
    ]
1452

    
1453
    // api status values
1454
    models.VM.CONNECT_STATES = [
1455
        'ACTIVE',
1456
        'REBOOT',
1457
        'SHUTDOWN'
1458
    ]
1459

    
1460
    // vm states
1461
    models.VM.STATES = models.VM.STATUSES.concat([
1462
        'DESTROY',
1463
        'BUILD_INIT',
1464
        'BUILD_COPY',
1465
        'BUILD_FINAL',
1466
        'SHUTDOWN',
1467
        'START',
1468
        'CONNECT',
1469
        'DISCONNECT',
1470
        'FIREWALL'
1471
    ]);
1472
    
1473
    models.VM.STATES_TRANSITIONS = {
1474
        'DESTROY' : ['DELETED'],
1475
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1476
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1477
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1478
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1479
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1480
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1481
        'BUILD_COPY': ['ERROR', 'ACTIVE', 'BUILD_FINAL', 'DESTROY'],
1482
        'BUILD_FINAL': ['ERROR', 'ACTIVE', 'DESTROY'],
1483
        'BUILD_INIT': ['ERROR', 'ACTIVE', 'BUILD_COPY', 'BUILD_FINAL', 'DESTROY']
1484
    }
1485

    
1486
    models.VM.TRANSITION_STATES = [
1487
        'DESTROY',
1488
        'SHUTDOWN',
1489
        'START',
1490
        'REBOOT',
1491
        'BUILD'
1492
    ]
1493

    
1494
    models.VM.ACTIVE_STATES = [
1495
        'BUILD', 'REBOOT', 'ACTIVE',
1496
        'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL',
1497
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1498
    ]
1499

    
1500
    models.VM.BUILDING_STATES = [
1501
        'BUILD', 'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL'
1502
    ]
1503

    
1504
    models.Networks = models.Collection.extend({
1505
        model: models.Network,
1506
        path: 'networks',
1507
        details: true,
1508
        //noUpdate: true,
1509
        defaults: {'nics':[],'linked_to':[]},
1510
        
1511
        parse: function (resp, xhr) {
1512
            // FIXME: depricated global var
1513
            if (!resp) { return []};
1514
               
1515
            var data = _.map(resp.networks.values, _.bind(this.parse_net_api_data, this));
1516
            return data;
1517
        },
1518

    
1519
        add: function() {
1520
            ret = models.Networks.__super__.add.apply(this, arguments);
1521
            // update nics after each network addition
1522
            ret.each(function(r){
1523
                synnefo.storage.nics.update_net_nics(r);
1524
            });
1525
        },
1526

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

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

    
1539
        parse_net_api_data: function(data) {
1540
            // append nic metadata
1541
            // net.get('nics') contains a list of vm/index objects 
1542
            // e.g. {'vm_id':12231, 'index':1}
1543
            // net.get('linked_to') contains a list of vms the network is 
1544
            // connected to e.g. [1001, 1002]
1545
            if (data.attachments && data.attachments.values) {
1546
                data['nics'] = {};
1547
                data['linked_to'] = [];
1548
                _.each(data.attachments.values, function(nic_id){
1549
                  
1550
                  var vm_id = NIC_REGEX.exec(nic_id)[1];
1551
                  var nic_index = parseInt(NIC_REGEX.exec(nic_id)[2]);
1552

    
1553
                  if (vm_id !== undefined && nic_index !== undefined) {
1554
                      data['nics'][nic_id] = {
1555
                          'vm_id': vm_id, 
1556
                          'index': nic_index, 
1557
                          'id': nic_id
1558
                      };
1559
                      if (data['linked_to'].indexOf(vm_id) == -1) {
1560
                        data['linked_to'].push(vm_id);
1561
                      }
1562
                  }
1563
                });
1564
            }
1565
            return data;
1566
        },
1567

    
1568
        create: function (name, type, cidr, dhcp, callback) {
1569
            var params = {
1570
                network:{
1571
                    name:name
1572
                }
1573
            };
1574

    
1575
            if (type) {
1576
                params.network.type = type;
1577
            }
1578
            if (cidr) {
1579
                params.network.cidr = cidr;
1580
            }
1581
            if (dhcp) {
1582
                params.network.dhcp = dhcp;
1583
            }
1584

    
1585
            if (dhcp === false) {
1586
                params.network.dhcp = false;
1587
            }
1588
            
1589
            return this.api_call(this.path, "create", params, callback);
1590
        }
1591
    })
1592

    
1593
    models.Images = models.Collection.extend({
1594
        model: models.Image,
1595
        path: 'images',
1596
        details: true,
1597
        noUpdate: true,
1598
        supportIncUpdates: false,
1599
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1600
        read_method: 'read',
1601

    
1602
        // update collection model with id passed
1603
        // making a direct call to the image
1604
        // api url
1605
        update_unknown_id: function(id, callback) {
1606
            var url = getUrl.call(this) + "/" + id;
1607
            this.api_call(this.path + "/" + id, this.read_method, {
1608
              _options:{
1609
                async:true, 
1610
                skip_api_error:true}
1611
              }, undefined, 
1612
            _.bind(function() {
1613
                if (!this.get(id)) {
1614
                            if (this.fallback_service) {
1615
                        // if current service has fallback_service attribute set
1616
                        // use this service to retrieve the missing image model
1617
                        var tmpservice = new this.fallback_service();
1618
                        tmpservice.update_unknown_id(id, _.bind(function(img){
1619
                            img.attributes.status = "DELETED";
1620
                            this.add(img.attributes);
1621
                            callback(this.get(id));
1622
                        }, this));
1623
                    } else {
1624
                        var title = synnefo.config.image_deleted_title || 'Deleted';
1625
                        // else add a dummy DELETED state image entry
1626
                        this.add({id:id, name:title, size:-1, 
1627
                                  progress:100, status:"DELETED"});
1628
                        callback(this.get(id));
1629
                    }   
1630
                } else {
1631
                    callback(this.get(id));
1632
                }
1633
            }, this), _.bind(function(image, msg, xhr) {
1634
                if (!image) {
1635
                    var title = synnefo.config.image_deleted_title || 'Deleted';
1636
                    this.add({id:id, name:title, size:-1, 
1637
                              progress:100, status:"DELETED"});
1638
                    callback(this.get(id));
1639
                    return;
1640
                }
1641
                var img_data = this._read_image_from_request(image, msg, xhr);
1642
                this.add(img_data);
1643
                callback(this.get(id));
1644
            }, this));
1645
        },
1646

    
1647
        _read_image_from_request: function(image, msg, xhr) {
1648
            return image.image;
1649
        },
1650

    
1651
        parse: function (resp, xhr) {
1652
            // FIXME: depricated global var
1653
            var data = _.map(resp.images.values, _.bind(this.parse_meta, this));
1654
            return resp.images.values;
1655
        },
1656

    
1657
        get_meta_key: function(img, key) {
1658
            if (img.metadata && img.metadata.values && img.metadata.values[key]) {
1659
                return _.escape(img.metadata.values[key]);
1660
            }
1661
            return undefined;
1662
        },
1663

    
1664
        comparator: function(img) {
1665
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1666
        },
1667

    
1668
        parse_meta: function(img) {
1669
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1670
                if (img[key]) { return };
1671
                img[key] = this.get_meta_key(img, key) || "";
1672
            }, this));
1673
            return img;
1674
        },
1675

    
1676
        active: function() {
1677
            return this.filter(function(img){return img.get('status') != "DELETED"});
1678
        },
1679

    
1680
        predefined: function() {
1681
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
1682
        },
1683
        
1684
        fetch_for_type: function(type, complete, error) {
1685
            this.fetch({update:true, 
1686
                        success: complete, 
1687
                        error: error, 
1688
                        skip_api_error: true });
1689
        },
1690
        
1691
        get_images_for_type: function(type) {
1692
            if (this['get_{0}_images'.format(type)]) {
1693
                return this['get_{0}_images'.format(type)]();
1694
            }
1695

    
1696
            return this.active();
1697
        },
1698

    
1699
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
1700
            var load = false;
1701
            error = onError || function() {};
1702
            function complete(collection) { 
1703
                onComplete(collection.get_images_for_type(type)); 
1704
            }
1705
            
1706
            // do we need to fetch/update current collection entries
1707
            if (load) {
1708
                onStart();
1709
                this.fetch_for_type(type, complete, error);
1710
            } else {
1711
                // fallback to complete
1712
                complete(this);
1713
            }
1714
        }
1715
    })
1716

    
1717
    models.Flavors = models.Collection.extend({
1718
        model: models.Flavor,
1719
        path: 'flavors',
1720
        details: true,
1721
        noUpdate: true,
1722
        supportIncUpdates: false,
1723
        // update collection model with id passed
1724
        // making a direct call to the flavor
1725
        // api url
1726
        update_unknown_id: function(id, callback) {
1727
            var url = getUrl.call(this) + "/" + id;
1728
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
1729
            _.bind(function() {
1730
                this.add({id:id, cpu:"", ram:"", disk:"", name: "", status:"DELETED"})
1731
            }, this), _.bind(function(flv) {
1732
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1733
                this.add(flv.flavor);
1734
            }, this));
1735
        },
1736

    
1737
        parse: function (resp, xhr) {
1738
            // FIXME: depricated global var
1739
            return _.map(resp.flavors.values, function(o) { o.disk_template = o['SNF:disk_template']; return o});
1740
        },
1741

    
1742
        comparator: function(flv) {
1743
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1744
        },
1745

    
1746
        unavailable_values_for_image: function(img, flavors) {
1747
            var flavors = flavors || this.active();
1748
            var size = img.get_size();
1749
            
1750
            var index = {cpu:[], disk:[], ram:[]};
1751

    
1752
            _.each(this.active(), function(el) {
1753
                var img_size = size;
1754
                var flv_size = el.get_disk_size();
1755
                if (flv_size < img_size) {
1756
                    if (index.disk.indexOf(flv_size) == -1) {
1757
                        index.disk.push(flv_size);
1758
                    }
1759
                };
1760
            });
1761
            
1762
            return index;
1763
        },
1764

    
1765
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
1766
            if (!filter_list) { filter_list = this.models };
1767
            
1768
            return this.select(function(flv){
1769
                if (flv.get("cpu") == cpu + "" &&
1770
                   flv.get("ram") == mem + "" &&
1771
                   flv.get("disk") == disk + "" &&
1772
                   flv.get("disk_template") == disk_template &&
1773
                   filter_list.indexOf(flv) > -1) { return true; }
1774
            })[0];
1775
        },
1776
        
1777
        get_data: function(lst) {
1778
            var data = {'cpu': [], 'mem':[], 'disk':[]};
1779

    
1780
            _.each(lst, function(flv) {
1781
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1782
                    data.cpu.push(flv.get("cpu"));
1783
                }
1784
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1785
                    data.mem.push(flv.get("ram"));
1786
                }
1787
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1788
                    data.disk.push(flv.get("disk"));
1789
                }
1790
            })
1791
            
1792
            return data;
1793
        },
1794

    
1795
        active: function() {
1796
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1797
        }
1798
            
1799
    })
1800

    
1801
    models.VMS = models.Collection.extend({
1802
        model: models.VM,
1803
        path: 'servers',
1804
        details: true,
1805
        copy_image_meta: true,
1806

    
1807
        parse: function (resp, xhr) {
1808
            // FIXME: depricated after refactoring
1809
            var data = resp;
1810
            if (!resp) { return [] };
1811
            data = _.filter(_.map(resp.servers.values, _.bind(this.parse_vm_api_data, this)), function(v){return v});
1812
            return data;
1813
        },
1814

    
1815
        parse_vm_api_data: function(data) {
1816
            // do not add non existing DELETED entries
1817
            if (data.status && data.status == "DELETED") {
1818
                if (!this.get(data.id)) {
1819
                    return false;
1820
                }
1821
            }
1822

    
1823
            // OS attribute
1824
            if (this.has_meta(data)) {
1825
                data['OS'] = data.metadata.values.OS || "okeanos";
1826
            }
1827
            
1828
            if (!data.diagnostics) {
1829
                data.diagnostics = [];
1830
            }
1831

    
1832
            // network metadata
1833
            data['firewalls'] = {};
1834
            data['nics'] = {};
1835
            data['linked_to'] = [];
1836

    
1837
            if (data['attachments'] && data['attachments'].values) {
1838
                var nics = data['attachments'].values;
1839
                _.each(nics, function(nic) {
1840
                    var net_id = nic.network_id;
1841
                    var index = parseInt(NIC_REGEX.exec(nic.id)[2]);
1842
                    if (data['linked_to'].indexOf(net_id) == -1) {
1843
                        data['linked_to'].push(net_id);
1844
                    }
1845

    
1846
                    data['nics'][nic.id] = nic;
1847
                })
1848
            }
1849
            
1850
            // if vm has no metadata, no metadata object
1851
            // is in json response, reset it to force
1852
            // value update
1853
            if (!data['metadata']) {
1854
                data['metadata'] = {values:{}};
1855
            }
1856

    
1857
            return data;
1858
        },
1859

    
1860
        add: function() {
1861
            ret = models.VMS.__super__.add.apply(this, arguments);
1862
            ret.each(function(r){
1863
                synnefo.storage.nics.update_vm_nics(r);
1864
            });
1865
        },
1866
        
1867
        get_reboot_required: function() {
1868
            return this.filter(function(vm){return vm.get("reboot_required") == true})
1869
        },
1870

    
1871
        has_pending_actions: function() {
1872
            return this.filter(function(vm){return vm.pending_action}).length > 0;
1873
        },
1874

    
1875
        reset_pending_actions: function() {
1876
            this.each(function(vm) {
1877
                vm.clear_pending_action();
1878
            })
1879
        },
1880

    
1881
        do_all_pending_actions: function(success, error) {
1882
            this.each(function(vm) {
1883
                if (vm.has_pending_action()) {
1884
                    vm.call(vm.pending_action, success, error);
1885
                    vm.clear_pending_action();
1886
                }
1887
            })
1888
        },
1889
        
1890
        do_all_reboots: function(success, error) {
1891
            this.each(function(vm) {
1892
                if (vm.get("reboot_required")) {
1893
                    vm.call("reboot", success, error);
1894
                }
1895
            });
1896
        },
1897

    
1898
        reset_reboot_required: function() {
1899
            this.each(function(vm) {
1900
                vm.set({'reboot_required': undefined});
1901
            })
1902
        },
1903
        
1904
        stop_stats_update: function(exclude) {
1905
            var exclude = exclude || [];
1906
            this.each(function(vm) {
1907
                if (exclude.indexOf(vm) > -1) {
1908
                    return;
1909
                }
1910
                vm.stop_stats_update();
1911
            })
1912
        },
1913
        
1914
        has_meta: function(vm_data) {
1915
            return vm_data.metadata && vm_data.metadata.values
1916
        },
1917

    
1918
        has_addresses: function(vm_data) {
1919
            return vm_data.metadata && vm_data.metadata.values
1920
        },
1921

    
1922
        create: function (name, image, flavor, meta, extra, callback) {
1923
            if (this.copy_image_meta) {
1924
                if (image.get("OS")) {
1925
                    meta['OS'] = image.get("OS");
1926
                }
1927
           }
1928
            
1929
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta}
1930
            opts = _.extend(opts, extra);
1931

    
1932
            this.api_call(this.path, "create", {'server': opts}, undefined, undefined, callback, {critical: true});
1933
        }
1934

    
1935
    })
1936
    
1937
    models.NIC = models.Model.extend({
1938
        
1939
        initialize: function() {
1940
            models.NIC.__super__.initialize.apply(this, arguments);
1941
            this.pending_for_firewall = false;
1942
            this.bind("change:firewallProfile", _.bind(this.check_firewall, this));
1943
            this.bind("change:pending_firewall", function(nic) {
1944
                nic.get_network().update_state();
1945
            });
1946
            this.get_vm().bind("remove", function(){
1947
              try {
1948
                this.collection.remove(this);
1949
              } catch (err) {};
1950
            }, this);
1951
            this.get_network().bind("remove", function(){
1952
                try {
1953
                    this.collection.remove(this);
1954
                } catch (err) {};
1955
            }, this);
1956
        },
1957

    
1958
        get_vm: function() {
1959
            return synnefo.storage.vms.get(parseInt(this.get('vm_id')));
1960
        },
1961

    
1962
        get_network: function() {
1963
            return synnefo.storage.networks.get(this.get('network_id'));
1964
        },
1965

    
1966
        get_v6_address: function() {
1967
            return this.get("ipv6");
1968
        },
1969

    
1970
        get_v4_address: function() {
1971
            return this.get("ipv4");
1972
        },
1973

    
1974
        set_firewall: function(value, callback, error, options) {
1975
            var net_id = this.get('network_id');
1976
            var self = this;
1977

    
1978
            // api call data
1979
            var payload = {"firewallProfile":{"profile":value}};
1980
            payload._options = _.extend({critical: false}, options);
1981
            
1982
            this.set({'pending_firewall': value});
1983
            this.set({'pending_firewall_sending': true});
1984

    
1985
            var success_cb = function() {
1986
                if (callback) {
1987
                    callback();
1988
                }
1989
                self.set({'pending_firewall_sending': false});
1990
            };
1991

    
1992
            var error_cb = function() {
1993
                self.reset_pending_firewall();
1994
            }
1995
            
1996
            this.get_vm().api_call(this.get_vm().api_path() + "/action", "create", payload, success_cb, error_cb);
1997
        },
1998

    
1999
        reset_pending_firewall: function() {
2000
            this.set({'pending_firewall': false});
2001
        },
2002

    
2003
        check_firewall: function() {
2004
            var firewall = this.get('firewallProfile');
2005
            var pending = this.get('pending_firewall');
2006
            this.reset_pending_firewall();
2007
        }
2008
        
2009
    });
2010

    
2011
    models.NICs = models.Collection.extend({
2012
        model: models.NIC,
2013
        
2014
        add_or_update: function(nic_id, data, vm) {
2015
            var params = _.clone(data);
2016
            params.attachment_id = params.id;
2017
            params.id = params.id + '-' + params.network_id;
2018
            params.vm_id = parseInt(NIC_REGEX.exec(nic_id)[1]);
2019

    
2020
            if (!this.get(params.id)) {
2021
                this.add(params);
2022
                var nic = this.get(params.id);
2023
                nic.get_network().decrease_connecting();
2024
                nic.bind("remove", function() {
2025
                    nic.set({"removing": 0});
2026
                    if (this.get_network()) {
2027
                        // network might got removed before nic
2028
                        nic.get_network().update_state();
2029
                    }
2030
                })
2031
            } else {
2032
                this.get(params.id).set(params);
2033
            }
2034
        },
2035
        
2036
        reset_nics: function(nics, filter_attr, filter_val) {
2037
            var nics_to_check = this.filter(function(nic) {
2038
                return nic.get(filter_attr) == filter_val;
2039
            });
2040
            
2041
            _.each(nics_to_check, function(nic) {
2042
                if (nics.indexOf(nic.get('id')) == -1) {
2043
                    this.remove(nic);
2044
                } else {
2045
                }
2046
            }, this);
2047
        },
2048

    
2049
        update_vm_nics: function(vm) {
2050
            var nics = vm.get('nics');
2051
            this.reset_nics(_.map(nics, function(nic, key){
2052
                return key + "-" + nic.network_id;
2053
            }), 'vm_id', vm.id);
2054

    
2055
            _.each(nics, function(val, key) {
2056
                var net = synnefo.storage.networks.get(val.network_id);
2057
                if (net && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
2058
                    this.add_or_update(key, vm.get('nics')[key], vm);
2059
                }
2060
            }, this);
2061
        },
2062

    
2063
        update_net_nics: function(net) {
2064
            var nics = net.get('nics');
2065
            this.reset_nics(_.map(nics, function(nic, key){
2066
                return key + "-" + net.get('id');
2067
            }), 'network_id', net.id);
2068

    
2069
            _.each(nics, function(val, key) {
2070
                var vm = synnefo.storage.vms.get(val.vm_id);
2071
                if (vm && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
2072
                    this.add_or_update(key, vm.get('nics')[key], vm);
2073
                }
2074
            }, this);
2075
        }
2076
    });
2077

    
2078
    models.PublicKey = models.Model.extend({
2079
        path: 'keys',
2080
        base_url: '/ui/userdata',
2081
        details: false,
2082
        noUpdate: true,
2083

    
2084

    
2085
        get_public_key: function() {
2086
            return cryptico.publicKeyFromString(this.get("content"));
2087
        },
2088

    
2089
        get_filename: function() {
2090
            return "{0}.pub".format(this.get("name"));
2091
        },
2092

    
2093
        identify_type: function() {
2094
            try {
2095
                var cont = snf.util.validatePublicKey(this.get("content"));
2096
                var type = cont.split(" ")[0];
2097
                return synnefo.util.publicKeyTypesMap[type];
2098
            } catch (err) { return false };
2099
        }
2100

    
2101
    })
2102
    
2103
    models.PublicKeys = models.Collection.extend({
2104
        model: models.PublicKey,
2105
        details: false,
2106
        path: 'keys',
2107
        base_url: '/ui/userdata',
2108
        noUpdate: true,
2109

    
2110
        generate_new: function(success, error) {
2111
            snf.api.sync('create', undefined, {
2112
                url: getUrl.call(this, this.base_url) + "/generate", 
2113
                success: success, 
2114
                error: error,
2115
                skip_api_error: true
2116
            });
2117
        },
2118

    
2119
        add_crypto_key: function(key, success, error, options) {
2120
            var options = options || {};
2121
            var m = new models.PublicKey();
2122

    
2123
            // guess a name
2124
            var name_tpl = "my generated public key";
2125
            var name = name_tpl;
2126
            var name_count = 1;
2127
            
2128
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
2129
                name = name_tpl + " " + name_count;
2130
                name_count++;
2131
            }
2132
            
2133
            m.set({name: name});
2134
            m.set({content: key});
2135
            
2136
            options.success = function () { return success(m) };
2137
            options.errror = error;
2138
            options.skip_api_error = true;
2139
            
2140
            this.create(m.attributes, options);
2141
        }
2142
    })
2143
    
2144
    // storage initialization
2145
    snf.storage.images = new models.Images();
2146
    snf.storage.flavors = new models.Flavors();
2147
    snf.storage.networks = new models.Networks();
2148
    snf.storage.vms = new models.VMS();
2149
    snf.storage.keys = new models.PublicKeys();
2150
    snf.storage.nics = new models.NICs();
2151

    
2152
    //snf.storage.vms.fetch({update:true});
2153
    //snf.storage.images.fetch({update:true});
2154
    //snf.storage.flavors.fetch({update:true});
2155

    
2156
})(this);