Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / ui / static / snf / js / models.js @ 6be6f0b6

History | View | Annotate | Download (100.7 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
        var append = "/";
58
        if (baseurl.split("").reverse()[0] == "/") {
59
          append = "";
60
        }
61
        return baseurl + append + this.path;
62
    }
63

    
64
    var NIC_REGEX = /^nic-([0-9]+)-([0-9]+)$/
65
    
66
    // i18n
67
    BUILDING_MESSAGES = window.BUILDING_MESSAGES || {'INIT': 'init', 'COPY': '{0}, {1}, {2}', 'FINAL': 'final'};
68

    
69
    // Base object for all our models
70
    models.Model = bb.Model.extend({
71
        sync: snf.api.sync,
72
        api: snf.api,
73
        api_type: 'compute',
74
        has_status: false,
75
        auto_bind: [],
76

    
77

    
78
        initialize: function() {
79
            var self = this;
80
            
81
            this._proxy_model_cache = {};
82
            _.each(this.auto_bind, function(fname) {
83
              self[fname] = _.bind(self[fname], self);
84
            });
85

    
86
            if (this.has_status) {
87
                this.bind("change:status", this.handle_remove);
88
                this.handle_remove();
89
            }
90
            
91
            this.api_call = _.bind(this.api.call, this);
92
              
93
            if (this.proxy_attrs) {
94
              this.init_proxy_attrs();             
95
            }
96

    
97
            if (this.storage_attrs) {
98
              this.init_storage_attrs();
99
            }
100

    
101
            if (this.model_actions) {
102
              this.init_model_actions();             
103
            }
104

    
105
            models.Model.__super__.initialize.apply(this, arguments);
106

    
107
        },
108
        
109
        // Initialize model actions object
110
        // For each entry in model's model_action object register the relevant 
111
        // model proxy `can_<actionname>` attributes.
112
        init_model_actions: function() {
113
          var actions = _.keys(this.model_actions);
114
          this.set({
115
            "actions": new models._ActionsModel({}, {
116
              actions: actions,
117
              model: this
118
            })
119
          });
120
          this.actions = this.get("actions");
121

    
122
          _.each(this.model_actions, function(params, key){
123
            var attr = 'can_' + key;
124
            if (params.length == 0) { return }
125
            var deps = params[0];
126
            var cb = _.bind(params[1], this);
127
            _.each(deps, function(dep) {
128
              this._set_proxy_attr(attr, dep, cb);
129
            }, this);
130
          }, this);
131
        },
132
        
133
        // Initialize proxy storage model attributes. These attribues allows 
134
        // us to automatically access cross collection associated objects.
135
        init_storage_attrs: function() {
136
          _.each(this.storage_attrs, function(params, attr) {
137
            var store, key, attr_name;
138
            store = synnefo.storage[params[0]];
139
            key = params[1];
140
            attr_name = attr;
141
          
142
            var resolve_related_instance = function(storage, attr_name, val) {
143
              var data = {};
144

    
145
              if (!val) { 
146
                // update with undefined and return
147
                data[key] = undefined;
148
                this.set(data);
149
                return;
150
              };
151
            
152
              // retrieve related object (check if its a Model??)
153
              var obj = store.get(val);
154
              
155
              if (obj) {
156
                // set related object
157
                data[attr_name] = obj;
158
                this.set(data, {silent:true})
159
                this.trigger("change:" + attr_name, obj);
160
              } else {
161
                var self = this;
162
                var retry = window.setInterval(function(){
163
                  var obj = store.get(val);
164
                  if (obj) {
165
                    data[key] = obj;
166
                    self.set(data, {silent:true})
167
                    self.trigger("change:" + attr_name, obj);
168
                    clearInterval(retry);
169
                  }
170
                }, 10);
171
              }
172
            }
173

    
174
            this.bind('change:' + attr, function() {
175
              resolve_related_instance.call(this, store, key, this.get(attr))
176
            });
177

    
178
            this.bind('add', function() {
179
              resolve_related_instance.call(this, store, key, this.get(attr))
180
            });
181
          }, this);
182
        },
183
        
184
        _proxy_model_cache: {},
185
        
186
        _set_proxy_attr: function(attr, check_attr, cb) {
187
          // initial set
188
          var data = {};
189
          data[attr] = cb.call(this, this.get(check_attr));
190
          if (data[attr] !== undefined) {
191
            this.set(data, {silent:true});
192
          }
193
          
194
          this.bind('change:' + check_attr, function() {
195
            if (this.get(check_attr) instanceof models.Model) {
196
              var model = this.get(check_attr);
197
              var proxy_cache_key = attr + '_' + check_attr;
198
              if (this._proxy_model_cache[proxy_cache_key]) {
199
                var proxy = this._proxy_model_cache[proxy_cache_key];
200
                proxy[0].unbind('change', proxy[1]);
201
              }
202
              var changebind = _.bind(function() {
203
                var data = {};
204
                data[attr] = cb.call(this, this.get(check_attr));
205
                this.set(data);
206
              }, this);
207
              model.bind('change', changebind);
208
              this._proxy_model_cache[proxy_cache_key] = [model, changebind];
209
            }
210
            var val = cb.call(this, this.get(check_attr));
211
            var data = {};
212
            if (this.get(attr) !== val) {
213
              data[attr] = val;
214
              this.set(data);
215
            }
216
          }, this);
217
        },
218

    
219
        init_proxy_attrs: function() {
220
          _.each(this.proxy_attrs, function(opts, attr){
221
            var cb = opts[1];
222
            _.each(opts[0], function(check_attr){
223
              this._set_proxy_attr(attr, check_attr, cb)
224
            }, this);
225
          }, this);
226
        },
227
        
228
        handle_remove: function() {
229
            if (this.get("status") == 'DELETED') {
230
                if (this.collection) {
231
                    try { this.clear_pending_action();} catch (err) {};
232
                    try { this.reset_pending_actions();} catch (err) {};
233
                    try { this.stop_stats_update();} catch (err) {};
234
                    this.collection.remove(this.id);
235
                }
236
            }
237
        },
238
        
239
        // custom set method to allow submodels to use
240
        // set_<attr> methods for handling the value of each
241
        // attribute and overriding the default set method
242
        // for specific parameters
243
        set: function(params, options) {
244
            _.each(params, _.bind(function(value, key){
245
                if (this["set_" + key]) {
246
                    params[key] = this["set_" + key](value);
247
                }
248
            }, this))
249
            var ret = bb.Model.prototype.set.call(this, params, options);
250
            return ret;
251
        },
252

    
253
        url: function(options) {
254
            return getUrl.call(this, this.base_url) + "/" + this.id;
255
        },
256

    
257
        api_path: function(options) {
258
            return this.path + "/" + this.id;
259
        },
260

    
261
        parse: function(resp, xhr) {
262
        },
263

    
264
        remove: function(complete, error, success) {
265
            this.api_call(this.api_path(), "delete", undefined, complete, error, success);
266
        },
267

    
268
        changedKeys: function() {
269
            return _.keys(this.changedAttributes() || {});
270
        },
271
            
272
        // return list of changed attributes that included in passed list
273
        // argument
274
        getKeysChanged: function(keys) {
275
            return _.intersection(keys, this.changedKeys());
276
        },
277
        
278
        // boolean check of keys changed
279
        keysChanged: function(keys) {
280
            return this.getKeysChanged(keys).length > 0;
281
        },
282

    
283
        // check if any of the passed attribues has changed
284
        hasOnlyChange: function(keys) {
285
            var ret = false;
286
            _.each(keys, _.bind(function(key) {
287
                if (this.changedKeys().length == 1 && this.changedKeys().indexOf(key) > -1) { ret = true};
288
            }, this));
289
            return ret;
290
        }
291

    
292
    })
293
    
294
    // Base object for all our model collections
295
    models.Collection = bb.Collection.extend({
296
        sync: snf.api.sync,
297
        api: snf.api,
298
        api_type: 'compute',
299
        supportIncUpdates: true,
300

    
301
        initialize: function() {
302
            models.Collection.__super__.initialize.apply(this, arguments);
303
            this.api_call = _.bind(this.api.call, this);
304
        },
305

    
306
        url: function(options, method) {
307
            return getUrl.call(this, this.base_url) + (
308
                    options.details || this.details && method != 'create' ? '/detail' : '');
309
        },
310

    
311
        fetch: function(options) {
312
            if (!options) { options = {} };
313
            // default to update
314
            if (!this.noUpdate) {
315
                if (options.update === undefined) { options.update = true };
316
                if (!options.removeMissing && options.refresh) { 
317
                  options.removeMissing = true;
318
                };
319
                // for collections which associated models don't support 
320
                // deleted state identification through attributes, resolve  
321
                // deleted entries by checking for missing objects in fetch 
322
                // responses.
323
                if (this.updateEntries && options.removeMissing === undefined) {
324
                  options.removeMissing = true;
325
                }
326
            } else {
327
                if (options.refresh === undefined) {
328
                    options.refresh = true;
329
                    if (this.updateEntries) {
330
                      options.update = true;
331
                      options.removeMissing = true;
332
                    }
333
                }
334
            }
335
            // custom event foreach fetch
336
            return bb.Collection.prototype.fetch.call(this, options)
337
        },
338

    
339
        create: function(model, options) {
340
            var coll = this;
341
            options || (options = {});
342
            model = this._prepareModel(model, options);
343
            if (!model) return false;
344
            var success = options.success;
345
            options.success = function(nextModel, resp, xhr) {
346
                if (success) success(nextModel, resp, xhr);
347
            };
348
            model.save(null, options);
349
            return model;
350
        },
351

    
352
        get_fetcher: function(interval, increase, fast, increase_after_calls, max, initial_call, params) {
353
            var fetch_params = params || {};
354
            var handler_options = {};
355

    
356
            fetch_params.skips_timeouts = true;
357
            handler_options.interval = interval;
358
            handler_options.increase = increase;
359
            handler_options.fast = fast;
360
            handler_options.increase_after_calls = increase_after_calls;
361
            handler_options.max= max;
362
            handler_options.id = "collection id";
363

    
364
            var last_ajax = undefined;
365
            var callback = _.bind(function() {
366
                // clone to avoid referenced objects
367
                var params = _.clone(fetch_params);
368
                updater._ajax = last_ajax;
369
                
370
                // wait for previous request to finish
371
                if (last_ajax && last_ajax.readyState < 4 && last_ajax.statusText != "timeout") {
372
                    // opera readystate for 304 responses is 0
373
                    if (!($.browser.opera && last_ajax.readyState == 0 && last_ajax.status == 304)) {
374
                        return;
375
                    }
376
                }
377
                last_ajax = this.fetch(params);
378
            }, this);
379
            handler_options.callback = callback;
380

    
381
            var updater = new snf.api.updateHandler(_.clone(_.extend(handler_options, fetch_params)));
382
            snf.api.bind("call", _.throttle(_.bind(function(){ updater.faster(true)}, this)), 1000);
383
            return updater;
384
        }
385
    });
386
    
387
    // Image model
