Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / ui / static / snf / js / models.js @ 213a8396

History | View | Annotate | Download (82.9 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
        _bind_model: function(model, attr, check_attr, cb) {
193
          var proxy_cache_key = attr + '_' + check_attr;
194
          if (this._proxy_model_cache[proxy_cache_key]) {
195
            var proxy = this._proxy_model_cache[proxy_cache_key];
196
            proxy[0].unbind('change', proxy[1]);
197
          }
198
          var data = {};
199
          var changebind = _.bind(function() {
200
            data[attr] = cb.call(this, this.get(check_attr));
201
            this.set(data);
202
          }, this);
203
          model.bind('change', changebind);
204
          this._proxy_model_cache[proxy_cache_key] = [model, changebind];
205
        },
206

    
207
        _bind_attr: function(attr, check_attr, cb) {
208
          this.bind('change:' + check_attr, function() {
209
            if (this.get(check_attr) instanceof models.Model) {
210
              var model = this.get(check_attr);
211
              this._bind_model(model, attr, check_attr, cb);
212
            }
213
            var val = cb.call(this, this.get(check_attr));
214
            var data = {};
215
            if (this.get(attr) !== val) {
216
              data[attr] = val;
217
              this.set(data);
218
            }
219
          }, this);
220
        },
221

    
222
        _set_proxy_attr: function(attr, check_attr, cb) {
223
          // initial set
224
          var data = {};
225
          data[attr] = cb.call(this, this.get(check_attr));
226
          if (data[attr] !== undefined) {
227
            this.set(data, {silent:true});
228
          }
229
          if(this.get(check_attr) instanceof models.Model) {
230
            this._bind_model(this.get(check_attr), attr, check_attr, cb);
231
          }
232
          this._bind_attr(attr, check_attr, cb);
233
        },
234

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

    
269
        url: function(options) {
270
            return getUrl.call(this, this.base_url) + "/" + this.id;
271
        },
272

    
273
        api_path: function(options) {
274
            return this.path + "/" + this.id;
275
        },
276

    
277
        parse: function(resp, xhr) {
278
        },
279

    
280
        remove: function(complete, error, success) {
281
            this.api_call(this.api_path(), "delete", undefined, complete, error, success);
282
        },
283

    
284
        changedKeys: function() {
285
            return _.keys(this.changedAttributes() || {});
286
        },
287
            
288
        // return list of changed attributes that included in passed list
289
        // argument
290
        getKeysChanged: function(keys) {
291
            return _.intersection(keys, this.changedKeys());
292
        },
293
        
294
        // boolean check of keys changed
295
        keysChanged: function(keys) {
296
            return this.getKeysChanged(keys).length > 0;
297
        },
298

    
299
        // check if any of the passed attribues has changed
300
        hasOnlyChange: function(keys) {
301
            var ret = false;
302
            _.each(keys, _.bind(function(key) {
303
                if (this.changedKeys().length == 1 && this.changedKeys().indexOf(key) > -1) { ret = true};
304
            }, this));
305
            return ret;
306
        }
307

    
308
    })
309
    
310
    // Base object for all our model collections
311
    models.Collection = bb.Collection.extend({
312
        sync: snf.api.sync,
313
        api: snf.api,
314
        api_type: 'compute',
315
        supportIncUpdates: true,
316

    
317
        initialize: function() {
318
            models.Collection.__super__.initialize.apply(this, arguments);
319
            this.api_call = _.bind(this.api.call, this);
320
        },
321

    
322
        url: function(options, method) {
323
            return getUrl.call(this, this.base_url) + (
324
                    options.details || this.details && method != 'create' ? '/detail' : '');
325
        },
326

    
327
        fetch: function(options) {
328
            if (!options) { options = {} };
329
            // default to update
330
            if (!this.noUpdate) {
331
                if (options.update === undefined) { options.update = true };
332
                if (!options.removeMissing && options.refresh) { 
333
                  options.removeMissing = true;
334
                };
335
                // for collections which associated models don't support 
336
                // deleted state identification through attributes, resolve  
337
                // deleted entries by checking for missing objects in fetch 
338
                // responses.
339
                if (this.updateEntries && options.removeMissing === undefined) {
340
                  options.removeMissing = true;
341
                }
342
            } else {
343
                if (options.refresh === undefined) {
344
                    options.refresh = true;
345
                    if (this.updateEntries) {
346
                      options.update = true;
347
                      options.removeMissing = true;
348
                    }
349
                }
350
            }
351
            // custom event foreach fetch
352
            return bb.Collection.prototype.fetch.call(this, options)
353
        },
354

    
355
        create: function(model, options) {
356
            var coll = this;
357
            options || (options = {});
358
            model = this._prepareModel(model, options);
359
            if (!model) return false;
360
            var success = options.success;
361
            options.success = function(nextModel, resp, xhr) {
362
                if (coll.add_on_create) {
363
                  coll.add(nextModel, options);
364
                }
365
                if (success) success(nextModel, resp, xhr);
366
            };
367
            model.save(null, options);
368
            return model;
369
        },
370

    
371
        get_fetcher: function(interval, increase, fast, increase_after_calls, max, initial_call, params) {
372
            var fetch_params = params || {};
373
            var handler_options = {};
374

    
375
            fetch_params.skips_timeouts = true;
376
            handler_options.interval = interval;
377
            handler_options.increase = increase;
378
            handler_options.fast = fast;
379
            handler_options.increase_after_calls = increase_after_calls;
380
            handler_options.max= max;
381
            handler_options.id = "collection id";
382

    
383
            var last_ajax = undefined;
384
            var callback = _.bind(function() {
385
                // clone to avoid referenced objects
386
                var params = _.clone(fetch_params);
387
                updater._ajax = last_ajax;
388
                
389
                // wait for previous request to finish
390
                if (last_ajax && last_ajax.readyState < 4 && last_ajax.statusText != "timeout") {
391
                    // opera readystate for 304 responses is 0
392
                    if (!($.browser.opera && last_ajax.readyState == 0 && last_ajax.status == 304)) {
393
                        return;
394
                    }
395
                }
396
                last_ajax = this.fetch(params);
397
            }, this);
398
            handler_options.callback = callback;
399

    
400
            var updater = new snf.api.updateHandler(_.clone(_.extend(handler_options, fetch_params)));
401
            snf.api.bind("call", _.throttle(_.bind(function(){ updater.faster(true)}, this)), 1000);
402
            return updater;
403
        }
404
    });
405
    
406
    // Image model
