Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / ui / static / snf / js / models.js @ 31bc4729

History | View | Annotate | Download (81.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
    // i18n
65
    BUILDING_MESSAGES = window.BUILDING_MESSAGES || {'INIT': 'init', 'COPY': '{0}, {1}, {2}', 'FINAL': 'final'};
66

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

    
75

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

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

    
95
            if (this.storage_attrs) {
96
              this.init_storage_attrs();
97
            }
98

    
99
            if (this.model_actions) {
100
              this.init_model_actions();             
101
            }
102

    
103
            models.Model.__super__.initialize.apply(this, arguments);
104

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

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

    
149
              if (!val) { 
150
                // update with undefined and return
151
                data[key] = undefined;
152
                this.set(data);
153
                return;
154
              };
155
            
156
              // retrieve related object (check if its a Model??)
157
              var obj = store.get(val);
158
              
159
              if (obj) {
160
                // set related object
161
                data[attr_name] = obj;
162
                this.set(data, {silent:true})
163
                this.trigger("change:" + attr_name, obj);
164
              } else {
165
                var self = this;
166
                var retry = window.setInterval(function(){
167
                  var obj = store.get(val);
168
                  if (obj) {
169
                    data[key] = obj;
170
                    self.set(data, {silent:true})
171
                    self.trigger("change:" + attr_name, obj);
172
                    clearInterval(retry);
173
                  }
174
                }, 500);
175
              }
176
            }
177
            
178
            var self = this;
179
            this.bind('change:' + attr, function(model) {
180
              resolve_related_instance.call(model, store, key, attr_resolver(model, attr));
181
            }, this);
182

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

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

    
259
        url: function(options) {
260
            return getUrl.call(this, this.base_url) + "/" + this.id;
261
        },
262

    
263
        api_path: function(options) {
264
            return this.path + "/" + this.id;
265
        },
266

    
267
        parse: function(resp, xhr) {
268
        },
269

    
270
        remove: function(complete, error, success) {
271
            this.api_call(this.api_path(), "delete", undefined, complete, error, success);
272
        },
273

    
274
        changedKeys: function() {
275
            return _.keys(this.changedAttributes() || {});
276
        },
277
            
278
        // return list of changed attributes that included in passed list
279
        // argument
280
        getKeysChanged: function(keys) {
281
            return _.intersection(keys, this.changedKeys());
282
        },
283
        
284
        // boolean check of keys changed
285
        keysChanged: function(keys) {
286
            return this.getKeysChanged(keys).length > 0;
287
        },
288

    
289
        // check if any of the passed attribues has changed
290
        hasOnlyChange: function(keys) {
291
            var ret = false;
292
            _.each(keys, _.bind(function(key) {
293
                if (this.changedKeys().length == 1 && this.changedKeys().indexOf(key) > -1) { ret = true};
294
            }, this));
295
            return ret;
296
        }
297

    
298
    })
299
    
300
    // Base object for all our model collections