388
    models.Image = models.Model.extend({
389
        path: 'images',
390
        
391
        get_size: function() {
392
            return parseInt(this.get('metadata') ? this.get('metadata').size : -1)
393
        },
394

    
395
        get_description: function(escape) {
396
            if (escape == undefined) { escape = true };
397
            if (escape) { return this.escape('description') || "No description available"}
398
            return this.get('description') || "No description available."
399
        },
400

    
401
        get_meta: function(key) {
402
            if (this.get('metadata') && this.get('metadata')) {
403
                if (!this.get('metadata')[key]) { return null }
404
                return _.escape(this.get('metadata')[key]);
405
            } else {
406
                return null;
407
            }
408
        },
409

    
410
        get_meta_keys: function() {
411
            if (this.get('metadata') && this.get('metadata')) {
412
                return _.keys(this.get('metadata'));
413
            } else {
414
                return [];
415
            }
416
        },
417

    
418
        get_owner: function() {
419
            return this.get('owner') || _.keys(synnefo.config.system_images_owners)[0];
420
        },
421

    
422
        get_owner_uuid: function() {
423
            return this.get('owner_uuid');
424
        },
425

    
426
        is_system_image: function() {
427
          var owner = this.get_owner();
428
          return _.include(_.keys(synnefo.config.system_images_owners), owner)
429
        },
430

    
431
        owned_by: function(user) {
432
          if (!user) { user = synnefo.user }
433
          return user.get_username() == this.get('owner_uuid');
434
        },
435

    
436
        display_owner: function() {
437
            var owner = this.get_owner();
438
            if (_.include(_.keys(synnefo.config.system_images_owners), owner)) {
439
                return synnefo.config.system_images_owners[owner];
440
            } else {
441
                return owner;
442
            }
443
        },
444
    
445
        get_readable_size: function() {
446
            if (this.is_deleted()) {
447
                return synnefo.config.image_deleted_size_title || '(none)';
448
            }
449
            return this.get_size() > 0 ? util.readablizeBytes(this.get_size() * 1024 * 1024) : '(none)';
450
        },
451

    
452
        get_os: function() {
453
            return this.get_meta('OS');
454
        },
455

    
456
        get_gui: function() {
457
            return this.get_meta('GUI');
458
        },
459

    
460
        get_created_users: function() {
461
            try {
462
              var users = this.get_meta('users').split(" ");
463
            } catch (err) { users = null }
464
            if (!users) {
465
                var osfamily = this.get_meta('osfamily');
466
                if (osfamily == 'windows') { 
467
                  users = ['Administrator'];
468
                } else {
469
                  users = ['root'];
470
                }
471
            }
472
            return users;
473
        },
474

    
475
        get_sort_order: function() {
476
            return parseInt(this.get('metadata') ? this.get('metadata').sortorder : -1)
477
        },
478

    
479
        get_vm: function() {
480
            var vm_id = this.get("serverRef");
481
            var vm = undefined;
482
            vm = storage.vms.get(vm_id);
483
            return vm;
484
        },
485

    
486
        is_public: function() {
487
            return this.get('is_public') == undefined ? true : this.get('is_public');
488
        },
489

    
490
        is_deleted: function() {
491
            return this.get('status') == "DELETED"
492
        },
493
        
494
        ssh_keys_paths: function() {
495
            return _.map(this.get_created_users(), function(username) {
496
                prepend = '';
497
                if (username != 'root') {
498
                    prepend = '/home'
499
                }
500
                return {'user': username, 'path': '{1}/{0}/.ssh/authorized_keys'.format(username, 
501
                                                             prepend)};
502
            });
503
        },
504

    
505
        _supports_ssh: function() {
506
            if (synnefo.config.support_ssh_os_list.indexOf(this.get_os()) > -1) {
507
                return true;
508
            }
509
            if (this.get_meta('osfamily') == 'linux') {
510
              return true;
511
            }
512
            return false;
513
        },
514

    
515
        supports: function(feature) {
516
            if (feature == "ssh") {
517
                return this._supports_ssh()
518
            }
519
            return false;
520
        },
521

    
522
        personality_data_for_keys: function(keys) {
523
            return _.map(this.ssh_keys_paths(), function(pathinfo) {
524
                var contents = '';
525
                _.each(keys, function(key){
526
                    contents = contents + key.get("content") + "\n"
527
                });
528
                contents = $.base64.encode(contents);
529

    
530
                return {
531
                    path: pathinfo.path,
532
                    contents: contents,
533
                    mode: 0600,
534
                    owner: pathinfo.user
535
                }
536
            });
537
        }
538
    });
539

    
540
    // Flavor model
541
    models.Flavor = models.Model.extend({
542
        path: 'flavors',
543

    
544
        details_string: function() {
545
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
546
        },
547

    
548
        get_disk_size: function() {
549
            return parseInt(this.get("disk") * 1000)
550
        },
551

    
552
        get_ram_size: function() {
553
            return parseInt(this.get("ram"))
554
        },
555

    
556
        get_disk_template_info: function() {
557
            var info = snf.config.flavors_disk_templates_info[this.get("disk_template")];
558
            if (!info) {
559
                info = { name: this.get("disk_template"), description:'' };
560
            }
561
            return info
562
        },
563

    
564
        disk_to_bytes: function() {
565
            return parseInt(this.get("disk")) * 1024 * 1024 * 1024;
566
        },
567

    
568
        ram_to_bytes: function() {
569
            return parseInt(this.get("ram")) * 1024 * 1024;
570
        },
571

    
572
    });
573
    
574
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
575
    _.extend(models.ParamsList.prototype, bb.Events, {
576

    
577
        initialize: function(parent, param_name) {
578
            this.parent = parent;
579
            this.actions = {};
580
            this.param_name = param_name;
581
            this.length = 0;
582
        },
583
        
584
        has_action: function(action) {
585
            return this.actions[action] ? true : false;
586
        },
587
            
588
        _parse_params: function(arguments) {
589
            if (arguments.length <= 1) {
590
                return [];
591
            }
592

    
593
            var args = _.toArray(arguments);
594
            return args.splice(1);
595
        },
596

    
597
        contains: function(action, params) {
598
            params = this._parse_params(arguments);
599
            var has_action = this.has_action(action);
600
            if (!has_action) { return false };
601

    
602
            var paramsEqual = false;
603
            _.each(this.actions[action], function(action_params) {
604
                if (_.isEqual(action_params, params)) {
605
                    paramsEqual = true;
606
                }
607
            });
608
                
609
            return paramsEqual;
610
        },
611
        
612
        is_empty: function() {
613
            return _.isEmpty(this.actions);
614
        },
615

    
616
        add: function(action, params) {
617
            params = this._parse_params(arguments);
618
            if (this.contains.apply(this, arguments)) { return this };
619
            var isnew = false
620
            if (!this.has_action(action)) {
621
                this.actions[action] = [];
622
                isnew = true;
623
            };
624

    
625
            this.actions[action].push(params);
626
            this.parent.trigger("change:" + this.param_name, this.parent, this);
627
            if (isnew) {
628
                this.trigger("add", action, params);
629
            } else {
630
                this.trigger("change", action, params);
631
            }
632
            return this;
633
        },
634
        
635
        remove_all: function(action) {
636
            if (this.has_action(action)) {
637
                delete this.actions[action];
638
                this.parent.trigger("change:" + this.param_name, this.parent, this);
639
                this.trigger("remove", action);
640
            }
641
            return this;
642
        },
643

    
644
        reset: function() {
645
            this.actions = {};
646
            this.parent.trigger("change:" + this.param_name, this.parent, this);
647
            this.trigger("reset");
648
            this.trigger("remove");
649
        },
650

    
651
        remove: function(action, params) {
652
            params = this._parse_params(arguments);
653
            if (!this.has_action(action)) { return this };
654
            var index = -1;
655
            _.each(this.actions[action], _.bind(function(action_params) {
656
                if (_.isEqual(action_params, params)) {
657
                    index = this.actions[action].indexOf(action_params);
658
                }
659
            }, this));
660
            
661
            if (index > -1) {
662
                this.actions[action].splice(index, 1);
663
                if (_.isEmpty(this.actions[action])) {
664
                    delete this.actions[action];
665
                }
666
                this.parent.trigger("change:" + this.param_name, this.parent, this);
667
                this.trigger("remove", action, params);
668
            }
669
        }
670

    
671
    });
672

    
673
    // Image model
674
    models.Network = models.Model.extend({
675
        path: 'networks',
676
        has_status: true,
677
        defaults: {'connecting':0},
678
        
679
        initialize: function() {
680
            var ret = models.Network.__super__.initialize.apply(this, arguments);
681
            this.set({"actions": new models.ParamsList(this, "actions")});
682
            this.update_state();
683
            this.bind("change:nics", _.bind(synnefo.storage.nics.update_net_nics, synnefo.storage.nics));
684
            this.bind("change:status", _.bind(this.update_state, this));
685
            return ret;
686
        },
687
        
688
        is_deleted: function() {
689
          return this.get('status') == 'DELETED';
690
        },
691

    
692
        toJSON: function() {
693
            var attrs = _.clone(this.attributes);
694
            attrs.actions = _.clone(this.get("actions").actions);
695
            return attrs;
696
        },
697
        
698
        set_state: function(val) {
699
            if (val == "PENDING" && this.get("state") == "DESTORY") {
700
                return "DESTROY";
701
            }
702
            return val;
703
        },
704

    
705
        update_state: function() {
706
            if (this.get("connecting") > 0) {
707
                this.set({state: "CONNECTING"});
708
                return
709
            }
710
            
711
            if (this.get_nics(function(nic){ return nic.get("removing") == 1}).length > 0) {
712
                this.set({state: "DISCONNECTING"});
713
                return
714
            }   
715
            
716
            if (this.contains_firewalling_nics() > 0) {
717
                this.set({state: "FIREWALLING"});
718
                return
719
            }   
720
            
721
            if (this.get("state") == "DESTROY") { 
722
                this.set({"destroyed":1});
723
            }
724
            
725
            this.set({state:this.get('status')});
726
        },
727

    
728
        is_public: function() {
729
            return this.get("public");
730
        },
731

    
732
        decrease_connecting: function() {
733
            var conn = this.get("connecting");
734
            if (!conn) { conn = 0 };
735
            if (conn > 0) {
736
                conn--;
737
            }
738
            this.set({"connecting": conn});
739
            this.update_state();
740
        },
741

    
742
        increase_connecting: function() {
743
            var conn = this.get("connecting");
744
            if (!conn) { conn = 0 };
745
            conn++;
746
            this.set({"connecting": conn});
747
            this.update_state();
748
        },
749

    
750
        connected_to: function(vm) {
751
            return this.get('linked_to').indexOf(""+vm.id) > -1;
752
        },
753

    
754
        connected_with_nic_id: function(nic_id) {
755
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
756
        },
757

    
758
        get_nics: function(filter) {
759
            var nics = synnefo.storage.nics.filter(function(nic) {
760
                return nic.get('network_id') == this.id;
761
            }, this);
762

    
763
            if (filter) {
764
                return _.filter(nics, filter);
765
            }
766
            return nics;
767
        },
768

    
769
        contains_firewalling_nics: function() {
770
            return this.get_nics(function(n){return n.get('pending_firewall')}).length
771
        },
772

    
773
        call: function(action, params, success, error) {
774
            if (action == "destroy") {
775
                var previous_state = this.get('state');
776
                var previous_status = this.get('status');
777

    
778
                this.set({state:"DESTROY"});
779

    
780
                var _success = _.bind(function() {
781
                    if (success) { success() };
782
                    synnefo.storage.quotas.get('cyclades.network.private').decrease();
783
                }, this);
784
                var _error = _.bind(function() {
785
                    this.set({state: previous_state, status: previous_status})
786
                    if (error) { error() };
787
                }, this);
788

    
789
                this.remove(undefined, _error, _success);
790
            }
791
            
792
            if (action == "disconnect") {
793
                if (this.get("state") == "DESTROY") {
794
                    return;
795
                }
796
                
797
                _.each(params, _.bind(function(nic_id) {
798
                    var nic = snf.storage.nics.get(nic_id);
799
                    this.get("actions").remove("disconnect", nic_id);
800
                    if (nic) {
801
                        this.remove_nic(nic, success, error);
802
                    }
803
                }, this));
804
            }
805
        },
806

    
807
        add_vm: function (vm, callback, error, options) {
808
            var payload = {add:{serverRef:"" + vm.id}};
809
            payload._options = options || {};
810
            return this.api_call(this.api_path() + "/action", "create", 
811
                                 payload,
812
                                 undefined,
813
                                 error,
814
                                 _.bind(function(){
815
                                     //this.vms.add_pending(vm.id);
816
                                     this.increase_connecting();
817
                                     if (callback) {callback()}
818
                                 },this), error);
819
        },
820

    
821
        remove_nic: function (nic, callback, error, options) {
822
            var payload = {remove:{attachment:"" + nic.get("attachment_id")}};
823
            payload._options = options || {};
824
            return this.api_call(this.api_path() + "/action", "create", 
825
                                 payload,
826
                                 undefined,
827
                                 error,
828
                                 _.bind(function(){
829
                                     nic.set({"removing": 1});
830
                                     nic.get_network().update_state();
831
                                     //this.vms.add_pending_for_remove(vm.id);
832
                                     if (callback) {callback()}
833
                                 },this), error);
834
        },
835

    
836
        rename: function(name, callback) {
837
            return this.api_call(this.api_path(), "update", {
838
                network:{name:name}, 
839
                _options:{
840
                    critical: false, 
841
                    error_params:{
842
                        title: "Network action failed",
843
                        ns: "Networks",
844
                        extra_details: {"Network id": this.id}
845
                    }
846
                }}, callback);
847
        },
848

    
849
        get_connectable_vms: function() {
850
            return storage.vms.filter(function(vm){
851
                return !vm.in_error_state() && !vm.is_building() && !vm.is_rebooting();
852
            });
853
        },
854

    
855
        state_message: function() {
856
            if (this.get("state") == "ACTIVE" && !this.is_public()) {
857
                if (this.get("cidr") && this.get("dhcp") == true) {
858
                    return this.get("cidr");
859
                } else {
860
                    return "Private network";
861
                }
862
            }
863
            if (this.get("state") == "ACTIVE" && this.is_public()) {
864
                  return "Public network";
865
            }
866

    
867
            return models.Network.STATES[this.get("state")];
868
        },
869

    
870
        in_progress: function() {
871
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
872
        },
873

    
874
        do_all_pending_actions: function(success, error) {
875
          var params, actions, action_params;
876
          actions = _.clone(this.get("actions").actions);
877
            _.each(actions, _.bind(function(params, action) {
878
                action_params = _.map(actions[action], function(a){ return _.clone(a)});
879
                _.each(action_params, _.bind(function(params) {
880
                    this.call(action, params, success, error);
881
                }, this));
882
            }, this));
883
            this.get("actions").reset();
884
        }
885
    });