407
    models.Image = models.Model.extend({
408
        path: 'images',
409
        
410
        get_size: function() {
411
            return parseInt(this.get('metadata') ? this.get('metadata').size : -1)
412
        },
413

    
414
        get_description: function(escape) {
415
            if (escape == undefined) { escape = true };
416
            if (escape) { return this.escape('description') || "No description available"}
417
            return this.get('description') || "No description available."
418
        },
419

    
420
        get_meta: function(key) {
421
            if (this.get('metadata') && this.get('metadata')) {
422
                if (!this.get('metadata')[key]) { return null }
423
                return _.escape(this.get('metadata')[key]);
424
            } else {
425
                return null;
426
            }
427
        },
428

    
429
        get_meta_keys: function() {
430
            if (this.get('metadata') && this.get('metadata')) {
431
                return _.keys(this.get('metadata'));
432
            } else {
433
                return [];
434
            }
435
        },
436

    
437
        get_owner: function() {
438
            return this.get('owner') || _.keys(synnefo.config.system_images_owners)[0];
439
        },
440

    
441
        get_owner_uuid: function() {
442
            return this.get('owner_uuid');
443
        },
444

    
445
        is_system_image: function() {
446
          var owner = this.get_owner();
447
          return _.include(_.keys(synnefo.config.system_images_owners), owner)
448
        },
449

    
450
        owned_by: function(user) {
451
          if (!user) { user = synnefo.user }
452
          return user.get_username() == this.get('owner_uuid');
453
        },
454

    
455
        display_owner: function() {
456
            var owner = this.get_owner();
457
            if (_.include(_.keys(synnefo.config.system_images_owners), owner)) {
458
                return synnefo.config.system_images_owners[owner];
459
            } else {
460
                return owner;
461
            }
462
        },
463
    
464
        get_readable_size: function() {
465
            if (this.is_deleted()) {
466
                return synnefo.config.image_deleted_size_title || '(none)';
467
            }
468
            return this.get_size() > 0 ? util.readablizeBytes(this.get_size() * 1024 * 1024) : '(none)';
469
        },
470

    
471
        get_os: function() {
472
            return this.get_meta('OS');
473
        },
474

    
475
        get_gui: function() {
476
            return this.get_meta('GUI');
477
        },
478

    
479
        get_created_users: function() {
480
            try {
481
              var users = this.get_meta('users').split(" ");
482
            } catch (err) { users = null }
483
            if (!users) {
484
                var osfamily = this.get_meta('osfamily');
485
                if (osfamily == 'windows') { 
486
                  users = ['Administrator'];
487
                } else {
488
                  users = ['root'];
489
                }
490
            }
491
            return users;
492
        },
493

    
494
        get_sort_order: function() {
495
            return parseInt(this.get('metadata') ? this.get('metadata').sortorder : -1)
496
        },
497

    
498
        get_vm: function() {
499
            var vm_id = this.get("serverRef");
500
            var vm = undefined;
501
            vm = storage.vms.get(vm_id);
502
            return vm;
503
        },
504

    
505
        is_public: function() {
506
            return this.get('is_public') == undefined ? true : this.get('is_public');
507
        },
508

    
509
        is_deleted: function() {
510
            return this.get('status') == "DELETED"
511
        },
512
        
513
        ssh_keys_paths: function() {
514
            return _.map(this.get_created_users(), function(username) {
515
                prepend = '';
516
                if (username != 'root') {
517
                    prepend = '/home'
518
                }
519
                return {'user': username, 'path': '{1}/{0}/.ssh/authorized_keys'.format(username, 
520
                                                             prepend)};
521
            });
522
        },
523

    
524
        _supports_ssh: function() {
525
            if (synnefo.config.support_ssh_os_list.indexOf(this.get_os()) > -1) {
526
                return true;
527
            }
528
            if (this.get_meta('osfamily') == 'linux') {
529
              return true;
530
            }
531
            return false;
532
        },
533

    
534
        supports: function(feature) {
535
            if (feature == "ssh") {
536
                return this._supports_ssh()
537
            }
538
            return false;
539
        },
540

    
541
        personality_data_for_keys: function(keys) {
542
            return _.map(this.ssh_keys_paths(), function(pathinfo) {
543
                var contents = '';
544
                _.each(keys, function(key){
545
                    contents = contents + key.get("content") + "\n"
546
                });
547
                contents = $.base64.encode(contents);
548

    
549
                return {
550
                    path: pathinfo.path,
551
                    contents: contents,
552
                    mode: 0600,
553
                    owner: pathinfo.user
554
                }
555
            });
556
        }
557
    });
558

    
559
    // Flavor model
560
    models.Flavor = models.Model.extend({
561
        path: 'flavors',
562

    
563
        details_string: function() {
564
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
565
        },
566

    
567
        get_disk_size: function() {
568
            return parseInt(this.get("disk") * 1024)
569
        },
570

    
571
        get_ram_size: function() {
572
            return parseInt(this.get("ram"))
573
        },
574

    
575
        get_disk_template_info: function() {
576
            var info = snf.config.flavors_disk_templates_info[this.get("disk_template")];
577
            if (!info) {
578
                info = { name: this.get("disk_template"), description:'' };
579
            }
580
            return info
581
        },
582

    
583
        disk_to_bytes: function() {
584
            return parseInt(this.get("disk")) * 1024 * 1024 * 1024;
585
        },
586

    
587
        ram_to_bytes: function() {
588
            return parseInt(this.get("ram")) * 1024 * 1024;
589
        },
590

    
591
    });
592
    
593
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
594
    _.extend(models.ParamsList.prototype, bb.Events, {
595

    
596
        initialize: function(parent, param_name) {
597
            this.parent = parent;
598
            this.actions = {};
599
            this.param_name = param_name;
600
            this.length = 0;
601
        },
602
        
603
        has_action: function(action) {
604
            return this.actions[action] ? true : false;
605
        },
606
            
607
        _parse_params: function(arguments) {
608
            if (arguments.length <= 1) {
609
                return [];
610
            }
611

    
612
            var args = _.toArray(arguments);
613
            return args.splice(1);
614
        },
615

    
616
        contains: function(action, params) {
617
            params = this._parse_params(arguments);
618
            var has_action = this.has_action(action);
619
            if (!has_action) { return false };
620

    
621
            var paramsEqual = false;
622
            _.each(this.actions[action], function(action_params) {
623
                if (_.isEqual(action_params, params)) {
624
                    paramsEqual = true;
625
                }
626
            });
627
                
628
            return paramsEqual;
629
        },
630
        
631
        is_empty: function() {
632
            return _.isEmpty(this.actions);
633
        },
634

    
635
        add: function(action, params) {
636
            params = this._parse_params(arguments);
637
            if (this.contains.apply(this, arguments)) { return this };
638
            var isnew = false
639
            if (!this.has_action(action)) {
640
                this.actions[action] = [];
641
                isnew = true;
642
            };
643

    
644
            this.actions[action].push(params);
645
            this.parent.trigger("change:" + this.param_name, this.parent, this);
646
            if (isnew) {
647
                this.trigger("add", action, params);
648
            } else {
649
                this.trigger("change", action, params);
650
            }
651
            return this;
652
        },
653
        
654
        remove_all: function(action) {
655
            if (this.has_action(action)) {
656
                delete this.actions[action];
657
                this.parent.trigger("change:" + this.param_name, this.parent, this);
658
                this.trigger("remove", action);
659
            }
660
            return this;
661
        },
662

    
663
        reset: function() {
664
            this.actions = {};
665
            this.parent.trigger("change:" + this.param_name, this.parent, this);
666
            this.trigger("reset");
667
            this.trigger("remove");
668
        },
669

    
670
        remove: function(action, params) {
671
            params = this._parse_params(arguments);
672
            if (!this.has_action(action)) { return this };
673
            var index = -1;
674
            _.each(this.actions[action], _.bind(function(action_params) {
675
                if (_.isEqual(action_params, params)) {
676
                    index = this.actions[action].indexOf(action_params);
677
                }
678
            }, this));
679
            
680
            if (index > -1) {
681
                this.actions[action].splice(index, 1);
682
                if (_.isEmpty(this.actions[action])) {
683
                    delete this.actions[action];
684
                }
685
                this.parent.trigger("change:" + this.param_name, this.parent, this);
686
                this.trigger("remove", action, params);
687
            }
688
        }
689

    
690
    });
691

    
692
    // Virtualmachine model
693
    models.VM = models.Model.extend({
694

    
695
        path: 'servers',
696
        has_status: true,
697
        proxy_attrs: {
698
          'busy': [
699
            ['status', 'state'], function() {
700
              return !_.contains(['ACTIVE', 'STOPPED'], this.get('status'));
701
            }
702
          ],
703
        },
704

    
705
        initialize: function(params) {
706
            var self = this;
707
            this.ports = new Backbone.FilteredCollection(undefined, {
708
              collection: synnefo.storage.ports,
709
              collectionFilter: function(m) {
710
                return self.id == m.get('device_id')
711
            }});
712

    
713
            this.pending_firewalls = {};
714
            
715
            models.VM.__super__.initialize.apply(this, arguments);
716

    
717

    
718
            this.set({state: params.status || "ERROR"});
719
            this.log = new snf.logging.logger("VM " + this.id);
720
            this.pending_action = undefined;
721
            
722
            // init stats parameter
723
            this.set({'stats': undefined}, {silent: true});
724
            // defaults to not update the stats
725
            // each view should handle this vm attribute 
726
            // depending on if it displays stat images or not
727
            this.do_update_stats = false;
728
            
729
            // interval time
730
            // this will dynamicaly change if the server responds that
731
            // images get refreshed on different intervals
732
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
733
            this.stats_available = false;
734

    
735
            // initialize interval
736
            this.init_stats_intervals(this.stats_update_interval);
737
            
738
            // handle progress message on instance change
739
            this.bind("change", _.bind(this.update_status_message, this));
740
            this.bind("change:task_state", _.bind(this.update_status, this));
741
            // force update of progress message
742
            this.update_status_message(true);
743
            
744
            // default values
745
            this.bind("change:state", _.bind(function(){
746
                if (this.state() == "DESTROY") { 
747
                    this.handle_destroy() 
748
                }
749
            }, this));
750

    
751
        },
752

    
753
        status: function(st) {
754
            if (!st) { return this.get("status")}
755
            return this.set({status:st});
756
        },
757
        
758
        update_status: function() {
759
            this.set_status(this.get('status'));
760
        },
761

    
762
        set_status: function(st) {
763
            var new_state = this.state_for_api_status(st);
764
            var transition = false;
765

    
766
            if (this.state() != new_state) {
767
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
768
                    transition = this.state();
769
                }
770
            }
771
            
772
            // call it silently to avoid double change trigger
773
            var state = this.state_for_api_status(st);
774
            this.set({'state': state}, {silent: true});
775
            
776
            // trigger transition
777
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
778
                this.trigger("transition", {from:transition, to:new_state}) 
779
            };
780
            return st;
781
        },