301
    models.Collection = bb.Collection.extend({
302
        sync: snf.api.sync,
303
        api: snf.api,
304
        api_type: 'compute',
305
        supportIncUpdates: true,
306

    
307
        initialize: function() {
308
            models.Collection.__super__.initialize.apply(this, arguments);
309
            this.api_call = _.bind(this.api.call, this);
310
        },
311

    
312
        url: function(options, method) {
313
            return getUrl.call(this, this.base_url) + (
314
                    options.details || this.details && method != 'create' ? '/detail' : '');
315
        },
316

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

    
345
        create: function(model, options) {
346
            var coll = this;
347
            options || (options = {});
348
            model = this._prepareModel(model, options);
349
            if (!model) return false;
350
            var success = options.success;
351
            options.success = function(nextModel, resp, xhr) {
352
                if (success) success(nextModel, resp, xhr);
353
            };
354
            model.save(null, options);
355
            return model;
356
        },
357

    
358
        get_fetcher: function(interval, increase, fast, increase_after_calls, max, initial_call, params) {
359
            var fetch_params = params || {};
360
            var handler_options = {};
361

    
362
            fetch_params.skips_timeouts = true;
363
            handler_options.interval = interval;
364
            handler_options.increase = increase;
365
            handler_options.fast = fast;
366
            handler_options.increase_after_calls = increase_after_calls;
367
            handler_options.max= max;
368
            handler_options.id = "collection id";
369

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

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

    
401
        get_description: function(escape) {
402
            if (escape == undefined) { escape = true };
403
            if (escape) { return this.escape('description') || "No description available"}
404
            return this.get('description') || "No description available."
405
        },
406

    
407
        get_meta: function(key) {
408
            if (this.get('metadata') && this.get('metadata')) {
409
                if (!this.get('metadata')[key]) { return null }
410
                return _.escape(this.get('metadata')[key]);
411
            } else {
412
                return null;
413
            }
414
        },
415

    
416
        get_meta_keys: function() {
417
            if (this.get('metadata') && this.get('metadata')) {
418
                return _.keys(this.get('metadata'));
419
            } else {
420
                return [];
421
            }
422
        },
423

    
424
        get_owner: function() {
425
            return this.get('owner') || _.keys(synnefo.config.system_images_owners)[0];
426
        },
427

    
428
        get_owner_uuid: function() {
429
            return this.get('owner_uuid');
430
        },
431

    
432
        is_system_image: function() {
433
          var owner = this.get_owner();
434
          return _.include(_.keys(synnefo.config.system_images_owners), owner)
435
        },
436

    
437
        owned_by: function(user) {
438
          if (!user) { user = synnefo.user }
439
          return user.get_username() == this.get('owner_uuid');
440
        },
441

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

    
458
        get_os: function() {
459
            return this.get_meta('OS');
460
        },
461

    
462
        get_gui: function() {
463
            return this.get_meta('GUI');
464
        },
465

    
466
        get_created_users: function() {
467
            try {
468
              var users = this.get_meta('users').split(" ");
469
            } catch (err) { users = null }
470
            if (!users) {
471
                var osfamily = this.get_meta('osfamily');
472
                if (osfamily == 'windows') { 
473
                  users = ['Administrator'];
474
                } else {
475
                  users = ['root'];
476
                }
477
            }
478
            return users;
479
        },
480

    
481
        get_sort_order: function() {
482
            return parseInt(this.get('metadata') ? this.get('metadata').sortorder : -1)
483
        },
484

    
485
        get_vm: function() {
486
            var vm_id = this.get("serverRef");
487
            var vm = undefined;
488
            vm = storage.vms.get(vm_id);
489
            return vm;
490
        },
491

    
492
        is_public: function() {
493
            return this.get('is_public') == undefined ? true : this.get('is_public');
494
        },
495

    
496
        is_deleted: function() {
497
            return this.get('status') == "DELETED"
498
        },
499
        
500
        ssh_keys_paths: function() {
501
            return _.map(this.get_created_users(), function(username) {
502
                prepend = '';
503
                if (username != 'root') {
504
                    prepend = '/home'
505
                }
506
                return {'user': username, 'path': '{1}/{0}/.ssh/authorized_keys'.format(username, 
507
                                                             prepend)};
508
            });
509
        },
510

    
511
        _supports_ssh: function() {
512
            if (synnefo.config.support_ssh_os_list.indexOf(this.get_os()) > -1) {
513
                return true;
514
            }
515
            if (this.get_meta('osfamily') == 'linux') {
516
              return true;
517
            }
518
            return false;
519
        },
520

    
521
        supports: function(feature) {
522
            if (feature == "ssh") {
523
                return this._supports_ssh()
524
            }
525
            return false;
526
        },
527

    
528
        personality_data_for_keys: function(keys) {
529
            return _.map(this.ssh_keys_paths(), function(pathinfo) {
530
                var contents = '';
531
                _.each(keys, function(key){
532
                    contents = contents + key.get("content") + "\n"
533
                });
534
                contents = $.base64.encode(contents);
535

    
536
                return {
537
                    path: pathinfo.path,
538
                    contents: contents,
539
                    mode: 0600,
540
                    owner: pathinfo.user
541
                }
542
            });
543
        }
544
    });
545

    
546
    // Flavor model
547
    models.Flavor = models.Model.extend({
548
        path: 'flavors',
549

    
550
        details_string: function() {
551
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
552
        },
553

    
554
        get_disk_size: function() {
555
            return parseInt(this.get("disk") * 1000)
556
        },
557

    
558
        get_ram_size: function() {
559
            return parseInt(this.get("ram"))
560
        },
561

    
562
        get_disk_template_info: function() {
563
            var info = snf.config.flavors_disk_templates_info[this.get("disk_template")];
564
            if (!info) {
565
                info = { name: this.get("disk_template"), description:'' };
566
            }
567
            return info
568
        },
569

    
570
        disk_to_bytes: function() {
571
            return parseInt(this.get("disk")) * 1024 * 1024 * 1024;
572
        },
573

    
574
        ram_to_bytes: function() {
575
            return parseInt(this.get("ram")) * 1024 * 1024;
576
        },
577

    
578
    });
579
    
580
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
581
    _.extend(models.ParamsList.prototype, bb.Events, {
582

    
583
        initialize: function(parent, param_name) {
584
            this.parent = parent;
585
            this.actions = {};
586
            this.param_name = param_name;
587
            this.length = 0;
588
        },
589
        
590
        has_action: function(action) {
591
            return this.actions[action] ? true : false;
592
        },
593
            
594
        _parse_params: function(arguments) {
595
            if (arguments.length <= 1) {
596
                return [];
597
            }
598

    
599
            var args = _.toArray(arguments);
600
            return args.splice(1);
601
        },
602

    
603
        contains: function(action, params) {
604
            params = this._parse_params(arguments);
605
            var has_action = this.has_action(action);
606
            if (!has_action) { return false };
607

    
608
            var paramsEqual = false;
609
            _.each(this.actions[action], function(action_params) {
610
                if (_.isEqual(action_params, params)) {
611
                    paramsEqual = true;
612
                }
613
            });
614
                
615
            return paramsEqual;
616
        },
617
        
618
        is_empty: function() {
619
            return _.isEmpty(this.actions);
620
        },
621

    
622
        add: function(action, params) {
623
            params = this._parse_params(arguments);
624
            if (this.contains.apply(this, arguments)) { return this };
625
            var isnew = false
626
            if (!this.has_action(action)) {
627
                this.actions[action] = [];
628
                isnew = true;
629
            };
630

    
631
            this.actions[action].push(params);
632
            this.parent.trigger("change:" + this.param_name, this.parent, this);
633
            if (isnew) {
634
                this.trigger("add", action, params);
635
            } else {
636
                this.trigger("change", action, params);
637
            }
638
            return this;
639
        },
640
        
641
        remove_all: function(action) {
642
            if (this.has_action(action)) {
643
                delete this.actions[action];
644
                this.parent.trigger("change:" + this.param_name, this.parent, this);
645
                this.trigger("remove", action);
646
            }
647
            return this;
648
        },
649

    
650
        reset: function() {
651
            this.actions = {};
652
            this.parent.trigger("change:" + this.param_name, this.parent, this);
653
            this.trigger("reset");
654
            this.trigger("remove");
655
        },
656

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

    
677
    });
678

    
679
    // Virtualmachine model
680
    models.VM = models.Model.extend({
681

    
682
        path: 'servers',
683
        has_status: true,
684
        proxy_attrs: {
685
          'busy': [
686
            ['status', 'state'], function() {
687
              return !_.contains(['ACTIVE', 'SHUTDOWN'], this.get('status'));
688
            }
689
          ],
690
        },
691

    
692
        initialize: function(params) {
693
            
694
            this.pending_firewalls = {};
695
            
696
            models.VM.__super__.initialize.apply(this, arguments);
697

    
698
            this.set({state: params.status || "ERROR"});
699
            this.log = new snf.logging.logger("VM " + this.id);
700
            this.pending_action = undefined;
701
            
702
            // init stats parameter
703
            this.set({'stats': undefined}, {silent: true});
704
            // defaults to not update the stats
705
            // each view should handle this vm attribute 
706
            // depending on if it displays stat images or not
707
            this.do_update_stats = false;
708
            
709
            // interval time
710
            // this will dynamicaly change if the server responds that
711
            // images get refreshed on different intervals
712
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
713
            this.stats_available = false;
714

    
715
            // initialize interval
716
            this.init_stats_intervals(this.stats_update_interval);
717
            
718
            // handle progress message on instance change
719
            this.bind("change", _.bind(this.update_status_message, this));
720
            this.bind("change:task_state", _.bind(this.update_status, this));
721
            // force update of progress message
722
            this.update_status_message(true);
723
            
724
            // default values
725
            this.bind("change:state", _.bind(function(){
726
                if (this.state() == "DESTROY") { 
727
                    this.handle_destroy() 
728
                }
729
            }, this));
730

    
731
        },
732

    
733
        status: function(st) {
734
            if (!st) { return this.get("status")}
735
            return this.set({status:st});
736
        },
737
        
738
        update_status: function() {
739
            this.set_status(this.get('status'));
740
        },
741

    
742
        set_status: function(st) {
743
            var new_state = this.state_for_api_status(st);
744
            var transition = false;
745

    
746
            if (this.state() != new_state) {
747
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
748
                    transition = this.state();
749
                }
750
            }
751
            
752
            // call it silently to avoid double change trigger
753
            var state = this.state_for_api_status(st);
754
            this.set({'state': state}, {silent: true});
755
            
756
            // trigger transition
757
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
758
                this.trigger("transition", {from:transition, to:new_state}) 
759
            };
760
            return st;
761
        },
762
            
763
        get_diagnostics: function(success) {
764
            this.__make_api_call(this.get_diagnostics_url(),
765
                                 "read", // create so that sync later uses POST to make the call
766
                                 null, // payload
767
                                 function(data) {
768
                                     success(data);
769
                                 },  
770
                                 null, 'diagnostics');
771
        },
772

    
773
        has_diagnostics: function() {
774
            return this.get("diagnostics") && this.get("diagnostics").length;
775
        },
776

    
777
        get_progress_info: function() {
778
            // details about progress message
779
            // contains a list of diagnostic messages
780
            return this.get("status_messages");
781
        },
782

    
783
        get_status_message: function() {
784
            return this.get('status_message');
785
        },
786
        
787
        // extract status message from diagnostics
788
        status_message_from_diagnostics: function(diagnostics) {
789
            var valid_sources_map = synnefo.config.diagnostics_status_messages_map;
790
            var valid_sources = valid_sources_map[this.get('status')];
791
            if (!valid_sources) { return null };
792
            
793
            // filter messsages based on diagnostic source
794
            var messages = _.filter(diagnostics, function(diag) {
795
                return valid_sources.indexOf(diag.source) > -1;
796
            });
797

    
798
            var msg = messages[0];
799
            if (msg) {
800
              var message = msg.message;
801
              var message_tpl = snf.config.diagnostic_messages_tpls[msg.source];
802

    
803
              if (message_tpl) {
804
                  message = message_tpl.replace('MESSAGE', msg.message);
805
              }
806
              return message;
807
            }
808
            
809
            // no message to display, but vm in build state, display
810
            // finalizing message.
811
            if (this.is_building() == 'BUILD') {
812
                return synnefo.config.BUILDING_MESSAGES['FINAL'];
813
            }
814
            return null;
815
        },
816

    
817
        update_status_message: function(force) {
818
            // update only if one of the specified attributes has changed
819
            if (
820
              !this.keysChanged(['diagnostics', 'progress', 'status', 'state'])
821
                && !force
822
            ) { return };
823
            
824
            // if user requested to destroy the vm set the appropriate 
825
            // message.
826
            if (this.get('state') == "DESTROY") { 
827
                message = "Terminating..."
828
                this.set({status_message: message})
829
                return;
830
            }
831
            
832
            // set error message, if vm has diagnostic message display it as
833
            // progress message
834
            if (this.in_error_state()) {
835
                var d = this.get('diagnostics');
836
                if (d && d.length) {
837
                    var message = this.status_message_from_diagnostics(d);
838
                    this.set({status_message: message});
839
                } else {
840
                    this.set({status_message: null});
841
                }
842
                return;
843
            }
844
            
845
            // identify building status message
846
            if (this.is_building()) {
847
                var self = this;
848
                var success = function(msg) {
849
                    self.set({status_message: msg});
850
                }
851
                this.get_building_status_message(success);
852
                return;
853
            }
854

    
855
            this.set({status_message:null});
856
        },
857
            
858
        // get building status message. Asynchronous function since it requires
859
        // access to vm image.
860
        get_building_status_message: function(callback) {
861
            // no progress is set, vm is in initial build status
862
            var progress = this.get("progress");
863
            if (progress == 0 || !progress) {
864
                return callback(BUILDING_MESSAGES['INIT']);
865
            }
866
            
867
            // vm has copy progress, display copy percentage
868
            if (progress > 0 && progress <= 99) {
869
                this.get_copy_details(true, undefined, _.bind(
870
                    function(details){
871
                        callback(BUILDING_MESSAGES['COPY'].format(details.copy, 
872
                                                           details.size, 
873
                                                           details.progress));
874
                }, this));
875
                return;
876
            }
877

    
878
            // copy finished display FINAL message or identify status message
879
            // from diagnostics.
880
            if (progress >= 100) {
881
                if (!this.has_diagnostics()) {
882
                        callback(BUILDING_MESSAGES['FINAL']);
883
                } else {
884
                        var d = this.get("diagnostics");
885
                        var msg = this.status_message_from_diagnostics(d);
886
                        if (msg) {
887
                              callback(msg);
888
                        }
889
                }
890
            }
891
        },
892

    
893
        get_copy_details: function(human, image, callback) {
894
            var human = human || false;
895
            var image = image || this.get_image(_.bind(function(image){
896
                var progress = this.get('progress');
897
                var size = image.get_size();
898
                var size_copied = (size * progress / 100).toFixed(2);
899
                
900
                if (human) {
901
                    size = util.readablizeBytes(size*1024*1024);
902
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
903
                }
904

    
905
                callback({'progress': progress, 'size': size, 'copy': size_copied})
906
            }, this));
907
        },
908

    
909
        start_stats_update: function(force_if_empty) {
910
            var prev_state = this.do_update_stats;
911

    
912
            this.do_update_stats = true;
913
            
914
            // fetcher initialized ??
915
            if (!this.stats_fetcher) {
916
                this.init_stats_intervals();
917
            }
918

    
919

    
920
            // fetcher running ???
921
            if (!this.stats_fetcher.running || !prev_state) {
922
                this.stats_fetcher.start();
923
            }
924

    
925
            if (force_if_empty && this.get("stats") == undefined) {
926
                this.update_stats(true);
927
            }
928
        },
929

    
930
        stop_stats_update: function(stop_calls) {
931
            this.do_update_stats = false;
932

    
933
            if (stop_calls) {
934
                this.stats_fetcher.stop();
935
            }
936
        },
937

    
938
        // clear and reinitialize update interval
939
        init_stats_intervals: function (interval) {
940
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
941
            this.stats_fetcher.start();
942
        },
943
        
944
        get_stats_fetcher: function(timeout) {
945
            var cb = _.bind(function(data){
946
                this.update_stats();
947
            }, this);
948
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
949
            return fetcher;
950
        },
951

    
952
        // do the api call
953
        update_stats: function(force) {
954
            // do not update stats if flag not set
955
            if ((!this.do_update_stats && !force) || this.updating_stats) {
956
                return;
957
            }
958

    
959
            // make the api call, execute handle_stats_update on sucess
960
            // TODO: onError handler ???
961
            stats_url = this.url() + "/stats";
962
            this.updating_stats = true;
963
            this.sync("read", this, {
964
                handles_error:true, 
965
                url: stats_url, 
966
                refresh:true, 
967
                success: _.bind(this.handle_stats_update, this),
968
                error: _.bind(this.handle_stats_error, this),
969
                complete: _.bind(function(){this.updating_stats = false;}, this),
970
                critical: false,
971
                log_error: false,
972
                skips_timeouts: true
973
            });
974
        },
975

    
976
        get_attachment: function(id) {
977
          var attachment = undefined;
978
          _.each(this.get("attachments"), function(a) {
979
            if (a.id == id) {
980
              attachment = a;
981
            }
982
          });
983
          return attachment
984
        },
985

    
986
        _set_stats: function(stats) {
987
            var silent = silent === undefined ? false : silent;
988
            // unavailable stats while building
989
            if (this.get("status") == "BUILD") { 
990
                this.stats_available = false;
991
            } else { this.stats_available = true; }
992

    
993
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
994
            
995
            this.set({stats: stats}, {silent:true});
996
            this.trigger("stats:update", stats);
997
        },
998

    
999
        unbind: function() {
1000
            models.VM.__super__.unbind.apply(this, arguments);
1001
        },
1002

    
1003
        can_resize: function() {
1004
          return this.get('status') == 'STOPPED';
1005
        },
1006

    
1007
        handle_stats_error: function() {
1008
            stats = {};
1009
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1010
                stats[k] = false;
1011
            });