886
    
887
    models.Network.STATES = {
888
        'ACTIVE': 'Private network',
889
        'CONNECTING': 'Connecting...',
890
        'DISCONNECTING': 'Disconnecting...',
891
        'FIREWALLING': 'Firewall update...',
892
        'DESTROY': 'Destroying...',
893
        'PENDING': 'Pending...',
894
        'ERROR': 'Error'
895
    }
896

    
897
    models.Network.STATES_TRANSITIONS = {
898
        'CONNECTING': ['ACTIVE'],
899
        'DISCONNECTING': ['ACTIVE'],
900
        'PENDING': ['ACTIVE'],
901
        'FIREWALLING': ['ACTIVE']
902
    }
903

    
904
    // Virtualmachine model
905
    models.VM = models.Model.extend({
906

    
907
        path: 'servers',
908
        has_status: true,
909
        initialize: function(params) {
910
            
911
            this.pending_firewalls = {};
912
            
913
            models.VM.__super__.initialize.apply(this, arguments);
914

    
915
            this.set({state: params.status || "ERROR"});
916
            this.log = new snf.logging.logger("VM " + this.id);
917
            this.pending_action = undefined;
918
            
919
            // init stats parameter
920
            this.set({'stats': undefined}, {silent: true});
921
            // defaults to not update the stats
922
            // each view should handle this vm attribute 
923
            // depending on if it displays stat images or not
924
            this.do_update_stats = false;
925
            
926
            // interval time
927
            // this will dynamicaly change if the server responds that
928
            // images get refreshed on different intervals
929
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
930
            this.stats_available = false;
931

    
932
            // initialize interval
933
            this.init_stats_intervals(this.stats_update_interval);
934
            
935
            // handle progress message on instance change
936
            this.bind("change", _.bind(this.update_status_message, this));
937
            this.bind("change:task_state", _.bind(this.update_status, this));
938
            // force update of progress message
939
            this.update_status_message(true);
940
            
941
            // default values
942
            this.bind("change:state", _.bind(function(){
943
                if (this.state() == "DESTROY") { 
944
                    this.handle_destroy() 
945
                }
946
            }, this));
947

    
948
            this.bind("change:nics", _.bind(synnefo.storage.nics.update_vm_nics, synnefo.storage.nics));
949
        },
950

    
951
        status: function(st) {
952
            if (!st) { return this.get("status")}
953
            return this.set({status:st});
954
        },
955
        
956
        update_status: function() {
957
            this.set_status(this.get('status'));
958
        },
959

    
960
        set_status: function(st) {
961
            var new_state = this.state_for_api_status(st);
962
            var transition = false;
963

    
964
            if (this.state() != new_state) {
965
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
966
                    transition = this.state();
967
                }
968
            }
969
            
970
            // call it silently to avoid double change trigger
971
            var state = this.state_for_api_status(st);
972
            this.set({'state': state}, {silent: true});
973
            
974
            // trigger transition
975
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
976
                this.trigger("transition", {from:transition, to:new_state}) 
977
            };
978
            return st;
979
        },
980
            
981
        get_diagnostics: function(success) {
982
            this.__make_api_call(this.get_diagnostics_url(),
983
                                 "read", // create so that sync later uses POST to make the call
984
                                 null, // payload
985
                                 function(data) {
986
                                     success(data);
987
                                 },  
988
                                 null, 'diagnostics');
989
        },
990

    
991
        has_diagnostics: function() {
992
            return this.get("diagnostics") && this.get("diagnostics").length;
993
        },
994

    
995
        get_progress_info: function() {
996
            // details about progress message
997
            // contains a list of diagnostic messages
998
            return this.get("status_messages");
999
        },
1000

    
1001
        get_status_message: function() {
1002
            return this.get('status_message');
1003
        },
1004
        
1005
        // extract status message from diagnostics
1006
        status_message_from_diagnostics: function(diagnostics) {
1007
            var valid_sources_map = synnefo.config.diagnostics_status_messages_map;
1008
            var valid_sources = valid_sources_map[this.get('status')];
1009
            if (!valid_sources) { return null };
1010
            
1011
            // filter messsages based on diagnostic source
1012
            var messages = _.filter(diagnostics, function(diag) {
1013
                return valid_sources.indexOf(diag.source) > -1;
1014
            });
1015

    
1016
            var msg = messages[0];
1017
            if (msg) {
1018
              var message = msg.message;
1019
              var message_tpl = snf.config.diagnostic_messages_tpls[msg.source];
1020

    
1021
              if (message_tpl) {
1022
                  message = message_tpl.replace('MESSAGE', msg.message);
1023
              }
1024
              return message;
1025
            }
1026
            
1027
            // no message to display, but vm in build state, display
1028
            // finalizing message.
1029
            if (this.is_building() == 'BUILD') {
1030
                return synnefo.config.BUILDING_MESSAGES['FINAL'];
1031
            }
1032
            return null;
1033
        },
1034

    
1035
        update_status_message: function(force) {
1036
            // update only if one of the specified attributes has changed
1037
            if (
1038
              !this.keysChanged(['diagnostics', 'progress', 'status', 'state'])
1039
                && !force
1040
            ) { return };
1041
            
1042
            // if user requested to destroy the vm set the appropriate 
1043
            // message.
1044
            if (this.get('state') == "DESTROY") { 
1045
                message = "Terminating..."
1046
                this.set({status_message: message})
1047
                return;
1048
            }
1049
            
1050
            // set error message, if vm has diagnostic message display it as
1051
            // progress message
1052
            if (this.in_error_state()) {
1053
                var d = this.get('diagnostics');
1054
                if (d && d.length) {
1055
                    var message = this.status_message_from_diagnostics(d);
1056
                    this.set({status_message: message});
1057
                } else {
1058
                    this.set({status_message: null});
1059
                }
1060
                return;
1061
            }
1062
            
1063
            // identify building status message
1064
            if (this.is_building()) {
1065
                var self = this;
1066
                var success = function(msg) {
1067
                    self.set({status_message: msg});
1068
                }
1069
                this.get_building_status_message(success);
1070
                return;
1071
            }
1072

    
1073
            this.set({status_message:null});
1074
        },
1075
            
1076
        // get building status message. Asynchronous function since it requires
1077
        // access to vm image.
1078
        get_building_status_message: function(callback) {
1079
            // no progress is set, vm is in initial build status
1080
            var progress = this.get("progress");
1081
            if (progress == 0 || !progress) {
1082
                return callback(BUILDING_MESSAGES['INIT']);
1083
            }
1084
            
1085
            // vm has copy progress, display copy percentage
1086
            if (progress > 0 && progress <= 99) {
1087
                this.get_copy_details(true, undefined, _.bind(
1088
                    function(details){
1089
                        callback(BUILDING_MESSAGES['COPY'].format(details.copy, 
1090
                                                           details.size, 
1091
                                                           details.progress));
1092
                }, this));
1093
                return;
1094
            }
1095

    
1096
            // copy finished display FINAL message or identify status message
1097
            // from diagnostics.
1098
            if (progress >= 100) {
1099
                if (!this.has_diagnostics()) {
1100
                        callback(BUILDING_MESSAGES['FINAL']);
1101
                } else {
1102
                        var d = this.get("diagnostics");
1103
                        var msg = this.status_message_from_diagnostics(d);
1104
                        if (msg) {
1105
                              callback(msg);
1106
                        }
1107
                }
1108
            }
1109
        },
1110

    
1111
        get_copy_details: function(human, image, callback) {
1112
            var human = human || false;
1113
            var image = image || this.get_image(_.bind(function(image){
1114
                var progress = this.get('progress');
1115
                var size = image.get_size();
1116
                var size_copied = (size * progress / 100).toFixed(2);
1117
                
1118
                if (human) {
1119
                    size = util.readablizeBytes(size*1024*1024);
1120
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
1121
                }
1122

    
1123
                callback({'progress': progress, 'size': size, 'copy': size_copied})
1124
            }, this));
1125
        },
1126

    
1127
        start_stats_update: function(force_if_empty) {
1128
            var prev_state = this.do_update_stats;
1129

    
1130
            this.do_update_stats = true;
1131
            
1132
            // fetcher initialized ??
1133
            if (!this.stats_fetcher) {
1134
                this.init_stats_intervals();
1135
            }
1136

    
1137

    
1138
            // fetcher running ???
1139
            if (!this.stats_fetcher.running || !prev_state) {
1140
                this.stats_fetcher.start();
1141
            }
1142

    
1143
            if (force_if_empty && this.get("stats") == undefined) {
1144
                this.update_stats(true);
1145
            }
1146
        },
1147

    
1148
        stop_stats_update: function(stop_calls) {
1149
            this.do_update_stats = false;
1150

    
1151
            if (stop_calls) {
1152
                this.stats_fetcher.stop();
1153
            }
1154
        },
1155

    
1156
        // clear and reinitialize update interval
1157
        init_stats_intervals: function (interval) {
1158
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
1159
            this.stats_fetcher.start();
1160
        },
1161
        
1162
        get_stats_fetcher: function(timeout) {
1163
            var cb = _.bind(function(data){
1164
                this.update_stats();
1165
            }, this);
1166
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
1167
            return fetcher;
1168
        },
1169

    
1170
        // do the api call