782
            
783
        get_diagnostics: function(success) {
784
            this.__make_api_call(this.get_diagnostics_url(),
785
                                 "read", // create so that sync later uses POST to make the call
786
                                 null, // payload
787
                                 function(data) {
788
                                     success(data);
789
                                 },  
790
                                 null, 'diagnostics');
791
        },
792

    
793
        has_diagnostics: function() {
794
            return this.get("diagnostics") && this.get("diagnostics").length;
795
        },
796

    
797
        get_progress_info: function() {
798
            // details about progress message
799
            // contains a list of diagnostic messages
800
            return this.get("status_messages");
801
        },
802

    
803
        get_status_message: function() {
804
            return this.get('status_message');
805
        },
806
        
807
        // extract status message from diagnostics
808
        status_message_from_diagnostics: function(diagnostics) {
809
            var valid_sources_map = synnefo.config.diagnostics_status_messages_map;
810
            var valid_sources = valid_sources_map[this.get('status')];
811
            if (!valid_sources) { return null };
812
            
813
            // filter messsages based on diagnostic source
814
            var messages = _.filter(diagnostics, function(diag) {
815
                return valid_sources.indexOf(diag.source) > -1;
816
            });
817

    
818
            var msg = messages[0];
819
            if (msg) {
820
              var message = msg.message;
821
              var message_tpl = snf.config.diagnostic_messages_tpls[msg.source];
822

    
823
              if (message_tpl) {
824
                  message = message_tpl.replace('MESSAGE', msg.message);
825
              }
826
              return message;
827
            }
828
            
829
            // no message to display, but vm in build state, display
830
            // finalizing message.
831
            if (this.is_building() == 'BUILD') {
832
                return synnefo.config.BUILDING_MESSAGES['FINAL'];
833
            }
834
            return null;
835
        },
836

    
837
        update_status_message: function(force) {
838
            // update only if one of the specified attributes has changed
839
            if (
840
              !this.keysChanged(['diagnostics', 'progress', 'status', 'state'])
841
                && !force
842
            ) { return };
843
            
844
            // if user requested to destroy the vm set the appropriate 
845
            // message.
846
            if (this.get('state') == "DESTROY") { 
847
                message = "Terminating..."
848
                this.set({status_message: message})
849
                return;
850
            }
851
            
852
            // set error message, if vm has diagnostic message display it as
853
            // progress message
854
            if (this.in_error_state()) {
855
                var d = this.get('diagnostics');
856
                if (d && d.length) {
857
                    var message = this.status_message_from_diagnostics(d);
858
                    this.set({status_message: message});
859
                } else {
860
                    this.set({status_message: null});
861
                }
862
                return;
863
            }
864
            
865
            // identify building status message
866
            if (this.is_building()) {
867
                var self = this;
868
                var success = function(msg) {
869
                    self.set({status_message: msg});
870
                }
871
                this.get_building_status_message(success);
872
                return;
873
            }
874

    
875
            this.set({status_message:null});
876
        },
877
            
878
        // get building status message. Asynchronous function since it requires
879
        // access to vm image.
880
        get_building_status_message: function(callback) {
881
            // no progress is set, vm is in initial build status
882
            var progress = this.get("progress");
883
            if (progress == 0 || !progress) {
884
                return callback(BUILDING_MESSAGES['INIT']);
885
            }
886
            
887
            // vm has copy progress, display copy percentage
888
            if (progress > 0 && progress <= 99) {
889
                this.get_copy_details(true, undefined, _.bind(
890
                    function(details){
891
                        callback(BUILDING_MESSAGES['COPY'].format(details.copy, 
892
                                                           details.size, 
893
                                                           details.progress));
894
                }, this));
895
                return;
896
            }
897

    
898
            // copy finished display FINAL message or identify status message
899
            // from diagnostics.
900
            if (progress >= 100) {
901
                if (!this.has_diagnostics()) {
902
                        callback(BUILDING_MESSAGES['FINAL']);
903
                } else {
904
                        var d = this.get("diagnostics");
905
                        var msg = this.status_message_from_diagnostics(d);
906
                        if (msg) {
907
                              callback(msg);
908
                        }
909
                }
910
            }
911
        },
912

    
913
        get_copy_details: function(human, image, callback) {
914
            var human = human || false;
915
            var image = image || this.get_image(_.bind(function(image){
916
                var progress = this.get('progress');
917
                var size = image.get_size();
918
                var size_copied = (size * progress / 100).toFixed(2);
919
                
920
                if (human) {
921
                    size = util.readablizeBytes(size*1024*1024);
922
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
923
                }
924

    
925
                callback({'progress': progress, 'size': size, 'copy': size_copied})
926
            }, this));
927
        },
928

    
929
        start_stats_update: function(force_if_empty) {
930
            var prev_state = this.do_update_stats;
931

    
932
            this.do_update_stats = true;
933
            
934
            // fetcher initialized ??
935
            if (!this.stats_fetcher) {
936
                this.init_stats_intervals();
937
            }
938

    
939

    
940
            // fetcher running ???
941
            if (!this.stats_fetcher.running || !prev_state) {
942
                this.stats_fetcher.start();
943
            }
944

    
945
            if (force_if_empty && this.get("stats") == undefined) {
946
                this.update_stats(true);
947
            }
948
        },
949

    
950
        stop_stats_update: function(stop_calls) {
951
            this.do_update_stats = false;
952

    
953
            if (stop_calls) {
954
                this.stats_fetcher.stop();
955
            }
956
        },
957

    
958
        // clear and reinitialize update interval
959
        init_stats_intervals: function (interval) {
960
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
961
            this.stats_fetcher.start();
962
        },
963
        
964
        get_stats_fetcher: function(timeout) {
965
            var cb = _.bind(function(data){
966
                this.update_stats();
967
            }, this);
968
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
969
            return fetcher;
970
        },
971

    
972
        // do the api call
973
        update_stats: function(force) {
974
            // do not update stats if flag not set
975
            if ((!this.do_update_stats && !force) || this.updating_stats) {
976
                return;
977
            }
978

    
979
            // make the api call, execute handle_stats_update on sucess
980
            // TODO: onError handler ???
981
            stats_url = this.url() + "/stats";
982
            this.updating_stats = true;
983
            this.sync("read", this, {
984
                handles_error:true, 
985
                url: stats_url, 
986
                refresh:true, 
987
                success: _.bind(this.handle_stats_update, this),
988
                error: _.bind(this.handle_stats_error, this),
989
                complete: _.bind(function(){this.updating_stats = false;}, this),
990
                critical: false,
991
                log_error: false,
992
                skips_timeouts: true
993
            });
994
        },
995

    
996
        get_attachment: function(id) {
997
          var attachment = undefined;
998
          _.each(this.get("attachments"), function(a) {
999
            if (a.id == id) {
1000
              attachment = a;
1001
            }
1002
          });
1003
          return attachment
1004
        },
1005

    
1006
        _set_stats: function(stats) {
1007
            var silent = silent === undefined ? false : silent;
1008
            // unavailable stats while building
1009
            if (this.get("status") == "BUILD") { 
1010
                this.stats_available = false;
1011
            } else { this.stats_available = true; }
1012

    
1013
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
1014
            
1015
            this.set({stats: stats}, {silent:true});
1016
            this.trigger("stats:update", stats);
1017
        },
1018

    
1019
        unbind: function() {
1020
            models.VM.__super__.unbind.apply(this, arguments);
1021
        },
1022
        
1023
        can_connect: function() {
1024
          return _.contains(["ACTIVE", "STOPPED"], this.get("status"))
1025
        },
1026

    
1027
        can_resize: function() {
1028
          return this.get('status') == 'STOPPED';
1029
        },
1030

    
1031
        handle_stats_error: function() {
1032
            stats = {};
1033
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1034
                stats[k] = false;
1035
            });
1036

    
1037
            this.set({'stats': stats});
1038
        },
1039

    
1040
        // this method gets executed after a successful vm stats api call