1012

    
1013
            this.set({'stats': stats});
1014
        },
1015

    
1016
        // this method gets executed after a successful vm stats api call
1017
        handle_stats_update: function(data) {
1018
            var self = this;
1019
            // avoid browser caching
1020
            
1021
            if (data.stats && _.size(data.stats) > 0) {
1022
                var ts = $.now();
1023
                var stats = data.stats;
1024
                var images_loaded = 0;
1025
                var images = {};
1026

    
1027
                function check_images_loaded() {
1028
                    images_loaded++;
1029

    
1030
                    if (images_loaded == 4) {
1031
                        self._set_stats(images);
1032
                    }
1033
                }
1034
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1035
                    
1036
                    stats[k] = stats[k] + "?_=" + ts;
1037
                    
1038
                    var stat = k.slice(0,3);
1039
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
1040
                    var img = $("<img />");
1041
                    var val = stats[k];
1042
                    
1043
                    // load stat image to a temporary dom element
1044
                    // update model stats on image load/error events
1045
                    img.load(function() {
1046
                        images[k] = val;
1047
                        check_images_loaded();
1048
                    });
1049

    
1050
                    img.error(function() {
1051
                        images[stat + type] = false;
1052
                        check_images_loaded();
1053
                    });
1054

    
1055
                    img.attr({'src': stats[k]});
1056
                })
1057
                data.stats = stats;
1058
            }
1059

    
1060
            // do we need to change the interval ??
1061
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
1062
                this.stats_update_interval = data.stats.refresh * 1000;
1063
                this.stats_fetcher.interval = this.stats_update_interval;
1064
                this.stats_fetcher.maximum_interval = this.stats_update_interval;
1065
                this.stats_fetcher.stop();
1066
                this.stats_fetcher.start(false);
1067
            }
1068
        },
1069

    
1070
        // helper method that sets the do_update_stats
1071
        // in the future this method could also make an api call
1072
        // immediaetly if needed
1073
        enable_stats_update: function() {
1074
            this.do_update_stats = true;
1075
        },
1076
        