1171
        update_stats: function(force) {
1172
            // do not update stats if flag not set
1173
            if ((!this.do_update_stats && !force) || this.updating_stats) {
1174
                return;
1175
            }
1176

    
1177
            // make the api call, execute handle_stats_update on sucess
1178
            // TODO: onError handler ???
1179
            stats_url = this.url() + "/stats";
1180
            this.updating_stats = true;
1181
            this.sync("read", this, {
1182
                handles_error:true, 
1183
                url: stats_url, 
1184
                refresh:true, 
1185
                success: _.bind(this.handle_stats_update, this),
1186
                error: _.bind(this.handle_stats_error, this),
1187
                complete: _.bind(function(){this.updating_stats = false;}, this),
1188
                critical: false,
1189
                log_error: false,
1190
                skips_timeouts: true
1191
            });
1192
        },
1193

    
1194
        get_stats_image: function(stat, type) {
1195
        },
1196
        
1197
        _set_stats: function(stats) {
1198
            var silent = silent === undefined ? false : silent;
1199
            // unavailable stats while building
1200
            if (this.get("status") == "BUILD") { 
1201
                this.stats_available = false;
1202
            } else { this.stats_available = true; }
1203

    
1204
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
1205
            
1206
            this.set({stats: stats}, {silent:true});
1207
            this.trigger("stats:update", stats);
1208
        },
1209

    
1210
        unbind: function() {
1211
            models.VM.__super__.unbind.apply(this, arguments);
1212
        },
1213

    
1214
        can_resize: function() {
1215
          return this.get('status') == 'STOPPED';
1216
        },
1217

    
1218
        handle_stats_error: function() {
1219
            stats = {};
1220
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1221
                stats[k] = false;
1222
            });
1223

    
1224
            this.set({'stats': stats});
1225
        },
1226

    
1227
        // this method gets executed after a successful vm stats api call
1228
        handle_stats_update: function(data) {
1229
            var self = this;
1230
            // avoid browser caching
1231
            
1232
            if (data.stats && _.size(data.stats) > 0) {
1233
                var ts = $.now();
1234
                var stats = data.stats;
1235
                var images_loaded = 0;
1236
                var images = {};
1237

    
1238
                function check_images_loaded() {
1239
                    images_loaded++;
1240

    
1241
                    if (images_loaded == 4) {
1242
                        self._set_stats(images);
1243
                    }
1244
                }
1245
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1246
                    
1247
                    stats[k] = stats[k] + "?_=" + ts;
1248
                    
1249
                    var stat = k.slice(0,3);
1250
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
1251
                    var img = $("<img />");
1252
                    var val = stats[k];
1253
                    
1254
                    // load stat image to a temporary dom element
1255
                    // update model stats on image load/error events
1256
                    img.load(function() {
1257
                        images[k] = val;
1258
                        check_images_loaded();
1259
                    });
1260

    
1261
                    img.error(function() {
1262
                        images[stat + type] = false;
1263
                        check_images_loaded();
1264
                    });
1265

    
1266
                    img.attr({'src': stats[k]});
1267
                })
1268
                data.stats = stats;
1269
            }
1270

    
1271
            // do we need to change the interval ??
1272
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
1273
                this.stats_update_interval = data.stats.refresh * 1000;
1274
                this.stats_fetcher.interval = this.stats_update_interval;
1275
                this.stats_fetcher.maximum_interval = this.stats_update_interval;
1276
                this.stats_fetcher.stop();
1277
                this.stats_fetcher.start(false);
1278
            }
1279
        },
1280

    
1281
        // helper method that sets the do_update_stats
1282
        // in the future this method could also make an api call
1283
        // immediaetly if needed
1284
        enable_stats_update: function() {
1285
            this.do_update_stats = true;
1286
        },
1287
        
1288
        handle_destroy: function() {
1289
            this.stats_fetcher.stop();
1290
        },
1291

    
1292
        require_reboot: function() {
1293
            if (this.is_active()) {
1294
                this.set({'reboot_required': true});
1295
            }
1296
        },
1297
        
1298
        set_pending_action: function(data) {
1299
            this.pending_action = data;
1300
            return data;
1301
        },
1302

    
1303
        // machine has pending action
1304
        update_pending_action: function(action, force) {
1305
            this.set({pending_action: action});
1306
        },
1307

    
1308
        clear_pending_action: function() {
1309
            this.set({pending_action: undefined});
1310
        },
1311

    
1312
        has_pending_action: function() {
1313
            return this.get("pending_action") ? this.get("pending_action") : false;
1314
        },
1315
        
1316
        // machine is active
1317
        is_active: function() {
1318
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
1319
        },
1320
        
1321
        // machine is building 
1322
        is_building: function() {
1323
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
1324
        },
1325
        
1326
        is_rebooting: function() {
1327
            return this.state() == 'REBOOT';
1328
        },
1329

    
1330
        in_error_state: function() {
1331
            return this.state() === "ERROR"
1332
        },
1333

    
1334
        // user can connect to machine
1335
        is_connectable: function() {
1336
            // check if ips exist
1337
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
1338
                return false;
1339
            }
1340
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1341
        },
1342
        
1343
        remove_meta: function(key, complete, error) {
1344
            var url = this.api_path() + "/metadata/" + key;
1345
            this.api_call(url, "delete", undefined, complete, error);
1346
        },
1347

    
1348
        save_meta: function(meta, complete, error) {
1349
            var url = this.api_path() + "/metadata/" + meta.key;
1350
            var payload = {meta:{}};
1351
            payload.meta[meta.key] = meta.value;
1352
            payload._options = {
1353
                critical:false, 
1354
                error_params: {
1355
                    title: "Machine metadata error",
1356
                    extra_details: {"Machine id": this.id}
1357
            }};
1358

    
1359
            this.api_call(url, "update", payload, complete, error);
1360
        },
1361

    
1362

    
1363
        // update/get the state of the machine
1364
        state: function() {
1365
            var args = slice.call(arguments);
1366
                
1367
            // TODO: it might not be a good idea to set the state in set_state method
1368
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1369
                this.set({'state': args[0]});
1370
            }
1371

    
1372
            return this.get('state');
1373
        },
1374
        
1375
        // get the state that the api status corresponds to
1376
        state_for_api_status: function(status) {
1377
            return this.state_transition(this.state(), status);
1378
        },
1379
        
1380
        // get transition state for the corresponging api status
1381
        state_transition: function(state, new_status) {
1382
            var statuses = models.VM.STATES_TRANSITIONS[state];
1383
            if (statuses) {
1384
                if (statuses.indexOf(new_status) > -1) {
1385
                    return new_status;
1386
                } else {
1387
                    return state;
1388
                }
1389
            } else {
1390
                return new_status;
1391
            }
1392
        },
1393
        
1394
        // the current vm state is a transition state
1395
        in_transition: function() {
1396
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
1397
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
1398
        },
1399
        
1400
        // get image object
1401
        get_image: function(callback) {
1402
            if (callback == undefined) { callback = function(){} }
1403
            var image = storage.images.get(this.get('image'));
1404
            if (!image) {
1405
                storage.images.update_unknown_id(this.get('image'), callback);
1406
                return;
1407
            }
1408
            callback(image);
1409
            return image;
1410
        },
1411
        
1412
        // get flavor object
1413
        get_flavor: function() {
1414
            var flv = storage.flavors.get(this.get('flavor'));
1415
            if (!flv) {
1416
                storage.flavors.update_unknown_id(this.get('flavor'));
1417
                flv = storage.flavors.get(this.get('flavor'));
1418
            }
1419
            return flv;
1420
        },
1421

    
1422
        get_resize_flavors: function() {
1423
          var vm_flavor = this.get_flavor();
1424
          var flavors = synnefo.storage.flavors.filter(function(f){
1425
              return f.get('disk_template') ==
1426
              vm_flavor.get('disk_template') && f.get('disk') ==
1427
              vm_flavor.get('disk');
1428
          });
1429
          return flavors;
1430
        },
1431

    
1432
        get_flavor_quotas: function() {
1433
          var flavor = this.get_flavor();
1434
          return {
1435
            cpu: flavor.get('cpu') + 1, 
1436
            ram: flavor.get_ram_size() + 1, 
1437
            disk:flavor.get_disk_size() + 1
1438
          }
1439
        },
1440

    
1441
        get_meta: function(key, deflt) {
1442
            if (this.get('metadata') && this.get('metadata')) {
1443
                if (!this.get('metadata')[key]) { return deflt }
1444
                return _.escape(this.get('metadata')[key]);
1445
            } else {
1446
                return deflt;
1447
            }
1448
        },
1449

    
1450
        get_meta_keys: function() {
1451
            if (this.get('metadata') && this.get('metadata')) {
1452
                return _.keys(this.get('metadata'));
1453
            } else {
1454
                return [];
1455
            }
1456
        },
1457
        
1458
        // get metadata OS value
1459
        get_os: function() {
1460
            var image = this.get_image();
1461
            return this.get_meta('OS') || (image ? 
1462
                                            image.get_os() || "okeanos" : "okeanos");
1463
        },
1464

    
1465
        get_gui: function() {
1466
            return this.get_meta('GUI');
1467
        },
1468
        
1469
        connected_to: function(net) {
1470
            return this.get('linked_to').indexOf(net.id) > -1;
1471
        },
1472

    
1473
        connected_with_nic_id: function(nic_id) {
1474
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
1475
        },
1476

    
1477
        get_nics: function(filter) {
1478
            ret = synnefo.storage.nics.filter(function(nic) {
1479
                return parseInt(nic.get('vm_id')) == this.id;
1480
            }, this);
1481

    
1482
            if (filter) {
1483
                return _.filter(ret, filter);
1484
            }
1485

    
1486
            return ret;
1487
        },
1488

    
1489
        get_net_nics: function(net_id) {
1490
            return this.get_nics(function(n){return n.get('network_id') == net_id});
1491
        },
1492

    
1493
        get_public_nic: function() {
1494
            return this.get_nics(function(n){ return n.get_network().is_public() === true })[0];
1495
        },
1496

    
1497
        get_hostname: function() {
1498
          var hostname = this.get_meta('hostname');
1499
          if (!hostname) {
1500
            if (synnefo.config.vm_hostname_format) {
1501
              hostname = synnefo.config.vm_hostname_format.format(this.id);
1502
            } else {
1503
              hostname = this.get_public_nic().get('ipv4');
1504
            }
1505
          }
1506
          return hostname;
1507
        },
1508

    
1509
        get_nic: function(net_id) {
1510
        },
1511

    
1512
        has_firewall: function() {
1513
            var nic = this.get_public_nic();
1514
            if (nic) {
1515
                var profile = nic.get('firewallProfile'); 
1516
                return ['ENABLED', 'PROTECTED'].indexOf(profile) > -1;
1517
            }
1518
            return false;
1519
        },
1520

    
1521
        get_firewall_profile: function() {
1522
            var nic = this.get_public_nic();
1523
            if (nic) {
1524
                return nic.get('firewallProfile');
1525
            }
1526
            return null;
1527
        },
1528

    
1529
        get_addresses: function() {
1530
            var pnic = this.get_public_nic();
1531
            if (!pnic) { return {'ip4': undefined, 'ip6': undefined }};
1532
            return {'ip4': pnic.get('ipv4'), 'ip6': pnic.get('ipv6')};
1533
        },
1534
    
1535
        // get actions that the user can execute
1536
        // depending on the vm state/status
1537
        get_available_actions: function() {
1538
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1539
        },
1540

    
1541
        set_profile: function(profile, net_id) {
1542
        },
1543
        
1544
        // call rename api