1041
        handle_stats_update: function(data) {
1042
            var self = this;
1043
            // avoid browser caching
1044
            
1045
            if (data.stats && _.size(data.stats) > 0) {
1046
                var ts = $.now();
1047
                var stats = data.stats;
1048
                var images_loaded = 0;
1049
                var images = {};
1050

    
1051
                function check_images_loaded() {
1052
                    images_loaded++;
1053

    
1054
                    if (images_loaded == 4) {
1055
                        self._set_stats(images);
1056
                    }
1057
                }
1058
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1059
                    
1060
                    stats[k] = stats[k] + "?_=" + ts;
1061
                    
1062
                    var stat = k.slice(0,3);
1063
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
1064
                    var img = $("<img />");
1065
                    var val = stats[k];
1066
                    
1067
                    // load stat image to a temporary dom element
1068
                    // update model stats on image load/error events
1069
                    img.load(function() {
1070
                        images[k] = val;
1071
                        check_images_loaded();
1072
                    });
1073

    
1074
                    img.error(function() {
1075
                        images[stat + type] = false;
1076
                        check_images_loaded();
1077
                    });
1078

    
1079
                    img.attr({'src': stats[k]});
1080
                })
1081
                data.stats = stats;
1082
            }
1083

    
1084
            // do we need to change the interval ??
1085
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
1086
                this.stats_update_interval = data.stats.refresh * 1000;
1087
                this.stats_fetcher.interval = this.stats_update_interval;
1088
                this.stats_fetcher.maximum_interval = this.stats_update_interval;
1089
                this.stats_fetcher.stop();
1090
                this.stats_fetcher.start(false);
1091
            }
1092
        },
1093

    
1094
        // helper method that sets the do_update_stats
1095
        // in the future this method could also make an api call
1096
        // immediaetly if needed
1097
        enable_stats_update: function() {
1098
            this.do_update_stats = true;
1099
        },
1100
        
1101
        handle_destroy: function() {
1102
            this.stats_fetcher.stop();
1103
        },
1104

    
1105
        require_reboot: function() {
1106
            if (this.is_active()) {
1107
                this.set({'reboot_required': true});
1108
            }
1109
        },
1110
        
1111
        set_pending_action: function(data) {
1112
            this.pending_action = data;
1113
            return data;
1114
        },
1115

    
1116
        // machine has pending action
1117
        update_pending_action: function(action, force) {
1118
            this.set({pending_action: action});
1119
        },
1120

    
1121
        clear_pending_action: function() {
1122
            this.set({pending_action: undefined});
1123
        },
1124

    
1125
        has_pending_action: function() {
1126
            return this.get("pending_action") ? this.get("pending_action") : false;
1127
        },
1128
        
1129
        // machine is active
1130
        is_active: function() {
1131
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
1132
        },
1133
        
1134
        // machine is building 
1135
        is_building: function() {
1136
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
1137
        },
1138
        
1139
        is_rebooting: function() {
1140
            return this.state() == 'REBOOT';
1141
        },
1142

    
1143
        in_error_state: function() {
1144
            return this.state() === "ERROR"
1145
        },
1146

    
1147
        // user can connect to machine
1148
        is_connectable: function() {
1149
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1150
        },
1151
        
1152
        remove_meta: function(key, complete, error) {
1153
            var url = this.api_path() + "/metadata/" + key;
1154
            this.api_call(url, "delete", undefined, complete, error);
1155
        },
1156

    
1157
        save_meta: function(meta, complete, error) {
1158
            var url = this.api_path() + "/metadata/" + meta.key;
1159
            var payload = {meta:{}};
1160
            payload.meta[meta.key] = meta.value;
1161
            payload._options = {
1162
                critical:false, 
1163
                error_params: {
1164
                    title: "Machine metadata error",
1165
                    extra_details: {"Machine id": this.id}
1166
            }};
1167

    
1168
            this.api_call(url, "update", payload, complete, error);
1169
        },
1170

    
1171

    
1172
        // update/get the state of the machine
1173
        state: function() {
1174
            var args = slice.call(arguments);
1175
                
1176
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1177
                this.set({'state': args[0]});
1178
            }
1179

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

    
1230
        get_resize_flavors: function() {
1231
          var vm_flavor = this.get_flavor();
1232
          var flavors = synnefo.storage.flavors.filter(function(f){
1233
              return f.get('disk_template') ==
1234
              vm_flavor.get('disk_template') && f.get('disk') ==
1235
              vm_flavor.get('disk');
1236
          });
1237
          return flavors;
1238
        },
1239

    
1240
        get_flavor_quotas: function() {
1241
          var flavor = this.get_flavor();
1242
          return {
1243
            cpu: flavor.get('cpu') + 1, 
1244
            ram: flavor.get_ram_size() + 1, 
1245
            disk:flavor.get_disk_size() + 1
1246
          }
1247
        },
1248

    
1249
        get_meta: function(key, deflt) {
1250
            if (this.get('metadata') && this.get('metadata')) {
1251
                if (!this.get('metadata')[key]) { return deflt }
1252
                return _.escape(this.get('metadata')[key]);
1253
            } else {
1254
                return deflt;
1255
            }
1256
        },
1257

    
1258
        get_meta_keys: function() {
1259
            if (this.get('metadata') && this.get('metadata')) {
1260
                return _.keys(this.get('metadata'));
1261
            } else {
1262
                return [];
1263
            }
1264
        },
1265
        
1266
        // get metadata OS value
1267
        get_os: function() {
1268
            var image = this.get_image();
1269
            return this.get_meta('OS') || (image ? 
1270
                                            image.get_os() || "okeanos" : "okeanos");
1271
        },
1272

    
1273
        get_gui: function() {
1274
            return this.get_meta('GUI');
1275
        },
1276
        
1277
        get_hostname: function() {
1278
          var hostname = this.get_meta('hostname');
1279
          if (!hostname) {
1280
            if (synnefo.config.vm_hostname_format) {
1281
              hostname = synnefo.config.vm_hostname_format.format(this.id);
1282
            } else {
1283
              // TODO: resolve public ip
1284
              hostname = 'PUBLIC NIC';
1285
            }
1286
          }
1287
          return hostname;
1288
        },
1289

    
1290
        // get actions that the user can execute
1291
        // depending on the vm state/status
1292
        get_available_actions: function() {
1293
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1294
        },
1295

    
1296
        set_profile: function(profile, net_id) {
1297
        },
1298
        
1299
        // call rename api
1300
        rename: function(new_name) {
1301
            //this.set({'name': new_name});
1302
            this.sync("update", this, {
1303
                critical: true,
1304
                data: {
1305
                    'server': {
1306
                        'name': new_name
1307
                    }
1308
                }, 
1309
                success: _.bind(function(){
1310
                    snf.api.trigger("call");
1311
                }, this)
1312
            });
1313
        },
1314
        
1315
        get_console_url: function(data) {
1316
            var url_params = {
1317
                machine: this.get("name"),
1318
                host_ip: this.get_hostname(),
1319
                host_ip_v6: this.get_hostname(),
1320
                host: data.host,
1321
                port: data.port,
1322
                password: data.password
1323
            }
1324
            return synnefo.config.ui_console_url + '?' + $.param(url_params);
1325
        },
1326
      
1327
        connect_floating_ip: function(ip, cb) {
1328
          synnefo.storage.ports.create({
1329
            port: {
1330
              network_id: ip.get('floating_network_id'),
1331
              device_id: this.id,
1332
              fixed_ips: [{'ip_address': ip.get('floating_ip_address')}]
1333
            }
1334
          }, {complete: cb, skip_api_error: false})
1335
          // TODO: Implement
1336
        },
1337

    
1338
        // action helper