1077
        handle_destroy: function() {
1078
            this.stats_fetcher.stop();
1079
        },
1080

    
1081
        require_reboot: function() {
1082
            if (this.is_active()) {
1083
                this.set({'reboot_required': true});
1084
            }
1085
        },
1086
        
1087
        set_pending_action: function(data) {
1088
            this.pending_action = data;
1089
            return data;
1090
        },
1091

    
1092
        // machine has pending action
1093
        update_pending_action: function(action, force) {
1094
            this.set({pending_action: action});
1095
        },
1096

    
1097
        clear_pending_action: function() {
1098
            this.set({pending_action: undefined});
1099
        },
1100

    
1101
        has_pending_action: function() {
1102
            return this.get("pending_action") ? this.get("pending_action") : false;
1103
        },
1104
        
1105
        // machine is active
1106
        is_active: function() {
1107
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
1108
        },
1109
        
1110
        // machine is building 
1111
        is_building: function() {
1112
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
1113
        },
1114
        
1115
        is_rebooting: function() {
1116
            return this.state() == 'REBOOT';
1117
        },
1118

    
1119
        in_error_state: function() {
1120
            return this.state() === "ERROR"
1121
        },
1122

    
1123
        // user can connect to machine
1124
        is_connectable: function() {
1125
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1126
        },
1127
        
1128
        remove_meta: function(key, complete, error) {
1129
            var url = this.api_path() + "/metadata/" + key;
1130
            this.api_call(url, "delete", undefined, complete, error);
1131
        },
1132

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

    
1144
            this.api_call(url, "update", payload, complete, error);
1145
        },
1146

    
1147

    
1148
        // update/get the state of the machine
1149
        state: function() {
1150
            var args = slice.call(arguments);
1151
                
1152
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1153
                this.set({'state': args[0]});
1154
            }
1155

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

    
1206
        get_resize_flavors: function() {
1207
          var vm_flavor = this.get_flavor();
1208
          var flavors = synnefo.storage.flavors.filter(function(f){
1209
              return f.get('disk_template') ==
1210
              vm_flavor.get('disk_template') && f.get('disk') ==
1211
              vm_flavor.get('disk');
1212
          });
1213
          return flavors;
1214
        },
1215

    
1216
        get_flavor_quotas: function() {
1217
          var flavor = this.get_flavor();
1218
          return {
1219
            cpu: flavor.get('cpu') + 1, 
1220
            ram: flavor.get_ram_size() + 1, 
1221
            disk:flavor.get_disk_size() + 1
1222
          }
1223
        },
1224

    
1225
        get_meta: function(key, deflt) {
1226
            if (this.get('metadata') && this.get('metadata')) {
1227
                if (!this.get('metadata')[key]) { return deflt }
1228
                return _.escape(this.get('metadata')[key]);
1229
            } else {
1230
                return deflt;
1231
            }
1232
        },
1233

    
1234
        get_meta_keys: function() {
1235
            if (this.get('metadata') && this.get('metadata')) {
1236
                return _.keys(this.get('metadata'));
1237
            } else {
1238
                return [];
1239
            }
1240
        },
1241
        
1242
        // get metadata OS value
1243
        get_os: function() {
1244
            var image = this.get_image();
1245
            return this.get_meta('OS') || (image ? 
1246
                                            image.get_os() || "okeanos" : "okeanos");
1247
        },
1248

    
1249
        get_gui: function() {
1250
            return this.get_meta('GUI');
1251
        },
1252
        
1253
        get_hostname: function() {
1254
          var hostname = this.get_meta('hostname');
1255
          if (!hostname) {
1256
            if (synnefo.config.vm_hostname_format) {
1257
              hostname = synnefo.config.vm_hostname_format.format(this.id);
1258
            } else {
1259
              // TODO: resolve public ip
1260
              hostname = 'PUBLIC NIC';
1261
            }
1262
          }
1263
          return hostname;
1264
        },
1265

    
1266
        // get actions that the user can execute
1267
        // depending on the vm state/status
1268
        get_available_actions: function() {
1269
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1270
        },
1271

    
1272
        set_profile: function(profile, net_id) {
1273
        },
1274
        
1275
        // call rename api
1276
        rename: function(new_name) {
1277
            //this.set({'name': new_name});
1278
            this.sync("update", this, {
1279
                critical: true,
1280
                data: {
1281
                    'server': {
1282
                        'name': new_name
1283
                    }
1284
                }, 
1285
                success: _.bind(function(){
1286
                    snf.api.trigger("call");
1287
                }, this)
1288
            });
1289
        },
1290
        
1291
        get_console_url: function(data) {
1292
            var url_params = {
1293
                machine: this.get("name"),
1294
                host_ip: this.get_hostname(),
1295
                host_ip_v6: this.get_hostname(),
1296
                host: data.host,
1297
                port: data.port,
1298
                password: data.password
1299
            }
1300
            return synnefo.config.ui_console_url + '?' + $.param(url_params);
1301
        },
1302
      
1303
        connect_floating_ip: function(ip, cb) {
1304
          // TODO: Implement
1305
        },
1306

    
1307
        // action helper
1308
        call: function(action_name, success, error, params) {
1309
            var id_param = [this.id];
1310
            
1311
            params = params || {};
1312
            success = success || function() {};
1313
            error = error || function() {};
1314

    
1315
            var self = this;
1316

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

    
1373
                                         },  
1374
                                         error, 'destroy', params);
1375
                    break;
1376
                case 'resize':
1377
                    this.__make_api_call(this.get_action_url(), // vm actions url
1378
                                         "create", // create so that sync later uses POST to make the call
1379
                                         {resize: {flavorRef:params.flavor}}, // payload
1380
                                         function() {
1381
                                             self.state('RESIZE');
1382
                                             success.apply(this, arguments);
1383
                                             snf.api.trigger("call");
1384
                                         },  
1385
                                         error, 'resize', params);
1386
                    break;
1387
                case 'addFloatingIp':
1388
                    this.__make_api_call(this.get_action_url(), // vm actions url
1389
                                         "create", // create so that sync later uses POST to make the call
1390
                                         {addFloatingIp: {address:params.address}}, // payload
1391
                                         function() {
1392
                                             self.state('CONNECT');
1393
                                             success.apply(this, arguments);
1394
                                             snf.api.trigger("call");
1395
                                         },  
1396
                                         error, 'addFloatingIp', params);
1397
                    break;
1398
                case 'removeFloatingIp':
1399
                    this.__make_api_call(this.get_action_url(), // vm actions url
1400
                                         "create", // create so that sync later uses POST to make the call
1401
                                         {removeFloatingIp: {address:params.address}}, // payload
1402
                                         function() {
1403
                                             self.state('DISCONNECT');
1404
                                             success.apply(this, arguments);
1405
                                             snf.api.trigger("call");
1406
                                         },  
1407
                                         error, 'addFloatingIp', params);
1408
                    break;
1409
                case 'destroy':
1410
                    this.__make_api_call(this.url(), // vm actions url
1411
                                         "delete", // create so that sync later uses POST to make the call
1412
                                         undefined, // payload
1413
                                         function() {
1414
                                             // set state after successful call
1415
                                             self.state('DESTROY');
1416
                                             success.apply(this, arguments);
1417
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1418

    
1419
                                         },  
1420
                                         error, 'destroy', params);
1421
                    break;
1422
                default:
1423
                    throw "Invalid VM action ("+action_name+")";
1424
            }
1425
        },
1426
        