1545
        rename: function(new_name) {
1546
            //this.set({'name': new_name});
1547
            this.sync("update", this, {
1548
                critical: true,
1549
                data: {
1550
                    'server': {
1551
                        'name': new_name
1552
                    }
1553
                }, 
1554
                // do the rename after the method succeeds
1555
                success: _.bind(function(){
1556
                    //this.set({name: new_name});
1557
                    snf.api.trigger("call");
1558
                }, this)
1559
            });
1560
        },
1561
        
1562
        get_console_url: function(data) {
1563
            var url_params = {
1564
                machine: this.get("name"),
1565
                host_ip: this.get_addresses().ip4,
1566
                host_ip_v6: this.get_addresses().ip6,
1567
                host: data.host,
1568
                port: data.port,
1569
                password: data.password
1570
            }
1571
            return synnefo.config.ui_console_url + '?' + $.param(url_params);
1572
        },
1573

    
1574
        // action helper
1575
        call: function(action_name, success, error, params) {
1576
            var id_param = [this.id];
1577
            
1578
            params = params || {};
1579
            success = success || function() {};
1580
            error = error || function() {};
1581

    
1582
            var self = this;
1583

    
1584
            switch(action_name) {
1585
                case 'start':
1586
                    this.__make_api_call(this.get_action_url(), // vm actions url
1587
                                         "create", // create so that sync later uses POST to make the call
1588
                                         {start:{}}, // payload
1589
                                         function() {
1590
                                             // set state after successful call
1591
                                             self.state("START"); 
1592
                                             success.apply(this, arguments);
1593
                                             snf.api.trigger("call");
1594
                                         },  
1595
                                         error, 'start', params);
1596
                    break;
1597
                case 'reboot':
1598
                    this.__make_api_call(this.get_action_url(), // vm actions url
1599
                                         "create", // create so that sync later uses POST to make the call
1600
                                         {reboot:{}}, // payload
1601
                                         function() {
1602
                                             // set state after successful call
1603
                                             self.state("REBOOT"); 
1604
                                             success.apply(this, arguments)
1605
                                             snf.api.trigger("call");
1606
                                             self.set({'reboot_required': false});
1607
                                         },
1608
                                         error, 'reboot', params);
1609
                    break;
1610
                case 'shutdown':
1611
                    this.__make_api_call(this.get_action_url(), // vm actions url
1612
                                         "create", // create so that sync later uses POST to make the call
1613
                                         {shutdown:{}}, // payload
1614
                                         function() {
1615
                                             // set state after successful call
1616
                                             self.state("SHUTDOWN"); 
1617
                                             success.apply(this, arguments)
1618
                                             snf.api.trigger("call");
1619
                                         },  
1620
                                         error, 'shutdown', params);
1621
                    break;
1622
                case 'console':
1623
                    this.__make_api_call(this.url() + "/action", "create", 
1624
                                         {'console': {'type':'vnc'}}, 
1625
                                         function(data) {
1626
                        var cons_data = data.console;
1627
                        success.apply(this, [cons_data]);
1628
                    }, undefined, 'console', params)
1629
                    break;
1630
                case 'destroy':
1631
                    this.__make_api_call(this.url(), // vm actions url
1632
                                         "delete", // create so that sync later uses POST to make the call
1633
                                         undefined, // payload
1634
                                         function() {
1635
                                             // set state after successful call
1636
                                             self.state('DESTROY');
1637
                                             success.apply(this, arguments);
1638
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1639

    
1640
                                         },  
1641
                                         error, 'destroy', params);
1642
                    break;
1643
                case 'resize':
1644
                    this.__make_api_call(this.get_action_url(), // vm actions url
1645
                                         "create", // create so that sync later uses POST to make the call
1646
                                         {resize: {flavorRef:params.flavor}}, // payload
1647
                                         function() {
1648
                                             self.state('RESIZE');
1649
                                             success.apply(this, arguments);
1650
                                             snf.api.trigger("call");
1651
                                         },  
1652
                                         error, 'resize', params);
1653
                    break;
1654
                case 'addFloatingIp':
1655
                    this.__make_api_call(this.get_action_url(), // vm actions url
1656
                                         "create", // create so that sync later uses POST to make the call
1657
                                         {addFloatingIp: {address:params.address}}, // payload
1658
                                         function() {
1659
                                             self.state('CONNECT');
1660
                                             success.apply(this, arguments);
1661
                                             snf.api.trigger("call");
1662
                                         },  
1663
                                         error, 'addFloatingIp', params);
1664
                    break;
1665
                case 'removeFloatingIp':
1666
                    this.__make_api_call(this.get_action_url(), // vm actions url
1667
                                         "create", // create so that sync later uses POST to make the call
1668
                                         {removeFloatingIp: {address:params.address}}, // payload
1669
                                         function() {
1670
                                             self.state('DISCONNECT');
1671
                                             success.apply(this, arguments);
1672
                                             snf.api.trigger("call");
1673
                                         },  
1674
                                         error, 'addFloatingIp', params);
1675
                    break;
1676
                case 'destroy':
1677
                    this.__make_api_call(this.url(), // vm actions url
1678
                                         "delete", // create so that sync later uses POST to make the call
1679
                                         undefined, // payload
1680
                                         function() {
1681
                                             // set state after successful call
1682
                                             self.state('DESTROY');
1683
                                             success.apply(this, arguments);
1684
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1685

    
1686
                                         },  
1687
                                         error, 'destroy', params);
1688
                    break;
1689
                default:
1690
                    throw "Invalid VM action ("+action_name+")";
1691
            }
1692
        },
1693
        
1694
        __make_api_call: function(url, method, data, success, error, action, 
1695
                                  extra_params) {
1696
            var self = this;
1697
            error = error || function(){};
1698
            success = success || function(){};
1699

    
1700
            var params = {
1701
                url: url,
1702
                data: data,
1703
                success: function() { 
1704
                  self.handle_action_succeed.apply(self, arguments); 
1705
                  success.apply(this, arguments)
1706
                },
1707
                error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)},
1708
                error_params: { ns: "Machines actions", 
1709
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1710
                                extra_details: {'Machine ID': this.id, 
1711
                                                'URL': url, 
1712
                                                'Action': action || "undefined" },
1713
                                allow_reload: false
1714
                              },
1715
                display: false,
1716
                critical: false
1717
            }
1718
            _.extend(params, extra_params);
1719
            this.sync(method, this, params);
1720
        },
1721

    
1722
        handle_action_succeed: function() {
1723
            this.trigger("action:success", arguments);
1724
        },
1725
        
1726
        reset_action_error: function() {
1727
            this.action_error = false;
1728
            this.trigger("action:fail:reset", this.action_error);
1729
        },
1730

    
1731
        handle_action_fail: function() {
1732
            this.action_error = arguments;
1733
            this.trigger("action:fail", arguments);
1734
        },
1735

    
1736
        get_action_url: function(name) {
1737
            return this.url() + "/action";
1738
        },
1739

    
1740
        get_diagnostics_url: function() {
1741
            return this.url() + "/diagnostics";
1742
        },
1743

    
1744
        get_users: function() {
1745
            var image;
1746
            var users = [];
1747
            try {
1748
              var users = this.get_meta('users').split(" ");
1749
            } catch (err) { users = null }
1750
            if (!users) {
1751
              image = this.get_image();
1752
              if (image) {
1753
                  users = image.get_created_users();
1754
              }
1755
            }
1756
            return users;
1757
        },
1758

    
1759
        get_connection_info: function(host_os, success, error) {
1760
            var url = synnefo.config.ui_connect_url;
1761
            var users = this.get_users();
1762

    
1763
            params = {
1764
                ip_address: this.get_public_nic().get('ipv4'),
1765
                hostname: this.get_hostname(),
1766
                os: this.get_os(),
1767
                host_os: host_os,
1768
                srv: this.id
1769
            }
1770
            
1771
            if (users.length) { 
1772
                params['username'] = _.last(users)
1773
            }
1774

    
1775
            url = url + "?" + $.param(params);
1776

    
1777
            var ajax = snf.api.sync("read", undefined, { url: url, 
1778
                                                         error:error, 
1779
                                                         success:success, 
1780
                                                         handles_error:1});
1781
        }
1782
    })
1783
    
1784
    models.VM.ACTIONS = [
1785
        'start',
1786
        'shutdown',
1787
        'reboot',
1788
        'console',
1789
        'destroy',
1790
        'resize'
1791
    ]
1792

    
1793
    models.VM.TASK_STATE_STATUS_MAP = {
1794
      'BULDING': 'BUILD',
1795
      'REBOOTING': 'REBOOT',
1796
      'STOPPING': 'SHUTDOWN',
1797
      'STARTING': 'START',
1798
      'RESIZING': 'RESIZE',
1799
      'CONNECTING': 'CONNECT',
1800
      'DISCONNECTING': 'DISCONNECT',
1801
      'DESTROYING': 'DESTROY'
1802
    }
1803

    
1804
    models.VM.AVAILABLE_ACTIONS = {
1805
        'UNKNWON'       : ['destroy'],
1806
        'BUILD'         : ['destroy'],
1807
        'REBOOT'        : ['destroy'],
1808
        'STOPPED'       : ['start', 'destroy'],
1809
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1810
        'ERROR'         : ['destroy'],
1811
        'DELETED'       : ['destroy'],
1812
        'DESTROY'       : ['destroy'],
1813
        'SHUTDOWN'      : ['destroy'],
1814
        'START'         : ['destroy'],
1815
        'CONNECT'       : ['destroy'],
1816
        'DISCONNECT'    : ['destroy'],
1817
        'RESIZE'        : ['destroy']
1818
    }
1819

    
1820
    // api status values
1821
    models.VM.STATUSES = [
1822
        'UNKNWON',
1823
        'BUILD',
1824
        'REBOOT',
1825
        'STOPPED',
1826
        'ACTIVE',
1827
        'ERROR',
1828
        'DELETED',
1829
        'RESIZE'
1830
    ]
1831

    
1832
    // api status values
1833
    models.VM.CONNECT_STATES = [
1834
        'ACTIVE',
1835
        'REBOOT',
1836
        'SHUTDOWN'
1837
    ]
1838

    
1839
    // vm states
1840
    models.VM.STATES = models.VM.STATUSES.concat([
1841
        'DESTROY',
1842
        'SHUTDOWN',
1843
        'START',
1844
        'CONNECT',
1845
        'DISCONNECT',
1846
        'FIREWALL',
1847
        'RESIZE'
1848
    ]);
1849
    
1850
    models.VM.STATES_TRANSITIONS = {
1851
        'DESTROY' : ['DELETED'],
1852
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1853
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1854
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1855
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1856
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1857
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1858
        'RESIZE': ['ERROR', 'STOPPED']
1859
    }
1860

    
1861
    models.VM.TRANSITION_STATES = [
1862
        'DESTROY',
1863
        'SHUTDOWN',
1864
        'START',
1865
        'REBOOT',
1866
        'BUILD',
1867
        'RESIZE'
1868
    ]
1869

    
1870
    models.VM.ACTIVE_STATES = [
1871
        'BUILD', 'REBOOT', 'ACTIVE',
1872
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1873
    ]
1874

    
1875
    models.VM.BUILDING_STATES = [
1876
        'BUILD'
1877
    ]