1339
        call: function(action_name, success, error, params) {
1340
            var id_param = [this.id];
1341
            
1342
            params = params || {};
1343
            success = success || function() {};
1344
            error = error || function() {};
1345

    
1346
            var self = this;
1347

    
1348
            switch(action_name) {
1349
                case 'start':
1350
                    this.__make_api_call(this.get_action_url(), // vm actions url
1351
                                         "create", // create so that sync later uses POST to make the call
1352
                                         {start:{}}, // payload
1353
                                         function() {
1354
                                             // set state after successful call
1355
                                             self.state("START"); 
1356
                                             success.apply(this, arguments);
1357
                                             snf.api.trigger("call");
1358
                                         },  
1359
                                         error, 'start', params);
1360
                    break;
1361
                case 'reboot':
1362
                    this.__make_api_call(this.get_action_url(), // vm actions url
1363
                                         "create", // create so that sync later uses POST to make the call
1364
                                         {reboot:{}}, // payload
1365
                                         function() {
1366
                                             // set state after successful call
1367
                                             self.state("REBOOT"); 
1368
                                             success.apply(this, arguments)
1369
                                             snf.api.trigger("call");
1370
                                             self.set({'reboot_required': false});
1371
                                         },
1372
                                         error, 'reboot', params);
1373
                    break;
1374
                case 'shutdown':
1375
                    this.__make_api_call(this.get_action_url(), // vm actions url
1376
                                         "create", // create so that sync later uses POST to make the call
1377
                                         {shutdown:{}}, // payload
1378
                                         function() {
1379
                                             // set state after successful call
1380
                                             self.state("SHUTDOWN"); 
1381
                                             success.apply(this, arguments)
1382
                                             snf.api.trigger("call");
1383
                                         },  
1384
                                         error, 'shutdown', params);
1385
                    break;
1386
                case 'console':
1387
                    this.__make_api_call(this.url() + "/action", "create", 
1388
                                         {'console': {'type':'vnc'}}, 
1389
                                         function(data) {
1390
                        var cons_data = data.console;
1391
                        success.apply(this, [cons_data]);
1392
                    }, undefined, 'console', params)
1393
                    break;
1394
                case 'destroy':
1395
                    this.__make_api_call(this.url(), // vm actions url
1396
                                         "delete", // create so that sync later uses POST to make the call
1397
                                         undefined, // payload
1398
                                         function() {
1399
                                             // set state after successful call
1400
                                             self.state('DESTROY');
1401
                                             success.apply(this, arguments);
1402
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1403

    
1404
                                         },  
1405
                                         error, 'destroy', params);
1406
                    break;
1407
                case 'resize':
1408
                    this.__make_api_call(this.get_action_url(), // vm actions url
1409
                                         "create", // create so that sync later uses POST to make the call
1410
                                         {resize: {flavorRef:params.flavor}}, // payload
1411
                                         function() {
1412
                                             self.state('RESIZE');
1413
                                             success.apply(this, arguments);
1414
                                             snf.api.trigger("call");
1415
                                         },  
1416
                                         error, 'resize', params);
1417
                    break;
1418
                case 'addFloatingIp':
1419
                    this.__make_api_call(this.get_action_url(), // vm actions url
1420
                                         "create", // create so that sync later uses POST to make the call
1421
                                         {addFloatingIp: {address:params.address}}, // payload
1422
                                         function() {
1423
                                             self.state('CONNECT');
1424
                                             success.apply(this, arguments);
1425
                                             snf.api.trigger("call");
1426
                                         },  
1427
                                         error, 'addFloatingIp', params);
1428
                    break;
1429
                case 'removeFloatingIp':
1430
                    this.__make_api_call(this.get_action_url(), // vm actions url
1431
                                         "create", // create so that sync later uses POST to make the call
1432
                                         {removeFloatingIp: {address:params.address}}, // payload
1433
                                         function() {
1434
                                             self.state('DISCONNECT');
1435
                                             success.apply(this, arguments);
1436
                                             snf.api.trigger("call");
1437
                                         },  
1438
                                         error, 'addFloatingIp', params);
1439
                    break;
1440
                case 'destroy':
1441
                    this.__make_api_call(this.url(), // vm actions url
1442
                                         "delete", // create so that sync later uses POST to make the call
1443
                                         undefined, // payload
1444
                                         function() {
1445
                                             // set state after successful call
1446
                                             self.state('DESTROY');
1447
                                             success.apply(this, arguments);
1448
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1449

    
1450
                                         },  
1451
                                         error, 'destroy', params);
1452
                    break;
1453
                default:
1454
                    throw "Invalid VM action ("+action_name+")";
1455
            }
1456
        },
1457
        
1458
        __make_api_call: function(url, method, data, success, error, action, 
1459
                                  extra_params) {
1460
            var self = this;
1461
            error = error || function(){};
1462
            success = success || function(){};
1463

    
1464
            var params = {
1465
                url: url,
1466
                data: data,
1467
                success: function() { 
1468
                  self.handle_action_succeed.apply(self, arguments); 
1469
                  success.apply(this, arguments)
1470
                },
1471
                error: function() { 
1472
                  self.handle_action_fail.apply(self, arguments);
1473
                  error.apply(this, arguments)
1474
                },
1475
                error_params: { ns: "Machines actions", 
1476
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1477
                                extra_details: {
1478
                                  'Machine ID': this.id, 
1479
                                  'URL': url, 
1480
                                  'Action': action || "undefined" },
1481
                                allow_reload: false
1482
                              },
1483
                display: false,
1484
                critical: false
1485
            }
1486
            _.extend(params, extra_params);
1487
            this.sync(method, this, params);
1488
        },
1489

    
1490
        handle_action_succeed: function() {
1491
            this.trigger("action:success", arguments);
1492
        },
1493
        
1494
        reset_action_error: function() {
1495
            this.action_error = false;
1496
            this.trigger("action:fail:reset", this.action_error);
1497
        },
1498

    
1499
        handle_action_fail: function() {
1500
            this.action_error = arguments;
1501
            this.trigger("action:fail", arguments);
1502
        },
1503

    
1504
        get_action_url: function(name) {
1505
            return this.url() + "/action";
1506
        },
1507

    
1508
        get_diagnostics_url: function() {
1509
            return this.url() + "/diagnostics";
1510
        },
1511

    
1512
        get_users: function() {
1513
            var image;
1514
            var users = [];
1515
            try {
1516
              var users = this.get_meta('users').split(" ");
1517
            } catch (err) { users = null }
1518
            if (!users) {
1519
              image = this.get_image();
1520
              if (image) {
1521
                  users = image.get_created_users();
1522
              }
1523
            }
1524
            return users;
1525
        },
1526

    
1527
        get_connection_info: function(host_os, success, error) {
1528
            var url = synnefo.config.ui_connect_url;
1529
            var users = this.get_users();
1530

    
1531
            params = {
1532
                ip_address: this.get_hostname(),
1533
                hostname: this.get_hostname(),
1534
                os: this.get_os(),
1535
                host_os: host_os,
1536
                srv: this.id
1537
            }
1538
            
1539
            if (users.length) { 
1540
                params['username'] = _.last(users)
1541
            }
1542

    
1543
            url = url + "?" + $.param(params);
1544

    
1545
            var ajax = snf.api.sync("read", undefined, { url: url, 
1546
                                                         error:error, 
1547
                                                         success:success, 
1548
                                                         handles_error:1});
1549
        }
1550
    });
1551
    
1552
    models.VM.ACTIONS = [
1553
        'start',
1554
        'shutdown',
1555
        'reboot',
1556
        'console',
1557
        'destroy',
1558
        'resize'
1559
    ]
1560

    
1561
    models.VM.TASK_STATE_STATUS_MAP = {
1562
      'BULDING': 'BUILD',
1563
      'REBOOTING': 'REBOOT',
1564
      'STOPPING': 'SHUTDOWN',
1565
      'STARTING': 'START',
1566
      'RESIZING': 'RESIZE',
1567
      'CONNECTING': 'CONNECT',
1568
      'DISCONNECTING': 'DISCONNECT',
1569
      'DESTROYING': 'DESTROY'
1570
    }
1571

    
1572
    models.VM.AVAILABLE_ACTIONS = {
1573
        'UNKNWON'       : ['destroy'],
1574
        'BUILD'         : ['destroy'],
1575
        'REBOOT'        : ['destroy'],
1576
        'STOPPED'       : ['start', 'destroy', 'resize'],
1577
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console', 'resize'],
1578
        'ERROR'         : ['destroy'],
1579
        'DELETED'       : ['destroy'],
1580
        'DESTROY'       : ['destroy'],
1581
        'SHUTDOWN'      : ['destroy'],
1582
        'START'         : ['destroy'],
1583
        'CONNECT'       : ['destroy'],
1584
        'DISCONNECT'    : ['destroy'],
1585
        'RESIZE'        : ['destroy']
1586
    }
1587
    
1588
    models.VM.AVAILABLE_ACTIONS_INACTIVE = {
1589
      'resize': ['ACTIVE']
1590
    }
1591

    
1592
    // api status values
1593
    models.VM.STATUSES = [
1594
        'UNKNWON',
1595
        'BUILD',
1596
        'REBOOT',
1597
        'STOPPED',
1598
        'ACTIVE',
1599
        'ERROR',
1600
        'DELETED',
1601
        'RESIZE'
1602
    ]
1603

    
1604
    // api status values
1605
    models.VM.CONNECT_STATES = [
1606
        'ACTIVE',
1607
        'REBOOT',
1608
        'SHUTDOWN'
1609
    ]
1610

    
1611
    // vm states
1612
    models.VM.STATES = models.VM.STATUSES.concat([
1613
        'DESTROY',
1614
        'SHUTDOWN',
1615
        'START',
1616
        'CONNECT',
1617
        'DISCONNECT',
1618
        'FIREWALL',
1619
        'RESIZE'
1620
    ]);
1621
    
1622
    models.VM.STATES_TRANSITIONS = {
1623
        'DESTROY' : ['DELETED'],
1624
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1625
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1626
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1627
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1628
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1629
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1630
        'RESIZE': ['ERROR', 'STOPPED']
1631
    }