1427
        __make_api_call: function(url, method, data, success, error, action, 
1428
                                  extra_params) {
1429
            var self = this;
1430
            error = error || function(){};
1431
            success = success || function(){};
1432

    
1433
            var params = {
1434
                url: url,
1435
                data: data,
1436
                success: function() { 
1437
                  self.handle_action_succeed.apply(self, arguments); 
1438
                  success.apply(this, arguments)
1439
                },
1440
                error: function() { 
1441
                  self.handle_action_fail.apply(self, arguments);
1442
                  error.apply(this, arguments)
1443
                },
1444
                error_params: { ns: "Machines actions", 
1445
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1446
                                extra_details: {
1447
                                  'Machine ID': this.id, 
1448
                                  'URL': url, 
1449
                                  'Action': action || "undefined" },
1450
                                allow_reload: false
1451
                              },
1452
                display: false,
1453
                critical: false
1454
            }
1455
            _.extend(params, extra_params);
1456
            this.sync(method, this, params);
1457
        },
1458

    
1459
        handle_action_succeed: function() {
1460
            this.trigger("action:success", arguments);
1461
        },
1462
        
1463
        reset_action_error: function() {
1464
            this.action_error = false;
1465
            this.trigger("action:fail:reset", this.action_error);
1466
        },
1467

    
1468
        handle_action_fail: function() {
1469
            this.action_error = arguments;
1470
            this.trigger("action:fail", arguments);
1471
        },
1472

    
1473
        get_action_url: function(name) {
1474
            return this.url() + "/action";
1475
        },
1476

    
1477
        get_diagnostics_url: function() {
1478
            return this.url() + "/diagnostics";
1479
        },
1480

    
1481
        get_users: function() {
1482
            var image;
1483
            var users = [];
1484
            try {
1485
              var users = this.get_meta('users').split(" ");
1486
            } catch (err) { users = null }
1487
            if (!users) {
1488
              image = this.get_image();
1489
              if (image) {
1490
                  users = image.get_created_users();
1491
              }
1492
            }
1493
            return users;
1494
        },
1495

    
1496
        get_connection_info: function(host_os, success, error) {
1497
            var url = synnefo.config.ui_connect_url;
1498
            var users = this.get_users();
1499

    
1500
            params = {
1501
                ip_address: this.get_hostname(),
1502
                hostname: this.get_hostname(),
1503
                os: this.get_os(),
1504
                host_os: host_os,
1505
                srv: this.id
1506
            }
1507
            
1508
            if (users.length) { 
1509
                params['username'] = _.last(users)
1510
            }
1511

    
1512
            url = url + "?" + $.param(params);
1513

    
1514
            var ajax = snf.api.sync("read", undefined, { url: url, 
1515
                                                         error:error, 
1516
                                                         success:success, 
1517
                                                         handles_error:1});
1518
        }
1519
    });
1520
    
1521
    models.VM.ACTIONS = [
1522
        'start',
1523
        'shutdown',
1524
        'reboot',
1525
        'console',
1526
        'destroy',
1527
        'resize'
1528
    ]
1529

    
1530
    models.VM.TASK_STATE_STATUS_MAP = {
1531
      'BULDING': 'BUILD',
1532
      'REBOOTING': 'REBOOT',
1533
      'STOPPING': 'SHUTDOWN',
1534
      'STARTING': 'START',
1535
      'RESIZING': 'RESIZE',
1536
      'CONNECTING': 'CONNECT',
1537
      'DISCONNECTING': 'DISCONNECT',
1538
      'DESTROYING': 'DESTROY'
1539
    }
1540

    
1541
    models.VM.AVAILABLE_ACTIONS = {
1542
        'UNKNWON'       : ['destroy'],
1543
        'BUILD'         : ['destroy'],
1544
        'REBOOT'        : ['destroy'],
1545
        'STOPPED'       : ['start', 'destroy'],
1546
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1547
        'ERROR'         : ['destroy'],
1548
        'DELETED'       : ['destroy'],
1549
        'DESTROY'       : ['destroy'],
1550
        'SHUTDOWN'      : ['destroy'],
1551
        'START'         : ['destroy'],
1552
        'CONNECT'       : ['destroy'],
1553
        'DISCONNECT'    : ['destroy'],
1554
        'RESIZE'        : ['destroy']
1555
    }
1556

    
1557
    // api status values
1558
    models.VM.STATUSES = [
1559
        'UNKNWON',
1560
        'BUILD',
1561
        'REBOOT',
1562
        'STOPPED',
1563
        'ACTIVE',
1564
        'ERROR',
1565
        'DELETED',
1566
        'RESIZE'
1567
    ]
1568

    
1569
    // api status values
1570
    models.VM.CONNECT_STATES = [
1571
        'ACTIVE',
1572
        'REBOOT',
1573
        'SHUTDOWN'
1574
    ]
1575

    
1576
    // vm states
1577
    models.VM.STATES = models.VM.STATUSES.concat([
1578
        'DESTROY',
1579
        'SHUTDOWN',
1580
        'START',
1581
        'CONNECT',
1582
        'DISCONNECT',
1583
        'FIREWALL',
1584
        'RESIZE'
1585
    ]);
1586
    
1587
    models.VM.STATES_TRANSITIONS = {
1588
        'DESTROY' : ['DELETED'],
1589
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1590
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1591
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1592
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1593
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1594
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1595
        'RESIZE': ['ERROR', 'STOPPED']
1596
    }
1597

    
1598
    models.VM.TRANSITION_STATES = [
1599
        'DESTROY',
1600
        'SHUTDOWN',
1601
        'START',
1602
        'REBOOT',
1603
        'BUILD',
1604
        'RESIZE'
1605
    ]
1606

    
1607
    models.VM.ACTIVE_STATES = [
1608
        'BUILD', 'REBOOT', 'ACTIVE',
1609
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1610
    ]
1611

    
1612
    models.VM.BUILDING_STATES = [
1613
        'BUILD'
1614
    ]
1615

    
1616
    models.Images = models.Collection.extend({
1617
        model: models.Image,
1618
        path: 'images',
1619
        details: true,
1620
        noUpdate: true,
1621
        supportIncUpdates: false,
1622
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1623
        meta_labels: {},
1624
        read_method: 'read',
1625

    
1626
        // update collection model with id passed
1627
        // making a direct call to the image
1628
        // api url
1629
        update_unknown_id: function(id, callback) {
1630
            var url = getUrl.call(this) + "/" + id;
1631
            this.api_call(this.path + "/" + id, this.read_method, {
1632
              _options:{
1633
                async:true, 
1634
                skip_api_error:true}
1635
              }, undefined, 
1636
            _.bind(function() {
1637
                if (!this.get(id)) {
1638
                            if (this.fallback_service) {
1639
                        // if current service has fallback_service attribute set
1640
                        // use this service to retrieve the missing image model
1641
                        var tmpservice = new this.fallback_service();
1642
                        tmpservice.update_unknown_id(id, _.bind(function(img){
1643
                            img.attributes.status = "DELETED";
1644
                            this.add(img.attributes);
1645
                            callback(this.get(id));
1646
                        }, this));
1647
                    } else {
1648
                        var title = synnefo.config.image_deleted_title || 'Deleted';
1649
                        // else add a dummy DELETED state image entry
1650
                        this.add({id:id, name:title, size:-1, 
1651
                                  progress:100, status:"DELETED"});
1652
                        callback(this.get(id));
1653
                    }   
1654
                } else {
1655
                    callback(this.get(id));
1656
                }
1657
            }, this), _.bind(function(image, msg, xhr) {
1658
                if (!image) {
1659
                    var title = synnefo.config.image_deleted_title || 'Deleted';
1660
                    this.add({id:id, name:title, size:-1, 
1661
                              progress:100, status:"DELETED"});
1662
                    callback(this.get(id));
1663
                    return;
1664
                }
1665
                var img_data = this._read_image_from_request(image, msg, xhr);
1666
                this.add(img_data);
1667
                callback(this.get(id));
1668
            }, this));
1669
        },
1670

    
1671
        _read_image_from_request: function(image, msg, xhr) {
1672
            return image.image;
1673
        },
1674

    
1675
        parse: function (resp, xhr) {
1676
            var parsed = _.map(resp.images, _.bind(this.parse_meta, this));
1677
            parsed = this.fill_owners(parsed);
1678
            return parsed;
1679
        },
1680

    
1681
        fill_owners: function(images) {
1682
            // do translate uuid->displayname if needed
1683
            // store display name in owner attribute for compatibility
1684
            var uuids = [];
1685

    
1686
            var images = _.map(images, function(img, index) {
1687
                if (synnefo.config.translate_uuids) {
1688
                    uuids.push(img['owner']);
1689
                }
1690
                img['owner_uuid'] = img['owner'];
1691
                return img;
1692
            });
1693
            
1694
            if (uuids.length > 0) {
1695
                var handle_results = function(data) {
1696
                    _.each(images, function (img) {
1697
                        img['owner'] = data.uuid_catalog[img['owner_uuid']];
1698
                    });
1699
                }
1700
                // notice the async false
1701
                var uuid_map = this.translate_uuids(uuids, false, 
1702
                                                    handle_results)
1703
            }
1704
            return images;
1705
        },
1706

    
1707
        translate_uuids: function(uuids, async, cb) {
1708
            var url = synnefo.config.user_catalog_url;
1709
            var data = JSON.stringify({'uuids': uuids});
1710
          
1711
            // post to user_catalogs api
1712
            snf.api.sync('create', undefined, {
1713
                url: url,
1714
                data: data,
1715
                async: async,
1716
                success:  cb
1717
            });
1718
        },
1719

    
1720
        get_meta_key: function(img, key) {
1721
            if (img.metadata && img.metadata && img.metadata[key]) {
1722
                return _.escape(img.metadata[key]);
1723
            }
1724
            return undefined;
1725
        },
1726

    
1727
        comparator: function(img) {
1728
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1729
        },
1730

    
1731
        parse_meta: function(img) {
1732
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1733
                if (img[key]) { return };
1734
                img[key] = this.get_meta_key(img, key) || "";
1735
            }, this));