1878

    
1879
    models.Networks = models.Collection.extend({
1880
        model: models.Network,
1881
        path: 'networks',
1882
        details: true,
1883
        //noUpdate: true,
1884
        defaults: {'nics':[],'linked_to':[]},
1885
        
1886
        parse: function (resp, xhr) {
1887
            // FIXME: depricated global var
1888
            if (!resp) { return []};
1889
            var data = _.filter(_.map(resp.networks, _.bind(this.parse_net_api_data, this)),
1890
                               function(e){ return e });
1891
            return data;
1892
        },
1893

    
1894
        add: function() {
1895
            ret = models.Networks.__super__.add.apply(this, arguments);
1896
            // update nics after each network addition
1897
            ret.each(function(r){
1898
                synnefo.storage.nics.update_net_nics(r);
1899
            });
1900
        },
1901

    
1902
        reset_pending_actions: function() {
1903
            this.each(function(net) {
1904
                net.get("actions").reset();
1905
            });
1906
        },
1907

    
1908
        do_all_pending_actions: function() {
1909
            this.each(function(net) {
1910
                net.do_all_pending_actions();
1911
            })
1912
        },
1913

    
1914
        parse_net_api_data: function(data) {
1915
            // append nic metadata
1916
            // net.get('nics') contains a list of vm/index objects 
1917
            // e.g. {'vm_id':12231, 'index':1}
1918
            // net.get('linked_to') contains a list of vms the network is 
1919
            // connected to e.g. [1001, 1002]
1920
            if (data.attachments && data.attachments) {
1921
                data['nics'] = {};
1922
                data['linked_to'] = [];
1923
                _.each(data.attachments, function(nic_id){
1924
                  
1925
                  var vm_id = NIC_REGEX.exec(nic_id)[1];
1926
                  var nic_index = parseInt(NIC_REGEX.exec(nic_id)[2]);
1927

    
1928
                  if (vm_id !== undefined && nic_index !== undefined) {
1929
                      data['nics'][nic_id] = {
1930
                          'vm_id': vm_id, 
1931
                          'index': nic_index, 
1932
                          'id': nic_id
1933
                      };
1934
                      if (data['linked_to'].indexOf(vm_id) == -1) {
1935
                        data['linked_to'].push(vm_id);
1936
                      }
1937
                  }
1938
                });
1939
            }
1940

    
1941
            if (data.status == "DELETED" && !this.get(parseInt(data.id))) {
1942
              return false;
1943
            }
1944
            return data;
1945
        },
1946

    
1947
        create: function (name, type, cidr, dhcp, callback) {
1948
            var params = {
1949
                network:{
1950
                    name:name
1951
                }
1952
            };
1953

    
1954
            if (!type) {
1955
                throw "Network type cannot be empty";
1956
            }
1957
            params.network.type = type;
1958
            if (cidr) {
1959
                params.network.cidr = cidr;
1960
            }
1961
            if (dhcp) {
1962
                params.network.dhcp = dhcp;
1963
            }
1964

    
1965
            if (dhcp === false) {
1966
                params.network.dhcp = false;
1967
            }
1968
            
1969
            var cb = function() {
1970
              callback();
1971
              synnefo.storage.quotas.get('cyclades.network.private').increase();
1972
            }
1973
            return this.api_call(this.path, "create", params, cb);
1974
        },
1975

    
1976
        get_public: function(){
1977
          return this.filter(function(n){return n.get('public')});
1978
        }
1979
    })
1980

    
1981
    models.Images = models.Collection.extend({
1982
        model: models.Image,
1983
        path: 'images',
1984
        details: true,
1985
        noUpdate: true,
1986
        supportIncUpdates: false,
1987
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1988
        meta_labels: {},
1989
        read_method: 'read',
1990

    
1991
        // update collection model with id passed
1992
        // making a direct call to the image
1993
        // api url
1994
        update_unknown_id: function(id, callback) {
1995
            var url = getUrl.call(this) + "/" + id;
1996
            this.api_call(this.path + "/" + id, this.read_method, {
1997
              _options:{
1998
                async:true, 
1999
                skip_api_error:true}
2000
              }, undefined, 
2001
            _.bind(function() {
2002
                if (!this.get(id)) {
2003
                            if (this.fallback_service) {
2004
                        // if current service has fallback_service attribute set
2005
                        // use this service to retrieve the missing image model
2006
                        var tmpservice = new this.fallback_service();
2007
                        tmpservice.update_unknown_id(id, _.bind(function(img){
2008
                            img.attributes.status = "DELETED";
2009
                            this.add(img.attributes);
2010
                            callback(this.get(id));
2011
                        }, this));
2012
                    } else {
2013
                        var title = synnefo.config.image_deleted_title || 'Deleted';
2014
                        // else add a dummy DELETED state image entry
2015
                        this.add({id:id, name:title, size:-1, 
2016
                                  progress:100, status:"DELETED"});
2017
                        callback(this.get(id));
2018
                    }   
2019
                } else {
2020
                    callback(this.get(id));
2021
                }
2022
            }, this), _.bind(function(image, msg, xhr) {
2023
                if (!image) {
2024
                    var title = synnefo.config.image_deleted_title || 'Deleted';
2025
                    this.add({id:id, name:title, size:-1, 
2026
                              progress:100, status:"DELETED"});
2027
                    callback(this.get(id));
2028
                    return;
2029
                }
2030
                var img_data = this._read_image_from_request(image, msg, xhr);
2031
                this.add(img_data);
2032
                callback(this.get(id));
2033
            }, this));
2034
        },
2035

    
2036
        _read_image_from_request: function(image, msg, xhr) {
2037
            return image.image;
2038
        },
2039

    
2040
        parse: function (resp, xhr) {
2041
            var parsed = _.map(resp.images, _.bind(this.parse_meta, this));
2042
            parsed = this.fill_owners(parsed);
2043
            return parsed;
2044
        },
2045

    
2046
        fill_owners: function(images) {
2047
            // do translate uuid->displayname if needed
2048
            // store display name in owner attribute for compatibility
2049
            var uuids = [];
2050

    
2051
            var images = _.map(images, function(img, index) {
2052
                if (synnefo.config.translate_uuids) {
2053
                    uuids.push(img['owner']);
2054
                }
2055
                img['owner_uuid'] = img['owner'];
2056
                return img;
2057
            });
2058
            
2059
            if (uuids.length > 0) {
2060
                var handle_results = function(data) {
2061
                    _.each(images, function (img) {
2062
                        img['owner'] = data.uuid_catalog[img['owner_uuid']];
2063
                    });
2064
                }
2065
                // notice the async false
2066
                var uuid_map = this.translate_uuids(uuids, false, 
2067
                                                    handle_results)
2068
            }
2069
            return images;
2070
        },
2071

    
2072
        translate_uuids: function(uuids, async, cb) {
2073
            var url = synnefo.config.user_catalog_url;
2074
            var data = JSON.stringify({'uuids': uuids});
2075
          
2076
            // post to user_catalogs api
2077
            snf.api.sync('create', undefined, {
2078
                url: url,
2079
                data: data,
2080
                async: async,
2081
                success:  cb
2082
            });
2083
        },
2084

    
2085
        get_meta_key: function(img, key) {
2086
            if (img.metadata && img.metadata && img.metadata[key]) {
2087
                return _.escape(img.metadata[key]);
2088
            }
2089
            return undefined;
2090
        },
2091

    
2092
        comparator: function(img) {
2093
            return -img.get_sort_order("sortorder") || 1000 * img.id;
2094
        },
2095

    
2096
        parse_meta: function(img) {
2097
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
2098
                if (img[key]) { return };
2099
                img[key] = this.get_meta_key(img, key) || "";
2100
            }, this));
2101
            return img;
2102
        },
2103

    
2104
        active: function() {
2105
            return this.filter(function(img){return img.get('status') != "DELETED"});
2106
        },
2107

    
2108
        predefined: function() {
2109
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
2110
        },
2111
        
2112
        fetch_for_type: function(type, complete, error) {
2113
            this.fetch({update:true, 
2114
                        success: complete, 
2115
                        error: error, 
2116
                        skip_api_error: true });
2117
        },
2118
        
2119
        get_images_for_type: function(type) {
2120
            if (this['get_{0}_images'.format(type)]) {
2121
                return this['get_{0}_images'.format(type)]();
2122
            }
2123

    
2124
            return this.active();
2125
        },
2126

    
2127
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
2128
            var load = false;
2129
            error = onError || function() {};
2130
            function complete(collection) { 
2131
                onComplete(collection.get_images_for_type(type)); 
2132
            }
2133
            
2134
            // do we need to fetch/update current collection entries
2135
            if (load) {
2136
                onStart();
2137
                this.fetch_for_type(type, complete, error);
2138
            } else {
2139
                // fallback to complete
2140
                complete(this);
2141
            }
2142
        }
2143
    })
2144

    
2145
    models.Flavors = models.Collection.extend({
2146
        model: models.Flavor,
2147
        path: 'flavors',
2148
        details: true,
2149
        noUpdate: true,
2150
        supportIncUpdates: false,
2151
        // update collection model with id passed
2152
        // making a direct call to the flavor
2153
        // api url
2154
        update_unknown_id: function(id, callback) {
2155
            var url = getUrl.call(this) + "/" + id;
2156
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
2157
            _.bind(function() {
2158
                this.add({id:id, cpu:"Unknown", ram:"Unknown", disk:"Unknown", name: "Unknown", status:"DELETED"})
2159
            }, this), _.bind(function(flv) {
2160
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
2161
                this.add(flv.flavor);
2162
            }, this));
2163
        },
2164

    
2165
        parse: function (resp, xhr) {
2166
            return _.map(resp.flavors, function(o) {
2167
              o.cpu = o['vcpus'];
2168
              o.disk_template = o['SNF:disk_template'];
2169
              return o
2170
            });
2171
        },
2172

    
2173
        comparator: function(flv) {
2174
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
2175
        },
2176
          
2177
        unavailable_values_for_quotas: function(quotas, flavors, extra) {
2178
            var flavors = flavors || this.active();
2179
            var index = {cpu:[], disk:[], ram:[]};
2180
            var extra = extra == undefined ? {cpu:0, disk:0, ram:0} : extra;
2181
            
2182
            _.each(flavors, function(el) {
2183

    
2184
                var disk_available = quotas['disk'] + extra.disk;
2185
                var disk_size = el.get_disk_size();
2186
                if (index.disk.indexOf(disk_size) == -1) {
2187
                  var disk = el.disk_to_bytes();
2188
                  if (disk > disk_available) {
2189
                    index.disk.push(disk_size);
2190
                  }
2191
                }
2192
                
2193
                var ram_available = quotas['ram'] + extra.ram * 1024 * 1024;
2194
                var ram_size = el.get_ram_size();
2195
                if (index.ram.indexOf(ram_size) == -1) {
2196
                  var ram = el.ram_to_bytes();
2197
                  if (ram > ram_available) {
2198
                    index.ram.push(el.get('ram'))
2199
                  }
2200
                }
2201

    
2202
                var cpu = el.get('cpu');
2203
                var cpu_available = quotas['cpu'] + extra.cpu;
2204
                if (index.cpu.indexOf(cpu) == -1) {
2205
                  if (cpu > cpu_available) {
2206
                    index.cpu.push(el.get('cpu'))
2207
                  }
2208
                }
2209
            });
2210
            return index;
2211
        },
2212

    
2213
        unavailable_values_for_image: function(img, flavors) {
2214
            var flavors = flavors || this.active();
2215
            var size = img.get_size();
2216
            
2217
            var index = {cpu:[], disk:[], ram:[]};
2218

    
2219
            _.each(this.active(), function(el) {
2220
                var img_size = size;
2221
                var flv_size = el.get_disk_size();
2222
                if (flv_size < img_size) {
2223
                    if (index.disk.indexOf(flv_size) == -1) {
2224
                        index.disk.push(flv_size);
2225
                    }
2226
                };
2227
            });
2228
            
2229
            return index;
2230
        },
2231

    
2232
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
2233
            if (!filter_list) { filter_list = this.models };
2234
            
2235
            return this.select(function(flv){
2236
                if (flv.get("cpu") == cpu + "" &&
2237
                   flv.get("ram") == mem + "" &&
2238
                   flv.get("disk") == disk + "" &&
2239
                   flv.get("disk_template") == disk_template &&
2240
                   filter_list.indexOf(flv) > -1) { return true; }
2241
            })[0];
2242
        },