1632

    
1633
    models.VM.TRANSITION_STATES = [
1634
        'DESTROY',
1635
        'SHUTDOWN',
1636
        'START',
1637
        'REBOOT',
1638
        'BUILD',
1639
        'RESIZE'
1640
    ]
1641

    
1642
    models.VM.ACTIVE_STATES = [
1643
        'BUILD', 'REBOOT', 'ACTIVE',
1644
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1645
    ]
1646

    
1647
    models.VM.BUILDING_STATES = [
1648
        'BUILD'
1649
    ]
1650

    
1651
    models.Images = models.Collection.extend({
1652
        model: models.Image,
1653
        path: 'images',
1654
        details: true,
1655
        noUpdate: true,
1656
        supportIncUpdates: false,
1657
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1658
        meta_labels: {},
1659
        read_method: 'read',
1660

    
1661
        // update collection model with id passed
1662
        // making a direct call to the image
1663
        // api url
1664
        update_unknown_id: function(id, callback) {
1665
            var url = getUrl.call(this) + "/" + id;
1666
            this.api_call(this.path + "/" + id, this.read_method, {
1667
              _options:{
1668
                async:true, 
1669
                skip_api_error:true}
1670
              }, undefined, 
1671
            _.bind(function() {
1672
                if (!this.get(id)) {
1673
                            if (this.fallback_service) {
1674
                        // if current service has fallback_service attribute set
1675
                        // use this service to retrieve the missing image model
1676
                        var tmpservice = new this.fallback_service();
1677
                        tmpservice.update_unknown_id(id, _.bind(function(img){
1678
                            img.attributes.status = "DELETED";
1679
                            this.add(img.attributes);
1680
                            callback(this.get(id));
1681
                        }, this));
1682
                    } else {
1683
                        var title = synnefo.config.image_deleted_title || 'Deleted';
1684
                        // else add a dummy DELETED state image entry
1685
                        this.add({id:id, name:title, size:-1, 
1686
                                  progress:100, status:"DELETED"});
1687
                        callback(this.get(id));
1688
                    }   
1689
                } else {
1690
                    callback(this.get(id));
1691
                }
1692
            }, this), _.bind(function(image, msg, xhr) {
1693
                if (!image) {
1694
                    var title = synnefo.config.image_deleted_title || 'Deleted';
1695
                    this.add({id:id, name:title, size:-1, 
1696
                              progress:100, status:"DELETED"});
1697
                    callback(this.get(id));
1698
                    return;
1699
                }
1700
                var img_data = this._read_image_from_request(image, msg, xhr);
1701
                this.add(img_data);
1702
                callback(this.get(id));
1703
            }, this));
1704
        },
1705

    
1706
        _read_image_from_request: function(image, msg, xhr) {
1707
            return image.image;
1708
        },
1709

    
1710
        parse: function (resp, xhr) {
1711
            var parsed = _.map(resp.images, _.bind(this.parse_meta, this));
1712
            parsed = this.fill_owners(parsed);
1713
            return parsed;
1714
        },
1715

    
1716
        fill_owners: function(images) {
1717
            // do translate uuid->displayname if needed
1718
            // store display name in owner attribute for compatibility
1719
            var uuids = [];
1720

    
1721
            var images = _.map(images, function(img, index) {
1722
                if (synnefo.config.translate_uuids) {
1723
                    uuids.push(img['owner']);
1724
                }
1725
                img['owner_uuid'] = img['owner'];
1726
                return img;
1727
            });
1728
            
1729
            if (uuids.length > 0) {
1730
                var handle_results = function(data) {
1731
                    _.each(images, function (img) {
1732
                        img['owner'] = data.uuid_catalog[img['owner_uuid']];
1733
                    });
1734
                }
1735
                // notice the async false
1736
                var uuid_map = this.translate_uuids(uuids, false, 
1737
                                                    handle_results)
1738
            }
1739
            return images;
1740
        },
1741

    
1742
        translate_uuids: function(uuids, async, cb) {
1743
            var url = synnefo.config.user_catalog_url;
1744
            var data = JSON.stringify({'uuids': uuids});
1745
          
1746
            // post to user_catalogs api
1747
            snf.api.sync('create', undefined, {
1748
                url: url,
1749
                data: data,
1750
                async: async,
1751
                success:  cb
1752
            });
1753
        },
1754

    
1755
        get_meta_key: function(img, key) {
1756
            if (img.metadata && img.metadata && img.metadata[key]) {
1757
                return _.escape(img.metadata[key]);
1758
            }
1759
            return undefined;
1760
        },
1761

    
1762
        comparator: function(img) {
1763
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1764
        },
1765

    
1766
        parse_meta: function(img) {
1767
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1768
                if (img[key]) { return };
1769
                img[key] = this.get_meta_key(img, key) || "";
1770
            }, this));
1771
            return img;
1772
        },
1773

    
1774
        active: function() {
1775
            return this.filter(function(img){return img.get('status') != "DELETED"});
1776
        },
1777

    
1778
        predefined: function() {
1779
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
1780
        },
1781
        
1782
        fetch_for_type: function(type, complete, error) {
1783
            this.fetch({update:true, 
1784
                        success: complete, 
1785
                        error: error, 
1786
                        skip_api_error: true });
1787
        },
1788
        
1789
        get_images_for_type: function(type) {
1790
            if (this['get_{0}_images'.format(type)]) {
1791
                return this['get_{0}_images'.format(type)]();
1792
            }
1793

    
1794
            return this.active();
1795
        },
1796

    
1797
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
1798
            var load = false;
1799
            error = onError || function() {};
1800
            function complete(collection) { 
1801
                onComplete(collection.get_images_for_type(type)); 
1802
            }
1803
            
1804
            // do we need to fetch/update current collection entries
1805
            if (load) {
1806
                onStart();
1807
                this.fetch_for_type(type, complete, error);
1808
            } else {
1809
                // fallback to complete
1810
                complete(this);
1811
            }
1812
        }
1813
    })
1814

    
1815
    models.Flavors = models.Collection.extend({
1816
        model: models.Flavor,
1817
        path: 'flavors',
1818
        details: true,
1819
        noUpdate: true,
1820
        supportIncUpdates: false,
1821
        // update collection model with id passed
1822
        // making a direct call to the flavor
1823
        // api url
1824
        update_unknown_id: function(id, callback) {
1825
            var url = getUrl.call(this) + "/" + id;
1826
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
1827
            _.bind(function() {
1828
                this.add({id:id, cpu:"Unknown", ram:"Unknown", disk:"Unknown", name: "Unknown", status:"DELETED"})
1829
            }, this), _.bind(function(flv) {
1830
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1831
                this.add(flv.flavor);
1832
            }, this));
1833
        },
1834

    
1835
        parse: function (resp, xhr) {
1836
            return _.map(resp.flavors, function(o) {
1837
              o.cpu = o['vcpus'];
1838
              o.disk_template = o['SNF:disk_template'];
1839
              return o
1840
            });
1841
        },
1842

    
1843
        comparator: function(flv) {
1844
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1845
        },
1846
          
1847
        unavailable_values_for_quotas: function(quotas, flavors, extra) {
1848
            var flavors = flavors || this.active();
1849
            var index = {cpu:[], disk:[], ram:[]};
1850
            var extra = extra == undefined ? {cpu:0, disk:0, ram:0} : extra;
1851
            
1852
            _.each(flavors, function(el) {
1853

    
1854
                var disk_available = quotas['disk'] + extra.disk;
1855
                var disk_size = el.get_disk_size();
1856
                if (index.disk.indexOf(disk_size) == -1) {
1857
                  var disk = el.disk_to_bytes();
1858
                  if (disk > disk_available) {
1859
                    index.disk.push(disk_size);
1860
                  }
1861
                }
1862
                
1863
                var ram_available = quotas['ram'] + extra.ram * 1024 * 1024;
1864
                var ram_size = el.get_ram_size();
1865
                if (index.ram.indexOf(ram_size) == -1) {
1866
                  var ram = el.ram_to_bytes();
1867
                  if (ram > ram_available) {
1868
                    index.ram.push(el.get('ram'))
1869
                  }
1870
                }
1871

    
1872
                var cpu = el.get('cpu');
1873
                var cpu_available = quotas['cpu'] + extra.cpu;
1874
                if (index.cpu.indexOf(cpu) == -1) {
1875
                  if (cpu > cpu_available) {
1876
                    index.cpu.push(el.get('cpu'))
1877
                  }
1878
                }
1879
            });
1880
            return index;
1881
        },
1882

    
1883
        unavailable_values_for_image: function(img, flavors) {
1884
            var flavors = flavors || this.active();
1885
            var size = img.get_size();
1886
            
1887
            var index = {cpu:[], disk:[], ram:[]};
1888

    
1889
            _.each(this.active(), function(el) {
1890
                var img_size = size;
1891
                var flv_size = el.get_disk_size();
1892
                if (flv_size < img_size) {
1893
                    if (index.disk.indexOf(flv_size) == -1) {
1894
                        index.disk.push(flv_size);
1895
                    }
1896
                };
1897
            });
1898
            
1899
            return index;
1900
        },
1901

    
1902
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
1903
            if (!filter_list) { filter_list = this.models };
1904
            
1905
            return this.select(function(flv){
1906
                if (flv.get("cpu") == cpu + "" &&
1907
                   flv.get("ram") == mem + "" &&
1908
                   flv.get("disk") == disk + "" &&
1909
                   flv.get("disk_template") == disk_template &&
1910
                   filter_list.indexOf(flv) > -1) { return true; }
1911
            })[0];
1912
        },