1736
            return img;
1737
        },
1738

    
1739
        active: function() {
1740
            return this.filter(function(img){return img.get('status') != "DELETED"});
1741
        },
1742

    
1743
        predefined: function() {
1744
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
1745
        },
1746
        
1747
        fetch_for_type: function(type, complete, error) {
1748
            this.fetch({update:true, 
1749
                        success: complete, 
1750
                        error: error, 
1751
                        skip_api_error: true });
1752
        },
1753
        
1754
        get_images_for_type: function(type) {
1755
            if (this['get_{0}_images'.format(type)]) {
1756
                return this['get_{0}_images'.format(type)]();
1757
            }
1758

    
1759
            return this.active();
1760
        },
1761

    
1762
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
1763
            var load = false;
1764
            error = onError || function() {};
1765
            function complete(collection) { 
1766
                onComplete(collection.get_images_for_type(type)); 
1767
            }
1768
            
1769
            // do we need to fetch/update current collection entries
1770
            if (load) {
1771
                onStart();
1772
                this.fetch_for_type(type, complete, error);
1773
            } else {
1774
                // fallback to complete
1775
                complete(this);
1776
            }
1777
        }
1778
    })
1779

    
1780
    models.Flavors = models.Collection.extend({
1781
        model: models.Flavor,
1782
        path: 'flavors',
1783
        details: true,
1784
        noUpdate: true,
1785
        supportIncUpdates: false,
1786
        // update collection model with id passed
1787
        // making a direct call to the flavor
1788
        // api url
1789
        update_unknown_id: function(id, callback) {
1790
            var url = getUrl.call(this) + "/" + id;
1791
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
1792
            _.bind(function() {
1793
                this.add({id:id, cpu:"Unknown", ram:"Unknown", disk:"Unknown", name: "Unknown", status:"DELETED"})
1794
            }, this), _.bind(function(flv) {
1795
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1796
                this.add(flv.flavor);
1797
            }, this));
1798
        },
1799

    
1800
        parse: function (resp, xhr) {
1801
            return _.map(resp.flavors, function(o) {
1802
              o.cpu = o['vcpus'];
1803
              o.disk_template = o['SNF:disk_template'];
1804
              return o
1805
            });
1806
        },
1807

    
1808
        comparator: function(flv) {
1809
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1810
        },
1811
          
1812
        unavailable_values_for_quotas: function(quotas, flavors, extra) {
1813
            var flavors = flavors || this.active();
1814
            var index = {cpu:[], disk:[], ram:[]};
1815
            var extra = extra == undefined ? {cpu:0, disk:0, ram:0} : extra;
1816
            
1817
            _.each(flavors, function(el) {
1818

    
1819
                var disk_available = quotas['disk'] + extra.disk;
1820
                var disk_size = el.get_disk_size();
1821
                if (index.disk.indexOf(disk_size) == -1) {
1822
                  var disk = el.disk_to_bytes();
1823
                  if (disk > disk_available) {
1824
                    index.disk.push(disk_size);
1825
                  }
1826
                }
1827
                
1828
                var ram_available = quotas['ram'] + extra.ram * 1024 * 1024;
1829
                var ram_size = el.get_ram_size();
1830
                if (index.ram.indexOf(ram_size) == -1) {
1831
                  var ram = el.ram_to_bytes();
1832
                  if (ram > ram_available) {
1833
                    index.ram.push(el.get('ram'))
1834
                  }
1835
                }
1836

    
1837
                var cpu = el.get('cpu');
1838
                var cpu_available = quotas['cpu'] + extra.cpu;
1839
                if (index.cpu.indexOf(cpu) == -1) {
1840
                  if (cpu > cpu_available) {
1841
                    index.cpu.push(el.get('cpu'))
1842
                  }
1843
                }
1844
            });
1845
            return index;
1846
        },
1847

    
1848
        unavailable_values_for_image: function(img, flavors) {
1849
            var flavors = flavors || this.active();
1850
            var size = img.get_size();
1851
            
1852
            var index = {cpu:[], disk:[], ram:[]};
1853

    
1854
            _.each(this.active(), function(el) {
1855
                var img_size = size;
1856
                var flv_size = el.get_disk_size();
1857
                if (flv_size < img_size) {
1858
                    if (index.disk.indexOf(flv_size) == -1) {
1859
                        index.disk.push(flv_size);
1860
                    }
1861
                };
1862
            });
1863
            
1864
            return index;
1865
        },
1866

    
1867
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
1868
            if (!filter_list) { filter_list = this.models };
1869
            
1870
            return this.select(function(flv){
1871
                if (flv.get("cpu") == cpu + "" &&
1872
                   flv.get("ram") == mem + "" &&
1873
                   flv.get("disk") == disk + "" &&
1874
                   flv.get("disk_template") == disk_template &&
1875
                   filter_list.indexOf(flv) > -1) { return true; }
1876
            })[0];
1877
        },
1878
        
1879
        get_data: function(lst) {
1880
            var data = {'cpu': [], 'mem':[], 'disk':[], 'disk_template':[]};
1881

    
1882
            _.each(lst, function(flv) {
1883
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1884
                    data.cpu.push(flv.get("cpu"));
1885
                }
1886
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1887
                    data.mem.push(flv.get("ram"));
1888
                }
1889
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1890
                    data.disk.push(flv.get("disk"));
1891
                }
1892
                if (data.disk_template.indexOf(flv.get("disk_template")) == -1) {
1893
                    data.disk_template.push(flv.get("disk_template"));
1894
                }
1895
            })
1896
            
1897
            return data;
1898
        },
1899

    
1900
        active: function() {
1901
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1902
        }
1903
            
1904
    })