2243
        
2244
        get_data: function(lst) {
2245
            var data = {'cpu': [], 'mem':[], 'disk':[], 'disk_template':[]};
2246

    
2247
            _.each(lst, function(flv) {
2248
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
2249
                    data.cpu.push(flv.get("cpu"));
2250
                }
2251
                if (data.mem.indexOf(flv.get("ram")) == -1) {
2252
                    data.mem.push(flv.get("ram"));
2253
                }
2254
                if (data.disk.indexOf(flv.get("disk")) == -1) {
2255
                    data.disk.push(flv.get("disk"));
2256
                }
2257
                if (data.disk_template.indexOf(flv.get("disk_template")) == -1) {
2258
                    data.disk_template.push(flv.get("disk_template"));
2259
                }
2260
            })
2261
            
2262
            return data;
2263
        },
2264

    
2265
        active: function() {
2266
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
2267
        }
2268
            
2269
    })
2270

    
2271
    models.VMS = models.Collection.extend({
2272
        model: models.VM,
2273
        path: 'servers',
2274
        details: true,
2275
        copy_image_meta: true,
2276

    
2277
        parse: function (resp, xhr) {
2278
            var data = resp;
2279
            if (!resp) { return [] };
2280
            data = _.filter(_.map(resp.servers, _.bind(this.parse_vm_api_data, this)), function(v){return v});
2281
            return data;
2282
        },
2283

    
2284
        parse_vm_api_data: function(data) {
2285
            // do not add non existing DELETED entries
2286
            if (data.status && data.status == "DELETED") {
2287
                if (!this.get(data.id)) {
2288
                    return false;
2289
                }
2290
            }
2291
            
2292
            if ('SNF:task_state' in data) { 
2293
                data['task_state'] = data['SNF:task_state'];
2294
                if (data['task_state']) {
2295
                    var status = models.VM.TASK_STATE_STATUS_MAP[data['task_state']];
2296
                    if (status) { data['status'] = status }
2297
                }
2298
            }
2299

    
2300
            // OS attribute
2301
            if (this.has_meta(data)) {
2302
                data['OS'] = data.metadata.OS || snf.config.unknown_os;
2303
            }
2304
            
2305
            if (!data.diagnostics) {
2306
                data.diagnostics = [];
2307
            }
2308

    
2309
            // network metadata
2310
            data['firewalls'] = {};
2311
            data['nics'] = {};
2312
            data['linked_to'] = [];
2313

    
2314
            if (data['attachments'] && data['attachments']) {
2315
                var nics = data['attachments'];
2316
                _.each(nics, function(nic) {
2317
                    var net_id = nic.network_id;
2318
                    var index = parseInt(NIC_REGEX.exec(nic.id)[2]);
2319
                    if (data['linked_to'].indexOf(net_id) == -1) {
2320
                        data['linked_to'].push(net_id);
2321
                    }
2322

    
2323
                    data['nics'][nic.id] = nic;
2324
                })
2325
            }
2326
            
2327
            // if vm has no metadata, no metadata object
2328
            // is in json response, reset it to force
2329
            // value update
2330
            if (!data['metadata']) {
2331
                data['metadata'] = {};
2332
            }
2333
            
2334
            // v2.0 API returns objects
2335
            data.image_obj = data.image;
2336
            data.image = data.image_obj.id;
2337
            data.flavor_obj = data.flavor;
2338
            data.flavor = data.flavor_obj.id;
2339

    
2340
            return data;
2341
        },
2342

    
2343
        add: function() {
2344
            ret = models.VMS.__super__.add.apply(this, arguments);
2345
            ret.each(function(r){
2346
                synnefo.storage.nics.update_vm_nics(r);
2347
            });
2348
        },
2349
        
2350
        get_reboot_required: function() {
2351
            return this.filter(function(vm){return vm.get("reboot_required") == true})
2352
        },
2353

    
2354
        has_pending_actions: function() {
2355
            return this.filter(function(vm){return vm.pending_action}).length > 0;
2356
        },
2357

    
2358
        reset_pending_actions: function() {
2359
            this.each(function(vm) {
2360
                vm.clear_pending_action();
2361
            })
2362
        },
2363

    
2364
        do_all_pending_actions: function(success, error) {
2365
            this.each(function(vm) {
2366
                if (vm.has_pending_action()) {
2367
                    vm.call(vm.pending_action, success, error);
2368
                    vm.clear_pending_action();
2369
                }
2370
            })
2371
        },
2372
        
2373
        do_all_reboots: function(success, error) {
2374
            this.each(function(vm) {
2375
                if (vm.get("reboot_required")) {
2376
                    vm.call("reboot", success, error);
2377
                }
2378
            });
2379
        },
2380

    
2381
        reset_reboot_required: function() {
2382
            this.each(function(vm) {
2383
                vm.set({'reboot_required': undefined});
2384
            })
2385
        },
2386
        
2387
        stop_stats_update: function(exclude) {
2388
            var exclude = exclude || [];
2389
            this.each(function(vm) {
2390
                if (exclude.indexOf(vm) > -1) {
2391
                    return;
2392
                }
2393
                vm.stop_stats_update();
2394
            })
2395
        },
2396
        
2397
        has_meta: function(vm_data) {
2398
            return vm_data.metadata && vm_data.metadata
2399
        },
2400

    
2401
        has_addresses: function(vm_data) {
2402
            return vm_data.metadata && vm_data.metadata
2403
        },
2404

    
2405
        create: function (name, image, flavor, meta, extra, callback) {
2406

    
2407
            if (this.copy_image_meta) {
2408
                if (synnefo.config.vm_image_common_metadata) {
2409
                    _.each(synnefo.config.vm_image_common_metadata, 
2410
                        function(key){
2411
                            if (image.get_meta(key)) {
2412
                                meta[key] = image.get_meta(key);
2413
                            }
2414
                    });
2415
                }
2416

    
2417
                if (image.get("OS")) {
2418
                    meta['OS'] = image.get("OS");
2419
                }
2420
            }
2421
            
2422
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, 
2423
                    metadata:meta}
2424
            opts = _.extend(opts, extra);
2425
            
2426
            var cb = function(data) {
2427
              synnefo.storage.quotas.get('cyclades.vm').increase();
2428
              callback(data);
2429
            }
2430

    
2431
            this.api_call(this.path, "create", {'server': opts}, undefined, 
2432
                          undefined, cb, {critical: true});
2433
        },
2434

    
2435
        load_missing_images: function(callback) {
2436
          var missing_ids = [];
2437
          var resolved = 0;
2438

    
2439
          // fill missing_ids
2440
          this.each(function(el) {
2441
            var imgid = el.get("image");
2442
            var existing = synnefo.storage.images.get(imgid);
2443
            if (!existing && missing_ids.indexOf(imgid) == -1) {
2444
              missing_ids.push(imgid);
2445
            }
2446
          });
2447
          var check = function() {
2448
            // once all missing ids where resolved continue calling the 
2449
            // callback
2450
            resolved++;
2451
            if (resolved == missing_ids.length) {
2452
              callback(missing_ids)
2453
            }
2454
          }
2455
          if (missing_ids.length == 0) {
2456
            callback(missing_ids);
2457
            return;
2458
          }
2459
          // start resolving missing image ids
2460
          _(missing_ids).each(function(imgid){
2461
            synnefo.storage.images.update_unknown_id(imgid, check);
2462
          });
2463
        },
2464

    
2465
        get_connectable: function() {
2466
            return storage.vms.filter(function(vm){
2467
                return !vm.in_error_state() && !vm.is_building();
2468
            });
2469
        }
2470
    })
2471
    
2472
    models.NIC = models.Model.extend({
2473
        
2474
        initialize: function() {
2475
            models.NIC.__super__.initialize.apply(this, arguments);
2476
            this.pending_for_firewall = false;
2477
            this.bind("change:firewallProfile", _.bind(this.check_firewall, this));
2478
            this.bind("change:pending_firewall", function(nic) {
2479
                nic.get_network().update_state();
2480
            });
2481
            this.get_vm().bind("remove", function(){
2482
                try {
2483
                    this.collection.remove(this);
2484
                } catch (err) {};
2485
            }, this);
2486
            this.get_network().bind("remove", function(){
2487
                try {
2488
                    this.collection.remove(this);
2489
                } catch (err) {};
2490
            }, this);
2491

    
2492
        },
2493

    
2494
        get_vm: function() {
2495
            return synnefo.storage.vms.get(parseInt(this.get('vm_id')));
2496
        },
2497

    
2498
        get_network: function() {
2499
            return synnefo.storage.networks.get(this.get('network_id'));
2500
        },
2501

    
2502
        get_v6_address: function() {
2503
            return this.get("ipv6");
2504
        },
2505

    
2506
        get_v4_address: function() {
2507
            return this.get("ipv4");
2508
        },
2509

    
2510
        set_firewall: function(value, callback, error, options) {
2511
            var net_id = this.get('network_id');
2512
            var self = this;
2513

    
2514
            // api call data
2515
            var payload = {"firewallProfile":{"profile":value}};
2516
            payload._options = _.extend({critical: false}, options);
2517
            
2518
            this.set({'pending_firewall': value});
2519
            this.set({'pending_firewall_sending': true});
2520
            this.set({'pending_firewall_from': this.get('firewallProfile')});
2521

    
2522
            var success_cb = function() {
2523
                if (callback) {
2524
                    callback();
2525
                }
2526
                self.set({'pending_firewall_sending': false});
2527
            };
2528

    
2529
            var error_cb = function() {
2530
                self.reset_pending_firewall();
2531
            }
2532
            
2533
            this.get_vm().api_call(this.get_vm().api_path() + "/action", "create", payload, success_cb, error_cb);
2534
        },
2535

    
2536
        reset_pending_firewall: function() {
2537
            this.set({'pending_firewall': false});
2538
            this.set({'pending_firewall': false});
2539
        },
2540

    
2541
        check_firewall: function() {
2542
            var firewall = this.get('firewallProfile');
2543
            var pending = this.get('pending_firewall');
2544
            var previous = this.get('pending_firewall_from');
2545
            if (previous != firewall) { this.get_vm().require_reboot() };
2546
            this.reset_pending_firewall();
2547
        }
2548
        
2549
    });
2550

    
2551
    models.NICs = models.Collection.extend({
2552
        model: models.NIC,
2553
        
2554
        add_or_update: function(nic_id, data, vm) {
2555
            var params = _.clone(data);
2556
            var vm;
2557
            params.attachment_id = params.id;
2558
            params.id = params.id + '-' + params.network_id;
2559
            params.vm_id = parseInt(NIC_REGEX.exec(nic_id)[1]);
2560

    
2561
            if (!this.get(params.id)) {
2562
                this.add(params);
2563
                var nic = this.get(params.id);
2564
                vm = nic.get_vm();
2565
                nic.get_network().decrease_connecting();
2566
                nic.bind("remove", function() {
2567
                    nic.set({"removing": 0});
2568
                    if (this.get_network()) {
2569
                        // network might got removed before nic
2570
                        nic.get_network().update_state();
2571
                    }
2572
                });
2573

    
2574
            } else {
2575
                this.get(params.id).set(params);
2576
                vm = this.get(params.id).get_vm();
2577
            }
2578
            
2579
            // vm nics changed, trigger vm update
2580
            if (vm) { vm.trigger("change", vm)};
2581
        },
2582
        
2583
        reset_nics: function(nics, filter_attr, filter_val) {
2584
            var nics_to_check = this.filter(function(nic) {
2585
                return nic.get(filter_attr) == filter_val;
2586
            });
2587
            
2588
            _.each(nics_to_check, function(nic) {
2589
                if (nics.indexOf(nic.get('id')) == -1) {
2590
                    this.remove(nic);
2591
                } else {
2592
                }
2593
            }, this);
2594
        },
2595

    
2596
        update_vm_nics: function(vm) {
2597
            var nics = vm.get('nics');
2598
            this.reset_nics(_.map(nics, function(nic, key){
2599
                return key + "-" + nic.network_id;
2600
            }), 'vm_id', vm.id);
2601

    
2602
            _.each(nics, function(val, key) {
2603
                var net = synnefo.storage.networks.get(val.network_id);
2604
                if (net && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
2605
                    this.add_or_update(key, vm.get('nics')[key], vm);
2606
                }
2607
            }, this);
2608
        },
2609

    
2610
        update_net_nics: function(net) {
2611
            var nics = net.get('nics');
2612
            this.reset_nics(_.map(nics, function(nic, key){
2613
                return key + "-" + net.get('id');
2614
            }), 'network_id', net.id);
2615

    
2616
            _.each(nics, function(val, key) {
2617
                var vm = synnefo.storage.vms.get(val.vm_id);
2618
                if (vm && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
2619
                    this.add_or_update(key, vm.get('nics')[key], vm);
2620
                }
2621
            }, this);
2622
        }
2623
    });