1913
        
1914
        get_data: function(lst) {
1915
            var data = {'cpu': [], 'mem':[], 'disk':[], 'disk_template':[]};
1916

    
1917
            _.each(lst, function(flv) {
1918
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1919
                    data.cpu.push(flv.get("cpu"));
1920
                }
1921
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1922
                    data.mem.push(flv.get("ram"));
1923
                }
1924
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1925
                    data.disk.push(flv.get("disk"));
1926
                }
1927
                if (data.disk_template.indexOf(flv.get("disk_template")) == -1) {
1928
                    data.disk_template.push(flv.get("disk_template"));
1929
                }
1930
            })
1931
            
1932
            return data;
1933
        },
1934

    
1935
        active: function() {
1936
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1937
        }
1938
            
1939
    })
1940

    
1941
    models.VMS = models.Collection.extend({
1942
        model: models.VM,
1943
        path: 'servers',
1944
        details: true,
1945
        copy_image_meta: true,
1946

    
1947
        parse: function (resp, xhr) {
1948
            var data = resp;
1949
            if (!resp) { return [] };
1950
            data = _.filter(_.map(resp.servers, 
1951
                                  _.bind(this.parse_vm_api_data, this)), 
1952
                                  function(v){return v});
1953
            return data;
1954
        },
1955

    
1956
        parse_vm_api_data: function(data) {
1957
            // do not add non existing DELETED entries
1958
            if (data.status && data.status == "DELETED") {
1959
                if (!this.get(data.id)) {
1960
                    return false;
1961
                }
1962
            }
1963
            
1964
            if ('SNF:task_state' in data) { 
1965
                data['task_state'] = data['SNF:task_state'];
1966
                if (data['task_state']) {
1967
                    var status = models.VM.TASK_STATE_STATUS_MAP[data['task_state']];
1968
                    if (status) { data['status'] = status }
1969
                }
1970
            }
1971

    
1972
            // OS attribute
1973
            if (this.has_meta(data)) {
1974
                data['OS'] = data.metadata.OS || snf.config.unknown_os;
1975
            }
1976
            
1977
            if (!data.diagnostics) {
1978
                data.diagnostics = [];
1979
            }
1980

    
1981
            // network metadata
1982
            data['firewalls'] = {};
1983

    
1984
            data['fqdn'] = synnefo.config.vm_hostname_format.format(data['id']);
1985

    
1986
            // if vm has no metadata, no metadata object
1987
            // is in json response, reset it to force
1988
            // value update
1989
            if (!data['metadata']) {
1990
                data['metadata'] = {};
1991
            }
1992
            
1993
            // v2.0 API returns objects
1994
            data.image_obj = data.image;
1995
            data.image = data.image_obj.id;
1996
            data.flavor_obj = data.flavor;
1997
            data.flavor = data.flavor_obj.id;
1998

    
1999
            return data;
2000
        },
2001

    
2002
        get_reboot_required: function() {
2003
            return this.filter(function(vm){return vm.get("reboot_required") == true})
2004
        },
2005

    
2006
        has_pending_actions: function() {
2007
            return this.filter(function(vm){return vm.pending_action}).length > 0;
2008
        },
2009

    
2010
        reset_pending_actions: function() {
2011
            this.each(function(vm) {
2012
                vm.clear_pending_action();
2013
            })
2014
        },
2015

    
2016
        do_all_pending_actions: function(success, error) {
2017
            this.each(function(vm) {
2018
                if (vm.has_pending_action()) {
2019
                    vm.call(vm.pending_action, success, error);
2020
                    vm.clear_pending_action();
2021
                }
2022
            })
2023
        },
2024
        
2025
        do_all_reboots: function(success, error) {
2026
            this.each(function(vm) {
2027
                if (vm.get("reboot_required")) {
2028
                    vm.call("reboot", success, error);
2029
                }
2030
            });
2031
        },
2032

    
2033
        reset_reboot_required: function() {
2034
            this.each(function(vm) {
2035
                vm.set({'reboot_required': undefined});
2036
            })
2037
        },
2038
        
2039
        stop_stats_update: function(exclude) {
2040
            var exclude = exclude || [];
2041
            this.each(function(vm) {
2042
                if (exclude.indexOf(vm) > -1) {
2043
                    return;
2044
                }
2045
                vm.stop_stats_update();
2046
            })
2047
        },
2048
        
2049
        has_meta: function(vm_data) {
2050
            return vm_data.metadata && vm_data.metadata
2051
        },
2052

    
2053
        has_addresses: function(vm_data) {
2054
            return vm_data.metadata && vm_data.metadata
2055
        },
2056

    
2057
        create: function (name, image, flavor, meta, extra, callback) {
2058

    
2059
            if (this.copy_image_meta) {
2060
                if (synnefo.config.vm_image_common_metadata) {
2061
                    _.each(synnefo.config.vm_image_common_metadata, 
2062
                        function(key){
2063
                            if (image.get_meta(key)) {
2064
                                meta[key] = image.get_meta(key);
2065
                            }
2066
                    });
2067
                }
2068

    
2069
                if (image.get("OS")) {
2070
                    meta['OS'] = image.get("OS");
2071
                }
2072
            }
2073
            
2074
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, 
2075
                    metadata:meta}
2076
            opts = _.extend(opts, extra);
2077
            
2078
            var cb = function(data) {
2079
              synnefo.storage.quotas.get('cyclades.vm').increase();
2080
              callback(data);
2081
            }
2082

    
2083
            this.api_call(this.path, "create", {'server': opts}, undefined, 
2084
                          undefined, cb, {critical: true});
2085
        },
2086

    
2087
        load_missing_images: function(callback) {
2088
          var missing_ids = [];
2089
          var resolved = 0;
2090

    
2091
          // fill missing_ids
2092
          this.each(function(el) {
2093
            var imgid = el.get("image");
2094
            var existing = synnefo.storage.images.get(imgid);
2095
            if (!existing && missing_ids.indexOf(imgid) == -1) {
2096
              missing_ids.push(imgid);
2097
            }
2098
          });
2099
          var check = function() {
2100
            // once all missing ids where resolved continue calling the 
2101
            // callback
2102
            resolved++;
2103
            if (resolved == missing_ids.length) {
2104
              callback(missing_ids)
2105
            }
2106
          }
2107
          if (missing_ids.length == 0) {
2108
            callback(missing_ids);
2109
            return;
2110
          }
2111
          // start resolving missing image ids
2112
          _(missing_ids).each(function(imgid){
2113
            synnefo.storage.images.update_unknown_id(imgid, check);
2114
          });
2115
        },
2116

    
2117
        get_connectable: function() {
2118
            return storage.vms.filter(function(vm){
2119
                return !vm.in_error_state() && !vm.is_building();
2120
            });
2121
        }
2122
    })
2123
    
2124
    models.PublicKey = models.Model.extend({
2125
        path: 'keys',
2126
        api_type: 'userdata',
2127
        detail: false,
2128
        model_actions: {
2129
          'remove': [['name'], function() {
2130
            return true;
2131
          }]
2132
        },
2133

    
2134
        get_public_key: function() {
2135
            return cryptico.publicKeyFromString(this.get("content"));
2136
        },
2137

    
2138
        get_filename: function() {
2139
            return "{0}.pub".format(this.get("name"));
2140
        },
2141

    
2142
        identify_type: function() {
2143
            try {
2144
                var cont = snf.util.validatePublicKey(this.get("content"));
2145
                var type = cont.split(" ")[0];
2146
                return synnefo.util.publicKeyTypesMap[type];
2147
            } catch (err) { return false };
2148
        },
2149

    
2150
        rename: function(new_name) {
2151
          //this.set({'name': new_name});
2152
          this.sync("update", this, {
2153
            critical: true,
2154
            data: {'name': new_name}, 
2155
            success: _.bind(function(){
2156
              snf.api.trigger("call");
2157
            }, this)
2158
          });
2159
        }
2160
    })