1905

    
1906
    models.VMS = models.Collection.extend({
1907
        model: models.VM,
1908
        path: 'servers',
1909
        details: true,
1910
        copy_image_meta: true,
1911

    
1912
        parse: function (resp, xhr) {
1913
            var data = resp;
1914
            if (!resp) { return [] };
1915
            data = _.filter(_.map(resp.servers, 
1916
                                  _.bind(this.parse_vm_api_data, this)), 
1917
                                  function(v){return v});
1918
            return data;
1919
        },
1920

    
1921
        parse_vm_api_data: function(data) {
1922
            // do not add non existing DELETED entries
1923
            if (data.status && data.status == "DELETED") {
1924
                if (!this.get(data.id)) {
1925
                    return false;
1926
                }
1927
            }
1928
            
1929
            if ('SNF:task_state' in data) { 
1930
                data['task_state'] = data['SNF:task_state'];
1931
                if (data['task_state']) {
1932
                    var status = models.VM.TASK_STATE_STATUS_MAP[data['task_state']];
1933
                    if (status) { data['status'] = status }
1934
                }
1935
            }
1936

    
1937
            // OS attribute
1938
            if (this.has_meta(data)) {
1939
                data['OS'] = data.metadata.OS || snf.config.unknown_os;
1940
            }
1941
            
1942
            if (!data.diagnostics) {
1943
                data.diagnostics = [];
1944
            }
1945

    
1946
            // network metadata
1947
            data['firewalls'] = {};
1948

    
1949
            data['fqdn'] = synnefo.config.vm_hostname_format.format(data['id']);
1950

    
1951
            // if vm has no metadata, no metadata object
1952
            // is in json response, reset it to force
1953
            // value update
1954
            if (!data['metadata']) {
1955
                data['metadata'] = {};
1956
            }
1957
            
1958
            // v2.0 API returns objects
1959
            data.image_obj = data.image;
1960
            data.image = data.image_obj.id;
1961
            data.flavor_obj = data.flavor;
1962
            data.flavor = data.flavor_obj.id;
1963

    
1964
            return data;
1965
        },
1966

    
1967
        get_reboot_required: function() {
1968
            return this.filter(function(vm){return vm.get("reboot_required") == true})
1969
        },
1970

    
1971
        has_pending_actions: function() {
1972
            return this.filter(function(vm){return vm.pending_action}).length > 0;
1973
        },
1974

    
1975
        reset_pending_actions: function() {
1976
            this.each(function(vm) {
1977
                vm.clear_pending_action();
1978
            })
1979
        },
1980

    
1981
        do_all_pending_actions: function(success, error) {
1982
            this.each(function(vm) {
1983
                if (vm.has_pending_action()) {
1984
                    vm.call(vm.pending_action, success, error);
1985
                    vm.clear_pending_action();
1986
                }
1987
            })
1988
        },
1989
        
1990
        do_all_reboots: function(success, error) {
1991
            this.each(function(vm) {
1992
                if (vm.get("reboot_required")) {
1993
                    vm.call("reboot", success, error);
1994
                }
1995
            });
1996
        },
1997

    
1998
        reset_reboot_required: function() {
1999
            this.each(function(vm) {
2000
                vm.set({'reboot_required': undefined});
2001
            })
2002
        },
2003
        
2004
        stop_stats_update: function(exclude) {
2005
            var exclude = exclude || [];
2006
            this.each(function(vm) {
2007
                if (exclude.indexOf(vm) > -1) {
2008
                    return;
2009
                }
2010
                vm.stop_stats_update();
2011
            })
2012
        },
2013
        
2014
        has_meta: function(vm_data) {
2015
            return vm_data.metadata && vm_data.metadata
2016
        },
2017

    
2018
        has_addresses: function(vm_data) {
2019
            return vm_data.metadata && vm_data.metadata
2020
        },
2021

    
2022
        create: function (name, image, flavor, meta, extra, callback) {
2023

    
2024
            if (this.copy_image_meta) {
2025
                if (synnefo.config.vm_image_common_metadata) {
2026
                    _.each(synnefo.config.vm_image_common_metadata, 
2027
                        function(key){
2028
                            if (image.get_meta(key)) {
2029
                                meta[key] = image.get_meta(key);
2030
                            }
2031
                    });
2032
                }
2033

    
2034
                if (image.get("OS")) {
2035
                    meta['OS'] = image.get("OS");
2036
                }
2037
            }
2038
            
2039
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, 
2040
                    metadata:meta}
2041
            opts = _.extend(opts, extra);
2042
            
2043
            var cb = function(data) {
2044
              synnefo.storage.quotas.get('cyclades.vm').increase();
2045
              callback(data);
2046
            }
2047

    
2048
            this.api_call(this.path, "create", {'server': opts}, undefined, 
2049
                          undefined, cb, {critical: true});
2050
        },
2051

    
2052
        load_missing_images: function(callback) {
2053
          var missing_ids = [];
2054
          var resolved = 0;
2055

    
2056
          // fill missing_ids
2057
          this.each(function(el) {
2058
            var imgid = el.get("image");
2059
            var existing = synnefo.storage.images.get(imgid);
2060
            if (!existing && missing_ids.indexOf(imgid) == -1) {
2061
              missing_ids.push(imgid);
2062
            }
2063
          });
2064
          var check = function() {
2065
            // once all missing ids where resolved continue calling the 
2066
            // callback
2067
            resolved++;
2068
            if (resolved == missing_ids.length) {
2069
              callback(missing_ids)
2070
            }
2071
          }
2072
          if (missing_ids.length == 0) {
2073
            callback(missing_ids);
2074
            return;
2075
          }
2076
          // start resolving missing image ids
2077
          _(missing_ids).each(function(imgid){
2078
            synnefo.storage.images.update_unknown_id(imgid, check);
2079
          });
2080
        },
2081

    
2082
        get_connectable: function() {
2083
            return storage.vms.filter(function(vm){
2084
                return !vm.in_error_state() && !vm.is_building();
2085
            });
2086
        }
2087
    })
2088
    
2089
    models.PublicKey = models.Model.extend({
2090
        path: 'keys',
2091
        api_type: 'userdata',
2092
        detail: false,
2093
        model_actions: {
2094
          'remove': [['name'], function() {
2095
            return true;
2096
          }]
2097
        },
2098

    
2099
        get_public_key: function() {
2100
            return cryptico.publicKeyFromString(this.get("content"));
2101
        },
2102

    
2103
        get_filename: function() {
2104
            return "{0}.pub".format(this.get("name"));
2105
        },
2106

    
2107
        identify_type: function() {
2108
            try {
2109
                var cont = snf.util.validatePublicKey(this.get("content"));
2110
                var type = cont.split(" ")[0];
2111
                return synnefo.util.publicKeyTypesMap[type];
2112
            } catch (err) { return false };
2113
        },
2114

    
2115
        rename: function(new_name) {
2116
          //this.set({'name': new_name});
2117
          this.sync("update", this, {
2118
            critical: true,
2119
            data: {'name': new_name}, 
2120
            success: _.bind(function(){
2121
              snf.api.trigger("call");
2122
            }, this)
2123
          });
2124
        }
2125
    })
2126
    