2624

    
2625
    models.PublicKey = models.Model.extend({
2626
        path: 'keys',
2627
        api_type: 'userdata',
2628
        details: false,
2629
        noUpdate: true,
2630

    
2631

    
2632
        get_public_key: function() {
2633
            return cryptico.publicKeyFromString(this.get("content"));
2634
        },
2635

    
2636
        get_filename: function() {
2637
            return "{0}.pub".format(this.get("name"));
2638
        },
2639

    
2640
        identify_type: function() {
2641
            try {
2642
                var cont = snf.util.validatePublicKey(this.get("content"));
2643
                var type = cont.split(" ")[0];
2644
                return synnefo.util.publicKeyTypesMap[type];
2645
            } catch (err) { return false };
2646
        }
2647

    
2648
    })
2649
    
2650
    models._ActionsModel = models.Model.extend({
2651
      defaults: { pending: null },
2652
      actions: [],
2653
      status: {
2654
        INACTIVE: 0,
2655
        PENDING: 1,
2656
        CALLED: 2
2657
      },
2658

    
2659
      initialize: function(attrs, opts) {
2660
        models._ActionsModel.__super__.initialize.call(this, attrs);
2661
        this.actions = opts.actions;
2662
        this.model = opts.model;
2663
        this.bind("change", function() {
2664
          this.set({'pending': this.get_pending()});
2665
        }, this);
2666
        this.clear();
2667
      },
2668
      
2669
      _in_status: function(st) {
2670
        var actions = null;
2671
        _.each(this.attributes, function(status, action){
2672
          if (status == st) {
2673
            if (!actions) {
2674
              actions = []
2675
            }
2676
            actions.push(action);
2677
          }
2678
        });
2679
        return actions;
2680
      },
2681

    
2682
      get_pending: function() {
2683
        return this._in_status(this.status.PENDING);
2684
      },
2685

    
2686
      unset_pending_action: function(action) {
2687
        var data = {};
2688
        data[action] = this.status.INACTIVE;
2689
        this.set(data);
2690
      },
2691

    
2692
      set_pending_action: function(action, reset_pending) {
2693
        reset_pending = reset_pending === undefined ? true : reset_pending;
2694
        var data = {};
2695
        data[action] = this.status.PENDING;
2696
        if (reset_pending) {
2697
          this.reset_pending();
2698
        }
2699
        this.set(data);
2700
      },
2701
      
2702
      reset_pending: function() {
2703
        var data = {};
2704
        _.each(this.actions, function(action) {
2705
          data[action] = this.status.INACTIVE;
2706
        }, this);
2707
        this.set(data);
2708
      }
2709
    });
2710

    
2711
    models.PublicPool = models.Model.extend({});
2712
    models.PublicPools = models.Collection.extend({
2713
      model: models.PublicPool,
2714
      path: 'os-floating-ip-pools',
2715
      api_type: 'compute',
2716
      noUpdate: true,
2717

    
2718
      parse: function(data) {
2719
        return _.map(data.floating_ip_pools, function(pool) {
2720
          pool.id = pool.name;
2721
          return pool;
2722
        });
2723
      }
2724
    });
2725

    
2726
    models.PublicIP = models.Model.extend({
2727
        path: 'os-floating-ips',
2728
        has_status: false,
2729
        
2730
        initialize: function() {
2731
            models.PublicIP.__super__.initialize.apply(this, arguments);
2732
            this.bind('change:instance_id', _.bind(this.handle_status_change, this));
2733
        },
2734

    
2735
        handle_status_change: function() {
2736
            this.set({state: null});
2737
        },
2738

    
2739
        get_vm: function() {
2740
            if (this.get('instance_id')) {
2741
                return synnefo.storage.vms.get(parseInt(this.get('instance_id')));
2742
            }
2743
            return null;
2744
        },
2745

    
2746
        connect_to: function(vm) {
2747
        }
2748
    });
2749

    
2750
    models.PublicIPs = models.Collection.extend({
2751
        model: models.PublicIP,
2752
        path: 'os-floating-ips',
2753
        api_type: 'compute',
2754
        noUpdate: true,
2755

    
2756
        parse: function(resp) {
2757
            resp = _.map(resp.floating_ips, function(ip) {
2758
              return ip;
2759
            });
2760

    
2761
            return resp;
2762
        }
2763
    });
2764

    
2765
    models.PublicKeys = models.Collection.extend({
2766
        model: models.PublicKey,
2767
        details: false,
2768
        path: 'keys',
2769
        api_type: 'userdata',
2770
        noUpdate: true,
2771

    
2772
        generate_new: function(success, error) {
2773
            snf.api.sync('create', undefined, {
2774
                url: getUrl.call(this, this.base_url) + "/generate", 
2775
                success: success, 
2776
                error: error,
2777
                skip_api_error: true
2778
            });
2779
        },
2780

    
2781
        add_crypto_key: function(key, success, error, options) {
2782
            var options = options || {};
2783
            var m = new models.PublicKey();
2784

    
2785
            // guess a name
2786
            var name_tpl = "my generated public key";
2787
            var name = name_tpl;
2788
            var name_count = 1;
2789
            
2790
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
2791
                name = name_tpl + " " + name_count;
2792
                name_count++;
2793
            }
2794
            
2795
            m.set({name: name});
2796
            m.set({content: key});
2797
            
2798
            options.success = function () { return success(m) };
2799
            options.errror = error;
2800
            options.skip_api_error = true;
2801
            
2802
            this.create(m.attributes, options);
2803
        }
2804
    });
2805

    
2806
  
2807
    models.Quota = models.Model.extend({
2808

    
2809
        initialize: function() {
2810
            models.Quota.__super__.initialize.apply(this, arguments);
2811
            this.bind("change", this.check, this);
2812
            this.check();
2813
        },
2814
        
2815
        check: function() {
2816
            var usage, limit;
2817
            usage = this.get('usage');
2818
            limit = this.get('limit');
2819
            if (usage >= limit) {
2820
                this.trigger("available");
2821
            } else {
2822
                this.trigger("unavailable");
2823
            }
2824
        },
2825

    
2826
        increase: function(val) {
2827
            if (val === undefined) { val = 1};
2828
            this.set({'usage': this.get('usage') + val})
2829
        },
2830

    
2831
        decrease: function(val) {
2832
            if (val === undefined) { val = 1};
2833
            this.set({'usage': this.get('usage') - val})
2834
        },
2835

    
2836
        can_consume: function() {
2837
            var usage, limit;
2838
            usage = this.get('usage');
2839
            limit = this.get('limit');
2840
            if (usage >= limit) {
2841
                return false
2842
            } else {
2843
                return true
2844
            }
2845
        },
2846
        
2847
        is_bytes: function() {
2848
            return this.get('resource').get('unit') == 'bytes';
2849
        },
2850
        
2851
        get_available: function(active) {
2852
            suffix = '';
2853
            if (active) { suffix = '_active'}
2854
            var value = this.get('limit'+suffix) - this.get('usage'+suffix);
2855
            if (active) {
2856
              if (this.get('available') <= value) {
2857
                value = this.get('available');
2858
              }
2859
            }
2860
            if (value < 0) { return value }
2861
            return value
2862
        },
2863

    
2864
        get_readable: function(key, active) {
2865
            var value;
2866
            if (key == 'available') {
2867
                value = this.get_available(active);
2868
            } else {
2869
                value = this.get(key)
2870
            }
2871
            if (value <= 0) { value = 0 }
2872
            if (!this.is_bytes()) {
2873
              return value + "";
2874
            }
2875
            return snf.util.readablizeBytes(value);
2876
        }
2877
    });
2878

    
2879
    models.Quotas = models.Collection.extend({
2880
        model: models.Quota,
2881
        api_type: 'accounts',
2882
        path: 'quotas',
2883
        parse: function(resp) {
2884
            filtered = _.map(resp.system, function(value, key) {
2885
                var available = (value.limit - value.usage) || 0;
2886
                var available_active = available;
2887
                var keysplit = key.split(".");
2888
                var limit_active = value.limit;
2889
                var usage_active = value.usage;
2890
                keysplit[keysplit.length-1] = "active_" + keysplit[keysplit.length-1];
2891
                var activekey = keysplit.join(".");
2892
                var exists = resp.system[activekey];
2893
                if (exists) {
2894
                    available_active = exists.limit - exists.usage;
2895
                    limit_active = exists.limit;
2896
                    usage_active = exists.usage;
2897
                }
2898
                return _.extend(value, {'name': key, 'id': key, 
2899
                          'available': available,
2900
                          'available_active': available_active,
2901
                          'limit_active': limit_active,
2902
                          'usage_active': usage_active,
2903
                          'resource': snf.storage.resources.get(key)});
2904
            });
2905
            return filtered;
2906
        },
2907
        
2908
        get_by_id: function(k) {
2909
          return this.filter(function(q) { return q.get('name') == k})[0]
2910
        },
2911

    
2912
        get_available_for_vm: function(options) {
2913
          var quotas = synnefo.storage.quotas;
2914
          var key = 'available';
2915
          var available_quota = {};
2916
          _.each(['cyclades.ram', 'cyclades.cpu', 'cyclades.disk'], 
2917
            function (key) {
2918
              var value = quotas.get(key).get_available(true);
2919
              available_quota[key.replace('cyclades.', '')] = value;
2920
          });
2921
          return available_quota;
2922
        }
2923
    })
2924

    
2925
    models.Resource = models.Model.extend({
2926
        api_type: 'accounts',
2927
        path: 'resources'
2928
    });
2929

    
2930
    models.Resources = models.Collection.extend({
2931
        api_type: 'accounts',
2932
        path: 'resources',
2933
        model: models.Network,
2934

    
2935
        parse: function(resp) {
2936
            return _.map(resp, function(value, key) {
2937
                return _.extend(value, {'name': key, 'id': key});
2938
            })
2939
        }
2940
    });
2941
    
2942
    // storage initialization
2943
    snf.storage.images = new models.Images();
2944
    snf.storage.flavors = new models.Flavors();
2945
    snf.storage.networks = new models.Networks();
2946
    snf.storage.vms = new models.VMS();
2947
    snf.storage.keys = new models.PublicKeys();
2948
    snf.storage.nics = new models.NICs();
2949
    snf.storage.resources = new models.Resources();
2950
    snf.storage.quotas = new models.Quotas();
2951
    snf.storage.public_ips = new models.PublicIPs();
2952
    snf.storage.public_pools = new models.PublicPools();
2953

    
2954
})(this);