2161
    
2162
    models._ActionsModel = models.Model.extend({
2163
      defaults: { pending: null },
2164
      actions: [],
2165
      status: {
2166
        INACTIVE: 0,
2167
        PENDING: 1,
2168
        CALLED: 2
2169
      },
2170

    
2171
      initialize: function(attrs, opts) {
2172
        models._ActionsModel.__super__.initialize.call(this, attrs);
2173
        this.actions = opts.actions;
2174
        this.model = opts.model;
2175
        this.bind("change", function() {
2176
          this.set({'pending': this.get_pending()});
2177
        }, this);
2178
        this.clear();
2179
      },
2180
      
2181
      _in_status: function(st) {
2182
        var actions = null;
2183
        _.each(this.attributes, function(status, action){
2184
          if (status == st) {
2185
            if (!actions) {
2186
              actions = []
2187
            }
2188
            actions.push(action);
2189
          }
2190
        });
2191
        return actions;
2192
      },
2193

    
2194
      get_pending: function() {
2195
        return this._in_status(this.status.PENDING);
2196
      },
2197

    
2198
      unset_pending_action: function(action) {
2199
        var data = {};
2200
        data[action] = this.status.INACTIVE;
2201
        this.set(data);
2202
      },
2203

    
2204
      set_pending_action: function(action, reset_pending) {
2205
        reset_pending = reset_pending === undefined ? true : reset_pending;
2206
        var data = {};
2207
        data[action] = this.status.PENDING;
2208
        if (reset_pending) {
2209
          this.reset_pending();
2210
        }
2211
        this.set(data);
2212
      },
2213
      
2214
      reset_pending: function() {
2215
        var data = {};
2216
        _.each(this.actions, function(action) {
2217
          data[action] = this.status.INACTIVE;
2218
        }, this);
2219
        this.set(data);
2220
      }
2221
    });
2222

    
2223
    models.PublicPool = models.Model.extend({});
2224
    models.PublicPools = models.Collection.extend({
2225
      model: models.PublicPool,
2226
      path: 'os-floating-ip-pools',
2227
      api_type: 'compute',
2228
      noUpdate: true,
2229

    
2230
      parse: function(data) {
2231
        return _.map(data.floating_ip_pools, function(pool) {
2232
          pool.id = pool.name;
2233
          return pool;
2234
        });
2235
      }
2236
    });
2237

    
2238
    models.PublicKeys = models.Collection.extend({
2239
        model: models.PublicKey,
2240
        details: false,
2241
        path: 'keys',
2242
        api_type: 'userdata',
2243
        noUpdate: true,
2244
        updateEntries: true,
2245

    
2246
        generate_new: function(success, error) {
2247
            snf.api.sync('create', undefined, {
2248
                url: getUrl.call(this, this.base_url) + "/generate", 
2249
                success: success, 
2250
                error: error,
2251
                skip_api_error: true
2252
            });
2253
        },
2254
        
2255
        add_crypto_key: function(key, success, error, options) {
2256
            var options = options || {};
2257
            var m = new models.PublicKey();
2258

    
2259
            // guess a name
2260
            var name_tpl = "my generated public key";
2261
            var name = name_tpl;
2262
            var name_count = 1;
2263
            
2264
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
2265
                name = name_tpl + " " + name_count;
2266
                name_count++;
2267
            }
2268
            
2269
            m.set({name: name});
2270
            m.set({content: key});
2271
            
2272
            options.success = function () { return success(m) };
2273
            options.errror = error;
2274
            options.skip_api_error = true;
2275
            
2276
            this.create(m.attributes, options);
2277
        }
2278
    });
2279

    
2280
  
2281
    models.Quota = models.Model.extend({
2282

    
2283
        initialize: function() {
2284
            models.Quota.__super__.initialize.apply(this, arguments);
2285
            this.bind("change", this.check, this);
2286
            this.check();
2287
        },
2288
        
2289
        check: function() {
2290
            var usage, limit;
2291
            usage = this.get('usage');
2292
            limit = this.get('limit');
2293
            if (usage >= limit) {
2294
                this.trigger("available");
2295
            } else {
2296
                this.trigger("unavailable");
2297
            }
2298
        },
2299

    
2300
        increase: function(val) {
2301
            if (val === undefined) { val = 1};
2302
            this.set({'usage': this.get('usage') + val})
2303
        },
2304

    
2305
        decrease: function(val) {
2306
            if (val === undefined) { val = 1};
2307
            this.set({'usage': this.get('usage') - val})
2308
        },
2309

    
2310
        can_consume: function() {
2311
            var usage, limit;
2312
            usage = this.get('usage');
2313
            limit = this.get('limit');
2314
            if (usage >= limit) {
2315
                return false
2316
            } else {
2317
                return true
2318
            }
2319
        },
2320
        
2321
        is_bytes: function() {
2322
            return this.get('resource').get('unit') == 'bytes';
2323
        },
2324
        
2325
        get_available: function(active) {
2326
            suffix = '';
2327
            if (active) { suffix = '_active'}
2328
            var value = this.get('limit'+suffix) - this.get('usage'+suffix);
2329
            if (active) {
2330
              if (this.get('available') <= value) {
2331
                value = this.get('available');
2332
              }
2333
            }
2334
            if (value < 0) { return value }
2335
            return value
2336
        },
2337

    
2338
        get_readable: function(key, active) {
2339
            var value;
2340
            if (key == 'available') {
2341
                value = this.get_available(active);
2342
            } else {
2343
                value = this.get(key)
2344
            }
2345
            if (value <= 0) { value = 0 }
2346
            if (!this.is_bytes()) {
2347
              return value + "";
2348
            }
2349
            return snf.util.readablizeBytes(value);
2350
        }
2351
    });
2352

    
2353
    models.Quotas = models.Collection.extend({
2354
        model: models.Quota,
2355
        api_type: 'accounts',
2356
        path: 'quotas',
2357
        parse: function(resp) {
2358
            filtered = _.map(resp.system, function(value, key) {
2359
                var available = (value.limit - value.usage) || 0;
2360
                var available_active = available;
2361
                var keysplit = key.split(".");
2362
                var limit_active = value.limit;
2363
                var usage_active = value.usage;
2364
                keysplit[keysplit.length-1] = "active_" + keysplit[keysplit.length-1];
2365
                var activekey = keysplit.join(".");
2366
                var exists = resp.system[activekey];
2367
                if (exists) {
2368
                    available_active = exists.limit - exists.usage;
2369
                    limit_active = exists.limit;
2370
                    usage_active = exists.usage;
2371
                }
2372
                return _.extend(value, {'name': key, 'id': key, 
2373
                          'available': available,
2374
                          'available_active': available_active,
2375
                          'limit_active': limit_active,
2376
                          'usage_active': usage_active,
2377
                          'resource': snf.storage.resources.get(key)});
2378
            });
2379
            return filtered;
2380
        },
2381
        
2382
        get_by_id: function(k) {
2383
          return this.filter(function(q) { return q.get('name') == k})[0]
2384
        },
2385

    
2386
        get_available_for_vm: function(options) {
2387
          var quotas = synnefo.storage.quotas;
2388
          var key = 'available';
2389
          var available_quota = {};
2390
          _.each(['cyclades.ram', 'cyclades.cpu', 'cyclades.disk'], 
2391
            function (key) {
2392
              var value = quotas.get(key).get_available(true);
2393
              available_quota[key.replace('cyclades.', '')] = value;
2394
          });
2395
          return available_quota;
2396
        }
2397
    })
2398

    
2399
    models.Resource = models.Model.extend({
2400
        api_type: 'accounts',
2401
        path: 'resources'
2402
    });
2403

    
2404
    models.Resources = models.Collection.extend({
2405
        api_type: 'accounts',
2406
        path: 'resources',
2407
        model: models.Network,
2408

    
2409
        parse: function(resp) {
2410
            return _.map(resp, function(value, key) {
2411
                return _.extend(value, {'name': key, 'id': key});
2412
            })
2413
        }
2414
    });
2415
    
2416
    // storage initialization
2417
    snf.storage.images = new models.Images();
2418
    snf.storage.flavors = new models.Flavors();
2419
    snf.storage.vms = new models.VMS();
2420
    snf.storage.keys = new models.PublicKeys();
2421
    snf.storage.resources = new models.Resources();
2422
    snf.storage.quotas = new models.Quotas();
2423
    snf.storage.public_pools = new models.PublicPools();
2424

    
2425
})(this);