2127
    models._ActionsModel = models.Model.extend({
2128
      defaults: { pending: null },
2129
      actions: [],
2130
      status: {
2131
        INACTIVE: 0,
2132
        PENDING: 1,
2133
        CALLED: 2
2134
      },
2135

    
2136
      initialize: function(attrs, opts) {
2137
        models._ActionsModel.__super__.initialize.call(this, attrs);
2138
        this.actions = opts.actions;
2139
        this.model = opts.model;
2140
        this.bind("change", function() {
2141
          this.set({'pending': this.get_pending()});
2142
        }, this);
2143
        this.clear();
2144
      },
2145
      
2146
      _in_status: function(st) {
2147
        var actions = null;
2148
        _.each(this.attributes, function(status, action){
2149
          if (status == st) {
2150
            if (!actions) {
2151
              actions = []
2152
            }
2153
            actions.push(action);
2154
          }
2155
        });
2156
        return actions;
2157
      },
2158

    
2159
      get_pending: function() {
2160
        return this._in_status(this.status.PENDING);
2161
      },
2162

    
2163
      unset_pending_action: function(action) {
2164
        var data = {};
2165
        data[action] = this.status.INACTIVE;
2166
        this.set(data);
2167
      },
2168

    
2169
      set_pending_action: function(action, reset_pending) {
2170
        reset_pending = reset_pending === undefined ? true : reset_pending;
2171
        var data = {};
2172
        data[action] = this.status.PENDING;
2173
        if (reset_pending) {
2174
          this.reset_pending();
2175
        }
2176
        this.set(data);
2177
      },
2178
      
2179
      reset_pending: function() {
2180
        var data = {};
2181
        _.each(this.actions, function(action) {
2182
          data[action] = this.status.INACTIVE;
2183
        }, this);
2184
        this.set(data);
2185
      }
2186
    });
2187

    
2188
    models.PublicPool = models.Model.extend({});
2189
    models.PublicPools = models.Collection.extend({
2190
      model: models.PublicPool,
2191
      path: 'os-floating-ip-pools',
2192
      api_type: 'compute',
2193
      noUpdate: true,
2194

    
2195
      parse: function(data) {
2196
        return _.map(data.floating_ip_pools, function(pool) {
2197
          pool.id = pool.name;
2198
          return pool;
2199
        });
2200
      }
2201
    });
2202

    
2203
    models.PublicKeys = models.Collection.extend({
2204
        model: models.PublicKey,
2205
        details: false,
2206
        path: 'keys',
2207
        api_type: 'userdata',
2208
        noUpdate: true,
2209
        updateEntries: true,
2210

    
2211
        generate_new: function(success, error) {
2212
            snf.api.sync('create', undefined, {
2213
                url: getUrl.call(this, this.base_url) + "/generate", 
2214
                success: success, 
2215
                error: error,
2216
                skip_api_error: true
2217
            });
2218
        },
2219
        
2220
        add_crypto_key: function(key, success, error, options) {
2221
            var options = options || {};
2222
            var m = new models.PublicKey();
2223

    
2224
            // guess a name
2225
            var name_tpl = "my generated public key";
2226
            var name = name_tpl;
2227
            var name_count = 1;
2228
            
2229
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
2230
                name = name_tpl + " " + name_count;
2231
                name_count++;
2232
            }
2233
            
2234
            m.set({name: name});
2235
            m.set({content: key});
2236
            
2237
            options.success = function () { return success(m) };
2238
            options.errror = error;
2239
            options.skip_api_error = true;
2240
            
2241
            this.create(m.attributes, options);
2242
        }
2243
    });
2244

    
2245
  
2246
    models.Quota = models.Model.extend({
2247

    
2248
        initialize: function() {
2249
            models.Quota.__super__.initialize.apply(this, arguments);
2250
            this.bind("change", this.check, this);
2251
            this.check();
2252
        },
2253
        
2254
        check: function() {
2255
            var usage, limit;
2256
            usage = this.get('usage');
2257
            limit = this.get('limit');
2258
            if (usage >= limit) {
2259
                this.trigger("available");
2260
            } else {
2261
                this.trigger("unavailable");
2262
            }
2263
        },
2264

    
2265
        increase: function(val) {
2266
            if (val === undefined) { val = 1};
2267
            this.set({'usage': this.get('usage') + val})
2268
        },
2269

    
2270
        decrease: function(val) {
2271
            if (val === undefined) { val = 1};
2272
            this.set({'usage': this.get('usage') - val})
2273
        },
2274

    
2275
        can_consume: function() {
2276
            var usage, limit;
2277
            usage = this.get('usage');
2278
            limit = this.get('limit');
2279
            if (usage >= limit) {
2280
                return false
2281
            } else {
2282
                return true
2283
            }
2284
        },
2285
        
2286
        is_bytes: function() {
2287
            return this.get('resource').get('unit') == 'bytes';
2288
        },
2289
        
2290
        get_available: function(active) {
2291
            suffix = '';
2292
            if (active) { suffix = '_active'}
2293
            var value = this.get('limit'+suffix) - this.get('usage'+suffix);
2294
            if (active) {
2295
              if (this.get('available') <= value) {
2296
                value = this.get('available');
2297
              }
2298
            }
2299
            if (value < 0) { return value }
2300
            return value
2301
        },
2302

    
2303
        get_readable: function(key, active) {
2304
            var value;
2305
            if (key == 'available') {
2306
                value = this.get_available(active);
2307
            } else {
2308
                value = this.get(key)
2309
            }
2310
            if (value <= 0) { value = 0 }
2311
            if (!this.is_bytes()) {
2312
              return value + "";
2313
            }
2314
            return snf.util.readablizeBytes(value);
2315
        }
2316
    });
2317

    
2318
    models.Quotas = models.Collection.extend({
2319
        model: models.Quota,
2320
        api_type: 'accounts',
2321
        path: 'quotas',
2322
        parse: function(resp) {
2323
            filtered = _.map(resp.system, function(value, key) {
2324
                var available = (value.limit - value.usage) || 0;
2325
                var available_active = available;
2326
                var keysplit = key.split(".");
2327
                var limit_active = value.limit;
2328
                var usage_active = value.usage;
2329
                keysplit[keysplit.length-1] = "active_" + keysplit[keysplit.length-1];
2330
                var activekey = keysplit.join(".");
2331
                var exists = resp.system[activekey];
2332
                if (exists) {
2333
                    available_active = exists.limit - exists.usage;
2334
                    limit_active = exists.limit;
2335
                    usage_active = exists.usage;
2336
                }
2337
                return _.extend(value, {'name': key, 'id': key, 
2338
                          'available': available,
2339
                          'available_active': available_active,
2340
                          'limit_active': limit_active,
2341
                          'usage_active': usage_active,
2342
                          'resource': snf.storage.resources.get(key)});
2343
            });
2344
            return filtered;
2345
        },
2346
        
2347
        get_by_id: function(k) {
2348
          return this.filter(function(q) { return q.get('name') == k})[0]
2349
        },
2350

    
2351
        get_available_for_vm: function(options) {
2352
          var quotas = synnefo.storage.quotas;
2353
          var key = 'available';
2354
          var available_quota = {};
2355
          _.each(['cyclades.ram', 'cyclades.cpu', 'cyclades.disk'], 
2356
            function (key) {
2357
              var value = quotas.get(key).get_available(true);
2358
              available_quota[key.replace('cyclades.', '')] = value;
2359
          });
2360
          return available_quota;
2361
        }
2362
    })
2363

    
2364
    models.Resource = models.Model.extend({
2365
        api_type: 'accounts',
2366
        path: 'resources'
2367
    });
2368

    
2369
    models.Resources = models.Collection.extend({
2370
        api_type: 'accounts',
2371
        path: 'resources',
2372
        model: models.Network,
2373

    
2374
        parse: function(resp) {
2375
            return _.map(resp, function(value, key) {
2376
                return _.extend(value, {'name': key, 'id': key});
2377
            })
2378
        }
2379
    });
2380
    
2381
    // storage initialization
2382
    snf.storage.images = new models.Images();
2383
    snf.storage.flavors = new models.Flavors();
2384
    snf.storage.vms = new models.VMS();
2385
    snf.storage.keys = new models.PublicKeys();
2386
    snf.storage.resources = new models.Resources();
2387
    snf.storage.quotas = new models.Quotas();
2388
    snf.storage.public_pools = new models.PublicPools();
2389

    
2390
})(this);