Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (82.3 kB)

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

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

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

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

    
64
    // i18n
65
    BUILDING_MESSAGES = window.BUILDING_MESSAGES || {'INIT': 'init', 'COPY': '{0}, {1}, {2}', 'FINAL': 'final'};
66

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

    
75

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
677
    });
678

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

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

    
692
        initialize: function(params) {
693
            var self = this;
694
            this.ports = new Backbone.FilteredCollection(undefined, {
695
              collection: synnefo.storage.ports,
696
              collectionFilter: function(m) {
697
                return self.id == m.get('device_id')
698
            }});
699

    
700
            this.pending_firewalls = {};
701
            
702
            models.VM.__super__.initialize.apply(this, arguments);
703

    
704

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

    
722
            // initialize interval
723
            this.init_stats_intervals(this.stats_update_interval);
724
            
725
            // handle progress message on instance change
726
            this.bind("change", _.bind(this.update_status_message, this));
727
            this.bind("change:task_state", _.bind(this.update_status, this));
728
            // force update of progress message
729
            this.update_status_message(true);
730
            
731
            // default values
732
            this.bind("change:state", _.bind(function(){
733
                if (this.state() == "DESTROY") { 
734
                    this.handle_destroy() 
735
                }
736
            }, this));
737

    
738
        },
739

    
740
        status: function(st) {
741
            if (!st) { return this.get("status")}
742
            return this.set({status:st});
743
        },
744
        
745
        update_status: function() {
746
            this.set_status(this.get('status'));
747
        },
748

    
749
        set_status: function(st) {
750
            var new_state = this.state_for_api_status(st);
751
            var transition = false;
752

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

    
780
        has_diagnostics: function() {
781
            return this.get("diagnostics") && this.get("diagnostics").length;
782
        },
783

    
784
        get_progress_info: function() {
785
            // details about progress message
786
            // contains a list of diagnostic messages
787
            return this.get("status_messages");
788
        },
789

    
790
        get_status_message: function() {
791
            return this.get('status_message');
792
        },
793
        
794
        // extract status message from diagnostics
795
        status_message_from_diagnostics: function(diagnostics) {
796
            var valid_sources_map = synnefo.config.diagnostics_status_messages_map;
797
            var valid_sources = valid_sources_map[this.get('status')];
798
            if (!valid_sources) { return null };
799
            
800
            // filter messsages based on diagnostic source
801
            var messages = _.filter(diagnostics, function(diag) {
802
                return valid_sources.indexOf(diag.source) > -1;
803
            });
804

    
805
            var msg = messages[0];
806
            if (msg) {
807
              var message = msg.message;
808
              var message_tpl = snf.config.diagnostic_messages_tpls[msg.source];
809

    
810
              if (message_tpl) {
811
                  message = message_tpl.replace('MESSAGE', msg.message);
812
              }
813
              return message;
814
            }
815
            
816
            // no message to display, but vm in build state, display
817
            // finalizing message.
818
            if (this.is_building() == 'BUILD') {
819
                return synnefo.config.BUILDING_MESSAGES['FINAL'];
820
            }
821
            return null;
822
        },
823

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

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

    
885
            // copy finished display FINAL message or identify status message
886
            // from diagnostics.
887
            if (progress >= 100) {
888
                if (!this.has_diagnostics()) {
889
                        callback(BUILDING_MESSAGES['FINAL']);
890
                } else {
891
                        var d = this.get("diagnostics");
892
                        var msg = this.status_message_from_diagnostics(d);
893
                        if (msg) {
894
                              callback(msg);
895
                        }
896
                }
897
            }
898
        },
899

    
900
        get_copy_details: function(human, image, callback) {
901
            var human = human || false;
902
            var image = image || this.get_image(_.bind(function(image){
903
                var progress = this.get('progress');
904
                var size = image.get_size();
905
                var size_copied = (size * progress / 100).toFixed(2);
906
                
907
                if (human) {
908
                    size = util.readablizeBytes(size*1024*1024);
909
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
910
                }
911

    
912
                callback({'progress': progress, 'size': size, 'copy': size_copied})
913
            }, this));
914
        },
915

    
916
        start_stats_update: function(force_if_empty) {
917
            var prev_state = this.do_update_stats;
918

    
919
            this.do_update_stats = true;
920
            
921
            // fetcher initialized ??
922
            if (!this.stats_fetcher) {
923
                this.init_stats_intervals();
924
            }
925

    
926

    
927
            // fetcher running ???
928
            if (!this.stats_fetcher.running || !prev_state) {
929
                this.stats_fetcher.start();
930
            }
931

    
932
            if (force_if_empty && this.get("stats") == undefined) {
933
                this.update_stats(true);
934
            }
935
        },
936

    
937
        stop_stats_update: function(stop_calls) {
938
            this.do_update_stats = false;
939

    
940
            if (stop_calls) {
941
                this.stats_fetcher.stop();
942
            }
943
        },
944

    
945
        // clear and reinitialize update interval
946
        init_stats_intervals: function (interval) {
947
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
948
            this.stats_fetcher.start();
949
        },
950
        
951
        get_stats_fetcher: function(timeout) {
952
            var cb = _.bind(function(data){
953
                this.update_stats();
954
            }, this);
955
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
956
            return fetcher;
957
        },
958

    
959
        // do the api call
960
        update_stats: function(force) {
961
            // do not update stats if flag not set
962
            if ((!this.do_update_stats && !force) || this.updating_stats) {
963
                return;
964
            }
965

    
966
            // make the api call, execute handle_stats_update on sucess
967
            // TODO: onError handler ???
968
            stats_url = this.url() + "/stats";
969
            this.updating_stats = true;
970
            this.sync("read", this, {
971
                handles_error:true, 
972
                url: stats_url, 
973
                refresh:true, 
974
                success: _.bind(this.handle_stats_update, this),
975
                error: _.bind(this.handle_stats_error, this),
976
                complete: _.bind(function(){this.updating_stats = false;}, this),
977
                critical: false,
978
                log_error: false,
979
                skips_timeouts: true
980
            });
981
        },
982

    
983
        get_attachment: function(id) {
984
          var attachment = undefined;
985
          _.each(this.get("attachments"), function(a) {
986
            if (a.id == id) {
987
              attachment = a;
988
            }
989
          });
990
          return attachment
991
        },
992

    
993
        _set_stats: function(stats) {
994
            var silent = silent === undefined ? false : silent;
995
            // unavailable stats while building
996
            if (this.get("status") == "BUILD") { 
997
                this.stats_available = false;
998
            } else { this.stats_available = true; }
999

    
1000
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
1001
            
1002
            this.set({stats: stats}, {silent:true});
1003
            this.trigger("stats:update", stats);
1004
        },
1005

    
1006
        unbind: function() {
1007
            models.VM.__super__.unbind.apply(this, arguments);
1008
        },
1009

    
1010
        can_resize: function() {
1011
          return this.get('status') == 'STOPPED';
1012
        },
1013

    
1014
        handle_stats_error: function() {
1015
            stats = {};
1016
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1017
                stats[k] = false;
1018
            });
1019

    
1020
            this.set({'stats': stats});
1021
        },
1022

    
1023
        // this method gets executed after a successful vm stats api call
1024
        handle_stats_update: function(data) {
1025
            var self = this;
1026
            // avoid browser caching
1027
            
1028
            if (data.stats && _.size(data.stats) > 0) {
1029
                var ts = $.now();
1030
                var stats = data.stats;
1031
                var images_loaded = 0;
1032
                var images = {};
1033

    
1034
                function check_images_loaded() {
1035
                    images_loaded++;
1036

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

    
1057
                    img.error(function() {
1058
                        images[stat + type] = false;
1059
                        check_images_loaded();
1060
                    });
1061

    
1062
                    img.attr({'src': stats[k]});
1063
                })
1064
                data.stats = stats;
1065
            }
1066

    
1067
            // do we need to change the interval ??
1068
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
1069
                this.stats_update_interval = data.stats.refresh * 1000;
1070
                this.stats_fetcher.interval = this.stats_update_interval;
1071
                this.stats_fetcher.maximum_interval = this.stats_update_interval;
1072
                this.stats_fetcher.stop();
1073
                this.stats_fetcher.start(false);
1074
            }
1075
        },
1076

    
1077
        // helper method that sets the do_update_stats
1078
        // in the future this method could also make an api call
1079
        // immediaetly if needed
1080
        enable_stats_update: function() {
1081
            this.do_update_stats = true;
1082
        },
1083
        
1084
        handle_destroy: function() {
1085
            this.stats_fetcher.stop();
1086
        },
1087

    
1088
        require_reboot: function() {
1089
            if (this.is_active()) {
1090
                this.set({'reboot_required': true});
1091
            }
1092
        },
1093
        
1094
        set_pending_action: function(data) {
1095
            this.pending_action = data;
1096
            return data;
1097
        },
1098

    
1099
        // machine has pending action
1100
        update_pending_action: function(action, force) {
1101
            this.set({pending_action: action});
1102
        },
1103

    
1104
        clear_pending_action: function() {
1105
            this.set({pending_action: undefined});
1106
        },
1107

    
1108
        has_pending_action: function() {
1109
            return this.get("pending_action") ? this.get("pending_action") : false;
1110
        },
1111
        
1112
        // machine is active
1113
        is_active: function() {
1114
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
1115
        },
1116
        
1117
        // machine is building 
1118
        is_building: function() {
1119
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
1120
        },
1121
        
1122
        is_rebooting: function() {
1123
            return this.state() == 'REBOOT';
1124
        },
1125

    
1126
        in_error_state: function() {
1127
            return this.state() === "ERROR"
1128
        },
1129

    
1130
        // user can connect to machine
1131
        is_connectable: function() {
1132
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1133
        },
1134
        
1135
        remove_meta: function(key, complete, error) {
1136
            var url = this.api_path() + "/metadata/" + key;
1137
            this.api_call(url, "delete", undefined, complete, error);
1138
        },
1139

    
1140
        save_meta: function(meta, complete, error) {
1141
            var url = this.api_path() + "/metadata/" + meta.key;
1142
            var payload = {meta:{}};
1143
            payload.meta[meta.key] = meta.value;
1144
            payload._options = {
1145
                critical:false, 
1146
                error_params: {
1147
                    title: "Machine metadata error",
1148
                    extra_details: {"Machine id": this.id}
1149
            }};
1150

    
1151
            this.api_call(url, "update", payload, complete, error);
1152
        },
1153

    
1154

    
1155
        // update/get the state of the machine
1156
        state: function() {
1157
            var args = slice.call(arguments);
1158
                
1159
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1160
                this.set({'state': args[0]});
1161
            }
1162

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

    
1213
        get_resize_flavors: function() {
1214
          var vm_flavor = this.get_flavor();
1215
          var flavors = synnefo.storage.flavors.filter(function(f){
1216
              return f.get('disk_template') ==
1217
              vm_flavor.get('disk_template') && f.get('disk') ==
1218
              vm_flavor.get('disk');
1219
          });
1220
          return flavors;
1221
        },
1222

    
1223
        get_flavor_quotas: function() {
1224
          var flavor = this.get_flavor();
1225
          return {
1226
            cpu: flavor.get('cpu') + 1, 
1227
            ram: flavor.get_ram_size() + 1, 
1228
            disk:flavor.get_disk_size() + 1
1229
          }
1230
        },
1231

    
1232
        get_meta: function(key, deflt) {
1233
            if (this.get('metadata') && this.get('metadata')) {
1234
                if (!this.get('metadata')[key]) { return deflt }
1235
                return _.escape(this.get('metadata')[key]);
1236
            } else {
1237
                return deflt;
1238
            }
1239
        },
1240

    
1241
        get_meta_keys: function() {
1242
            if (this.get('metadata') && this.get('metadata')) {
1243
                return _.keys(this.get('metadata'));
1244
            } else {
1245
                return [];
1246
            }
1247
        },
1248
        
1249
        // get metadata OS value
1250
        get_os: function() {
1251
            var image = this.get_image();
1252
            return this.get_meta('OS') || (image ? 
1253
                                            image.get_os() || "okeanos" : "okeanos");
1254
        },
1255

    
1256
        get_gui: function() {
1257
            return this.get_meta('GUI');
1258
        },
1259
        
1260
        get_hostname: function() {
1261
          var hostname = this.get_meta('hostname');
1262
          if (!hostname) {
1263
            if (synnefo.config.vm_hostname_format) {
1264
              hostname = synnefo.config.vm_hostname_format.format(this.id);
1265
            } else {
1266
              // TODO: resolve public ip
1267
              hostname = 'PUBLIC NIC';
1268
            }
1269
          }
1270
          return hostname;
1271
        },
1272

    
1273
        // get actions that the user can execute
1274
        // depending on the vm state/status
1275
        get_available_actions: function() {
1276
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1277
        },
1278

    
1279
        set_profile: function(profile, net_id) {
1280
        },
1281
        
1282
        // call rename api
1283
        rename: function(new_name) {
1284
            //this.set({'name': new_name});
1285
            this.sync("update", this, {
1286
                critical: true,
1287
                data: {
1288
                    'server': {
1289
                        'name': new_name
1290
                    }
1291
                }, 
1292
                success: _.bind(function(){
1293
                    snf.api.trigger("call");
1294
                }, this)
1295
            });
1296
        },
1297
        
1298
        get_console_url: function(data) {
1299
            var url_params = {
1300
                machine: this.get("name"),
1301
                host_ip: this.get_hostname(),
1302
                host_ip_v6: this.get_hostname(),
1303
                host: data.host,
1304
                port: data.port,
1305
                password: data.password
1306
            }
1307
            return synnefo.config.ui_console_url + '?' + $.param(url_params);
1308
        },
1309
      
1310
        connect_floating_ip: function(ip, cb) {
1311
          synnefo.storage.ports.create({
1312
            port: {
1313
              network_id: ip.get('floating_network_id'),
1314
              device_id: this.id,
1315
              fixed_ips: [{'ip_address': ip.get('floating_ip_address')}]
1316
            }
1317
          }, {complete: cb, skip_api_error: false})
1318
          // TODO: Implement
1319
        },
1320

    
1321
        // action helper
1322
        call: function(action_name, success, error, params) {
1323
            var id_param = [this.id];
1324
            
1325
            params = params || {};
1326
            success = success || function() {};
1327
            error = error || function() {};
1328

    
1329
            var self = this;
1330

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

    
1387
                                         },  
1388
                                         error, 'destroy', params);
1389
                    break;
1390
                case 'resize':
1391
                    this.__make_api_call(this.get_action_url(), // vm actions url
1392
                                         "create", // create so that sync later uses POST to make the call
1393
                                         {resize: {flavorRef:params.flavor}}, // payload
1394
                                         function() {
1395
                                             self.state('RESIZE');
1396
                                             success.apply(this, arguments);
1397
                                             snf.api.trigger("call");
1398
                                         },  
1399
                                         error, 'resize', params);
1400
                    break;
1401
                case 'addFloatingIp':
1402
                    this.__make_api_call(this.get_action_url(), // vm actions url
1403
                                         "create", // create so that sync later uses POST to make the call
1404
                                         {addFloatingIp: {address:params.address}}, // payload
1405
                                         function() {
1406
                                             self.state('CONNECT');
1407
                                             success.apply(this, arguments);
1408
                                             snf.api.trigger("call");
1409
                                         },  
1410
                                         error, 'addFloatingIp', params);
1411
                    break;
1412
                case 'removeFloatingIp':
1413
                    this.__make_api_call(this.get_action_url(), // vm actions url
1414
                                         "create", // create so that sync later uses POST to make the call
1415
                                         {removeFloatingIp: {address:params.address}}, // payload
1416
                                         function() {
1417
                                             self.state('DISCONNECT');
1418
                                             success.apply(this, arguments);
1419
                                             snf.api.trigger("call");
1420
                                         },  
1421
                                         error, 'addFloatingIp', params);
1422
                    break;
1423
                case 'destroy':
1424
                    this.__make_api_call(this.url(), // vm actions url
1425
                                         "delete", // create so that sync later uses POST to make the call
1426
                                         undefined, // payload
1427
                                         function() {
1428
                                             // set state after successful call
1429
                                             self.state('DESTROY');
1430
                                             success.apply(this, arguments);
1431
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1432

    
1433
                                         },  
1434
                                         error, 'destroy', params);
1435
                    break;
1436
                default:
1437
                    throw "Invalid VM action ("+action_name+")";
1438
            }
1439
        },
1440
        
1441
        __make_api_call: function(url, method, data, success, error, action, 
1442
                                  extra_params) {
1443
            var self = this;
1444
            error = error || function(){};
1445
            success = success || function(){};
1446

    
1447
            var params = {
1448
                url: url,
1449
                data: data,
1450
                success: function() { 
1451
                  self.handle_action_succeed.apply(self, arguments); 
1452
                  success.apply(this, arguments)
1453
                },
1454
                error: function() { 
1455
                  self.handle_action_fail.apply(self, arguments);
1456
                  error.apply(this, arguments)
1457
                },
1458
                error_params: { ns: "Machines actions", 
1459
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1460
                                extra_details: {
1461
                                  'Machine ID': this.id, 
1462
                                  'URL': url, 
1463
                                  'Action': action || "undefined" },
1464
                                allow_reload: false
1465
                              },
1466
                display: false,
1467
                critical: false
1468
            }
1469
            _.extend(params, extra_params);
1470
            this.sync(method, this, params);
1471
        },
1472

    
1473
        handle_action_succeed: function() {
1474
            this.trigger("action:success", arguments);
1475
        },
1476
        
1477
        reset_action_error: function() {
1478
            this.action_error = false;
1479
            this.trigger("action:fail:reset", this.action_error);
1480
        },
1481

    
1482
        handle_action_fail: function() {
1483
            this.action_error = arguments;
1484
            this.trigger("action:fail", arguments);
1485
        },
1486

    
1487
        get_action_url: function(name) {
1488
            return this.url() + "/action";
1489
        },
1490

    
1491
        get_diagnostics_url: function() {
1492
            return this.url() + "/diagnostics";
1493
        },
1494

    
1495
        get_users: function() {
1496
            var image;
1497
            var users = [];
1498
            try {
1499
              var users = this.get_meta('users').split(" ");
1500
            } catch (err) { users = null }
1501
            if (!users) {
1502
              image = this.get_image();
1503
              if (image) {
1504
                  users = image.get_created_users();
1505
              }
1506
            }
1507
            return users;
1508
        },
1509

    
1510
        get_connection_info: function(host_os, success, error) {
1511
            var url = synnefo.config.ui_connect_url;
1512
            var users = this.get_users();
1513

    
1514
            params = {
1515
                ip_address: this.get_hostname(),
1516
                hostname: this.get_hostname(),
1517
                os: this.get_os(),
1518
                host_os: host_os,
1519
                srv: this.id
1520
            }
1521
            
1522
            if (users.length) { 
1523
                params['username'] = _.last(users)
1524
            }
1525

    
1526
            url = url + "?" + $.param(params);
1527

    
1528
            var ajax = snf.api.sync("read", undefined, { url: url, 
1529
                                                         error:error, 
1530
                                                         success:success, 
1531
                                                         handles_error:1});
1532
        }
1533
    });
1534
    
1535
    models.VM.ACTIONS = [
1536
        'start',
1537
        'shutdown',
1538
        'reboot',
1539
        'console',
1540
        'destroy',
1541
        'resize'
1542
    ]
1543

    
1544
    models.VM.TASK_STATE_STATUS_MAP = {
1545
      'BULDING': 'BUILD',
1546
      'REBOOTING': 'REBOOT',
1547
      'STOPPING': 'SHUTDOWN',
1548
      'STARTING': 'START',
1549
      'RESIZING': 'RESIZE',
1550
      'CONNECTING': 'CONNECT',
1551
      'DISCONNECTING': 'DISCONNECT',
1552
      'DESTROYING': 'DESTROY'
1553
    }
1554

    
1555
    models.VM.AVAILABLE_ACTIONS = {
1556
        'UNKNWON'       : ['destroy'],
1557
        'BUILD'         : ['destroy'],
1558
        'REBOOT'        : ['destroy'],
1559
        'STOPPED'       : ['start', 'destroy', 'resize'],
1560
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console', 'resize'],
1561
        'ERROR'         : ['destroy'],
1562
        'DELETED'       : ['destroy'],
1563
        'DESTROY'       : ['destroy'],
1564
        'SHUTDOWN'      : ['destroy'],
1565
        'START'         : ['destroy'],
1566
        'CONNECT'       : ['destroy'],
1567
        'DISCONNECT'    : ['destroy'],
1568
        'RESIZE'        : ['destroy']
1569
    }
1570
    
1571
    models.VM.AVAILABLE_ACTIONS_INACTIVE = {
1572
      'resize': ['ACTIVE']
1573
    }
1574

    
1575
    // api status values
1576
    models.VM.STATUSES = [
1577
        'UNKNWON',
1578
        'BUILD',
1579
        'REBOOT',
1580
        'STOPPED',
1581
        'ACTIVE',
1582
        'ERROR',
1583
        'DELETED',
1584
        'RESIZE'
1585
    ]
1586

    
1587
    // api status values
1588
    models.VM.CONNECT_STATES = [
1589
        'ACTIVE',
1590
        'REBOOT',
1591
        'SHUTDOWN'
1592
    ]
1593

    
1594
    // vm states
1595
    models.VM.STATES = models.VM.STATUSES.concat([
1596
        'DESTROY',
1597
        'SHUTDOWN',
1598
        'START',
1599
        'CONNECT',
1600
        'DISCONNECT',
1601
        'FIREWALL',
1602
        'RESIZE'
1603
    ]);
1604
    
1605
    models.VM.STATES_TRANSITIONS = {
1606
        'DESTROY' : ['DELETED'],
1607
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1608
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1609
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1610
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1611
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1612
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1613
        'RESIZE': ['ERROR', 'STOPPED']
1614
    }
1615

    
1616
    models.VM.TRANSITION_STATES = [
1617
        'DESTROY',
1618
        'SHUTDOWN',
1619
        'START',
1620
        'REBOOT',
1621
        'BUILD',
1622
        'RESIZE'
1623
    ]
1624

    
1625
    models.VM.ACTIVE_STATES = [
1626
        'BUILD', 'REBOOT', 'ACTIVE',
1627
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1628
    ]
1629

    
1630
    models.VM.BUILDING_STATES = [
1631
        'BUILD'
1632
    ]
1633

    
1634
    models.Images = models.Collection.extend({
1635
        model: models.Image,
1636
        path: 'images',
1637
        details: true,
1638
        noUpdate: true,
1639
        supportIncUpdates: false,
1640
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1641
        meta_labels: {},
1642
        read_method: 'read',
1643

    
1644
        // update collection model with id passed
1645
        // making a direct call to the image
1646
        // api url
1647
        update_unknown_id: function(id, callback) {
1648
            var url = getUrl.call(this) + "/" + id;
1649
            this.api_call(this.path + "/" + id, this.read_method, {
1650
              _options:{
1651
                async:true, 
1652
                skip_api_error:true}
1653
              }, undefined, 
1654
            _.bind(function() {
1655
                if (!this.get(id)) {
1656
                            if (this.fallback_service) {
1657
                        // if current service has fallback_service attribute set
1658
                        // use this service to retrieve the missing image model
1659
                        var tmpservice = new this.fallback_service();
1660
                        tmpservice.update_unknown_id(id, _.bind(function(img){
1661
                            img.attributes.status = "DELETED";
1662
                            this.add(img.attributes);
1663
                            callback(this.get(id));
1664
                        }, this));
1665
                    } else {
1666
                        var title = synnefo.config.image_deleted_title || 'Deleted';
1667
                        // else add a dummy DELETED state image entry
1668
                        this.add({id:id, name:title, size:-1, 
1669
                                  progress:100, status:"DELETED"});
1670
                        callback(this.get(id));
1671
                    }   
1672
                } else {
1673
                    callback(this.get(id));
1674
                }
1675
            }, this), _.bind(function(image, msg, xhr) {
1676
                if (!image) {
1677
                    var title = synnefo.config.image_deleted_title || 'Deleted';
1678
                    this.add({id:id, name:title, size:-1, 
1679
                              progress:100, status:"DELETED"});
1680
                    callback(this.get(id));
1681
                    return;
1682
                }
1683
                var img_data = this._read_image_from_request(image, msg, xhr);
1684
                this.add(img_data);
1685
                callback(this.get(id));
1686
            }, this));
1687
        },
1688

    
1689
        _read_image_from_request: function(image, msg, xhr) {
1690
            return image.image;
1691
        },
1692

    
1693
        parse: function (resp, xhr) {
1694
            var parsed = _.map(resp.images, _.bind(this.parse_meta, this));
1695
            parsed = this.fill_owners(parsed);
1696
            return parsed;
1697
        },
1698

    
1699
        fill_owners: function(images) {
1700
            // do translate uuid->displayname if needed
1701
            // store display name in owner attribute for compatibility
1702
            var uuids = [];
1703

    
1704
            var images = _.map(images, function(img, index) {
1705
                if (synnefo.config.translate_uuids) {
1706
                    uuids.push(img['owner']);
1707
                }
1708
                img['owner_uuid'] = img['owner'];
1709
                return img;
1710
            });
1711
            
1712
            if (uuids.length > 0) {
1713
                var handle_results = function(data) {
1714
                    _.each(images, function (img) {
1715
                        img['owner'] = data.uuid_catalog[img['owner_uuid']];
1716
                    });
1717
                }
1718
                // notice the async false
1719
                var uuid_map = this.translate_uuids(uuids, false, 
1720
                                                    handle_results)
1721
            }
1722
            return images;
1723
        },
1724

    
1725
        translate_uuids: function(uuids, async, cb) {
1726
            var url = synnefo.config.user_catalog_url;
1727
            var data = JSON.stringify({'uuids': uuids});
1728
          
1729
            // post to user_catalogs api
1730
            snf.api.sync('create', undefined, {
1731
                url: url,
1732
                data: data,
1733
                async: async,
1734
                success:  cb
1735
            });
1736
        },
1737

    
1738
        get_meta_key: function(img, key) {
1739
            if (img.metadata && img.metadata && img.metadata[key]) {
1740
                return _.escape(img.metadata[key]);
1741
            }
1742
            return undefined;
1743
        },
1744

    
1745
        comparator: function(img) {
1746
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1747
        },
1748

    
1749
        parse_meta: function(img) {
1750
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1751
                if (img[key]) { return };
1752
                img[key] = this.get_meta_key(img, key) || "";
1753
            }, this));
1754
            return img;
1755
        },
1756

    
1757
        active: function() {
1758
            return this.filter(function(img){return img.get('status') != "DELETED"});
1759
        },
1760

    
1761
        predefined: function() {
1762
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
1763
        },
1764
        
1765
        fetch_for_type: function(type, complete, error) {
1766
            this.fetch({update:true, 
1767
                        success: complete, 
1768
                        error: error, 
1769
                        skip_api_error: true });
1770
        },
1771
        
1772
        get_images_for_type: function(type) {
1773
            if (this['get_{0}_images'.format(type)]) {
1774
                return this['get_{0}_images'.format(type)]();
1775
            }
1776

    
1777
            return this.active();
1778
        },
1779

    
1780
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
1781
            var load = false;
1782
            error = onError || function() {};
1783
            function complete(collection) { 
1784
                onComplete(collection.get_images_for_type(type)); 
1785
            }
1786
            
1787
            // do we need to fetch/update current collection entries
1788
            if (load) {
1789
                onStart();
1790
                this.fetch_for_type(type, complete, error);
1791
            } else {
1792
                // fallback to complete
1793
                complete(this);
1794
            }
1795
        }
1796
    })
1797

    
1798
    models.Flavors = models.Collection.extend({
1799
        model: models.Flavor,
1800
        path: 'flavors',
1801
        details: true,
1802
        noUpdate: true,
1803
        supportIncUpdates: false,
1804
        // update collection model with id passed
1805
        // making a direct call to the flavor
1806
        // api url
1807
        update_unknown_id: function(id, callback) {
1808
            var url = getUrl.call(this) + "/" + id;
1809
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
1810
            _.bind(function() {
1811
                this.add({id:id, cpu:"Unknown", ram:"Unknown", disk:"Unknown", name: "Unknown", status:"DELETED"})
1812
            }, this), _.bind(function(flv) {
1813
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1814
                this.add(flv.flavor);
1815
            }, this));
1816
        },
1817

    
1818
        parse: function (resp, xhr) {
1819
            return _.map(resp.flavors, function(o) {
1820
              o.cpu = o['vcpus'];
1821
              o.disk_template = o['SNF:disk_template'];
1822
              return o
1823
            });
1824
        },
1825

    
1826
        comparator: function(flv) {
1827
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1828
        },
1829
          
1830
        unavailable_values_for_quotas: function(quotas, flavors, extra) {
1831
            var flavors = flavors || this.active();
1832
            var index = {cpu:[], disk:[], ram:[]};
1833
            var extra = extra == undefined ? {cpu:0, disk:0, ram:0} : extra;
1834
            
1835
            _.each(flavors, function(el) {
1836

    
1837
                var disk_available = quotas['disk'] + extra.disk;
1838
                var disk_size = el.get_disk_size();
1839
                if (index.disk.indexOf(disk_size) == -1) {
1840
                  var disk = el.disk_to_bytes();
1841
                  if (disk > disk_available) {
1842
                    index.disk.push(disk_size);
1843
                  }
1844
                }
1845
                
1846
                var ram_available = quotas['ram'] + extra.ram * 1024 * 1024;
1847
                var ram_size = el.get_ram_size();
1848
                if (index.ram.indexOf(ram_size) == -1) {
1849
                  var ram = el.ram_to_bytes();
1850
                  if (ram > ram_available) {
1851
                    index.ram.push(el.get('ram'))
1852
                  }
1853
                }
1854

    
1855
                var cpu = el.get('cpu');
1856
                var cpu_available = quotas['cpu'] + extra.cpu;
1857
                if (index.cpu.indexOf(cpu) == -1) {
1858
                  if (cpu > cpu_available) {
1859
                    index.cpu.push(el.get('cpu'))
1860
                  }
1861
                }
1862
            });
1863
            return index;
1864
        },
1865

    
1866
        unavailable_values_for_image: function(img, flavors) {
1867
            var flavors = flavors || this.active();
1868
            var size = img.get_size();
1869
            
1870
            var index = {cpu:[], disk:[], ram:[]};
1871

    
1872
            _.each(this.active(), function(el) {
1873
                var img_size = size;
1874
                var flv_size = el.get_disk_size();
1875
                if (flv_size < img_size) {
1876
                    if (index.disk.indexOf(flv_size) == -1) {
1877
                        index.disk.push(flv_size);
1878
                    }
1879
                };
1880
            });
1881
            
1882
            return index;
1883
        },
1884

    
1885
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
1886
            if (!filter_list) { filter_list = this.models };
1887
            
1888
            return this.select(function(flv){
1889
                if (flv.get("cpu") == cpu + "" &&
1890
                   flv.get("ram") == mem + "" &&
1891
                   flv.get("disk") == disk + "" &&
1892
                   flv.get("disk_template") == disk_template &&
1893
                   filter_list.indexOf(flv) > -1) { return true; }
1894
            })[0];
1895
        },
1896
        
1897
        get_data: function(lst) {
1898
            var data = {'cpu': [], 'mem':[], 'disk':[], 'disk_template':[]};
1899

    
1900
            _.each(lst, function(flv) {
1901
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1902
                    data.cpu.push(flv.get("cpu"));
1903
                }
1904
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1905
                    data.mem.push(flv.get("ram"));
1906
                }
1907
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1908
                    data.disk.push(flv.get("disk"));
1909
                }
1910
                if (data.disk_template.indexOf(flv.get("disk_template")) == -1) {
1911
                    data.disk_template.push(flv.get("disk_template"));
1912
                }
1913
            })
1914
            
1915
            return data;
1916
        },
1917

    
1918
        active: function() {
1919
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1920
        }
1921
            
1922
    })
1923

    
1924
    models.VMS = models.Collection.extend({
1925
        model: models.VM,
1926
        path: 'servers',
1927
        details: true,
1928
        copy_image_meta: true,
1929

    
1930
        parse: function (resp, xhr) {
1931
            var data = resp;
1932
            if (!resp) { return [] };
1933
            data = _.filter(_.map(resp.servers, 
1934
                                  _.bind(this.parse_vm_api_data, this)), 
1935
                                  function(v){return v});
1936
            return data;
1937
        },
1938

    
1939
        parse_vm_api_data: function(data) {
1940
            // do not add non existing DELETED entries
1941
            if (data.status && data.status == "DELETED") {
1942
                if (!this.get(data.id)) {
1943
                    return false;
1944
                }
1945
            }
1946
            
1947
            if ('SNF:task_state' in data) { 
1948
                data['task_state'] = data['SNF:task_state'];
1949
                if (data['task_state']) {
1950
                    var status = models.VM.TASK_STATE_STATUS_MAP[data['task_state']];
1951
                    if (status) { data['status'] = status }
1952
                }
1953
            }
1954

    
1955
            // OS attribute
1956
            if (this.has_meta(data)) {
1957
                data['OS'] = data.metadata.OS || snf.config.unknown_os;
1958
            }
1959
            
1960
            if (!data.diagnostics) {
1961
                data.diagnostics = [];
1962
            }
1963

    
1964
            // network metadata
1965
            data['firewalls'] = {};
1966

    
1967
            data['fqdn'] = synnefo.config.vm_hostname_format.format(data['id']);
1968

    
1969
            // if vm has no metadata, no metadata object
1970
            // is in json response, reset it to force
1971
            // value update
1972
            if (!data['metadata']) {
1973
                data['metadata'] = {};
1974
            }
1975
            
1976
            // v2.0 API returns objects
1977
            data.image_obj = data.image;
1978
            data.image = data.image_obj.id;
1979
            data.flavor_obj = data.flavor;
1980
            data.flavor = data.flavor_obj.id;
1981

    
1982
            return data;
1983
        },
1984

    
1985
        get_reboot_required: function() {
1986
            return this.filter(function(vm){return vm.get("reboot_required") == true})
1987
        },
1988

    
1989
        has_pending_actions: function() {
1990
            return this.filter(function(vm){return vm.pending_action}).length > 0;
1991
        },
1992

    
1993
        reset_pending_actions: function() {
1994
            this.each(function(vm) {
1995
                vm.clear_pending_action();
1996
            })
1997
        },
1998

    
1999
        do_all_pending_actions: function(success, error) {
2000
            this.each(function(vm) {
2001
                if (vm.has_pending_action()) {
2002
                    vm.call(vm.pending_action, success, error);
2003
                    vm.clear_pending_action();
2004
                }
2005
            })
2006
        },
2007
        
2008
        do_all_reboots: function(success, error) {
2009
            this.each(function(vm) {
2010
                if (vm.get("reboot_required")) {
2011
                    vm.call("reboot", success, error);
2012
                }
2013
            });
2014
        },
2015

    
2016
        reset_reboot_required: function() {
2017
            this.each(function(vm) {
2018
                vm.set({'reboot_required': undefined});
2019
            })
2020
        },
2021
        
2022
        stop_stats_update: function(exclude) {
2023
            var exclude = exclude || [];
2024
            this.each(function(vm) {
2025
                if (exclude.indexOf(vm) > -1) {
2026
                    return;
2027
                }
2028
                vm.stop_stats_update();
2029
            })
2030
        },
2031
        
2032
        has_meta: function(vm_data) {
2033
            return vm_data.metadata && vm_data.metadata
2034
        },
2035

    
2036
        has_addresses: function(vm_data) {
2037
            return vm_data.metadata && vm_data.metadata
2038
        },
2039

    
2040
        create: function (name, image, flavor, meta, extra, callback) {
2041

    
2042
            if (this.copy_image_meta) {
2043
                if (synnefo.config.vm_image_common_metadata) {
2044
                    _.each(synnefo.config.vm_image_common_metadata, 
2045
                        function(key){
2046
                            if (image.get_meta(key)) {
2047
                                meta[key] = image.get_meta(key);
2048
                            }
2049
                    });
2050
                }
2051

    
2052
                if (image.get("OS")) {
2053
                    meta['OS'] = image.get("OS");
2054
                }
2055
            }
2056
            
2057
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, 
2058
                    metadata:meta}
2059
            opts = _.extend(opts, extra);
2060
            
2061
            var cb = function(data) {
2062
              synnefo.storage.quotas.get('cyclades.vm').increase();
2063
              callback(data);
2064
            }
2065

    
2066
            this.api_call(this.path, "create", {'server': opts}, undefined, 
2067
                          undefined, cb, {critical: true});
2068
        },
2069

    
2070
        load_missing_images: function(callback) {
2071
          var missing_ids = [];
2072
          var resolved = 0;
2073

    
2074
          // fill missing_ids
2075
          this.each(function(el) {
2076
            var imgid = el.get("image");
2077
            var existing = synnefo.storage.images.get(imgid);
2078
            if (!existing && missing_ids.indexOf(imgid) == -1) {
2079
              missing_ids.push(imgid);
2080
            }
2081
          });
2082
          var check = function() {
2083
            // once all missing ids where resolved continue calling the 
2084
            // callback
2085
            resolved++;
2086
            if (resolved == missing_ids.length) {
2087
              callback(missing_ids)
2088
            }
2089
          }
2090
          if (missing_ids.length == 0) {
2091
            callback(missing_ids);
2092
            return;
2093
          }
2094
          // start resolving missing image ids
2095
          _(missing_ids).each(function(imgid){
2096
            synnefo.storage.images.update_unknown_id(imgid, check);
2097
          });
2098
        },
2099

    
2100
        get_connectable: function() {
2101
            return storage.vms.filter(function(vm){
2102
                return !vm.in_error_state() && !vm.is_building();
2103
            });
2104
        }
2105
    })
2106
    
2107
    models.PublicKey = models.Model.extend({
2108
        path: 'keys',
2109
        api_type: 'userdata',
2110
        detail: false,
2111
        model_actions: {
2112
          'remove': [['name'], function() {
2113
            return true;
2114
          }]
2115
        },
2116

    
2117
        get_public_key: function() {
2118
            return cryptico.publicKeyFromString(this.get("content"));
2119
        },
2120

    
2121
        get_filename: function() {
2122
            return "{0}.pub".format(this.get("name"));
2123
        },
2124

    
2125
        identify_type: function() {
2126
            try {
2127
                var cont = snf.util.validatePublicKey(this.get("content"));
2128
                var type = cont.split(" ")[0];
2129
                return synnefo.util.publicKeyTypesMap[type];
2130
            } catch (err) { return false };
2131
        },
2132

    
2133
        rename: function(new_name) {
2134
          //this.set({'name': new_name});
2135
          this.sync("update", this, {
2136
            critical: true,
2137
            data: {'name': new_name}, 
2138
            success: _.bind(function(){
2139
              snf.api.trigger("call");
2140
            }, this)
2141
          });
2142
        }
2143
    })
2144
    
2145
    models._ActionsModel = models.Model.extend({
2146
      defaults: { pending: null },
2147
      actions: [],
2148
      status: {
2149
        INACTIVE: 0,
2150
        PENDING: 1,
2151
        CALLED: 2
2152
      },
2153

    
2154
      initialize: function(attrs, opts) {
2155
        models._ActionsModel.__super__.initialize.call(this, attrs);
2156
        this.actions = opts.actions;
2157
        this.model = opts.model;
2158
        this.bind("change", function() {
2159
          this.set({'pending': this.get_pending()});
2160
        }, this);
2161
        this.clear();
2162
      },
2163
      
2164
      _in_status: function(st) {
2165
        var actions = null;
2166
        _.each(this.attributes, function(status, action){
2167
          if (status == st) {
2168
            if (!actions) {
2169
              actions = []
2170
            }
2171
            actions.push(action);
2172
          }
2173
        });
2174
        return actions;
2175
      },
2176

    
2177
      get_pending: function() {
2178
        return this._in_status(this.status.PENDING);
2179
      },
2180

    
2181
      unset_pending_action: function(action) {
2182
        var data = {};
2183
        data[action] = this.status.INACTIVE;
2184
        this.set(data);
2185
      },
2186

    
2187
      set_pending_action: function(action, reset_pending) {
2188
        reset_pending = reset_pending === undefined ? true : reset_pending;
2189
        var data = {};
2190
        data[action] = this.status.PENDING;
2191
        if (reset_pending) {
2192
          this.reset_pending();
2193
        }
2194
        this.set(data);
2195
      },
2196
      
2197
      reset_pending: function() {
2198
        var data = {};
2199
        _.each(this.actions, function(action) {
2200
          data[action] = this.status.INACTIVE;
2201
        }, this);
2202
        this.set(data);
2203
      }
2204
    });
2205

    
2206
    models.PublicPool = models.Model.extend({});
2207
    models.PublicPools = models.Collection.extend({
2208
      model: models.PublicPool,
2209
      path: 'os-floating-ip-pools',
2210
      api_type: 'compute',
2211
      noUpdate: true,
2212

    
2213
      parse: function(data) {
2214
        return _.map(data.floating_ip_pools, function(pool) {
2215
          pool.id = pool.name;
2216
          return pool;
2217
        });
2218
      }
2219
    });
2220

    
2221
    models.PublicKeys = models.Collection.extend({
2222
        model: models.PublicKey,
2223
        details: false,
2224
        path: 'keys',
2225
        api_type: 'userdata',
2226
        noUpdate: true,
2227
        updateEntries: true,
2228

    
2229
        generate_new: function(success, error) {
2230
            snf.api.sync('create', undefined, {
2231
                url: getUrl.call(this, this.base_url) + "/generate", 
2232
                success: success, 
2233
                error: error,
2234
                skip_api_error: true
2235
            });
2236
        },
2237
        
2238
        add_crypto_key: function(key, success, error, options) {
2239
            var options = options || {};
2240
            var m = new models.PublicKey();
2241

    
2242
            // guess a name
2243
            var name_tpl = "my generated public key";
2244
            var name = name_tpl;
2245
            var name_count = 1;
2246
            
2247
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
2248
                name = name_tpl + " " + name_count;
2249
                name_count++;
2250
            }
2251
            
2252
            m.set({name: name});
2253
            m.set({content: key});
2254
            
2255
            options.success = function () { return success(m) };
2256
            options.errror = error;
2257
            options.skip_api_error = true;
2258
            
2259
            this.create(m.attributes, options);
2260
        }
2261
    });
2262

    
2263
  
2264
    models.Quota = models.Model.extend({
2265

    
2266
        initialize: function() {
2267
            models.Quota.__super__.initialize.apply(this, arguments);
2268
            this.bind("change", this.check, this);
2269
            this.check();
2270
        },
2271
        
2272
        check: function() {
2273
            var usage, limit;
2274
            usage = this.get('usage');
2275
            limit = this.get('limit');
2276
            if (usage >= limit) {
2277
                this.trigger("available");
2278
            } else {
2279
                this.trigger("unavailable");
2280
            }
2281
        },
2282

    
2283
        increase: function(val) {
2284
            if (val === undefined) { val = 1};
2285
            this.set({'usage': this.get('usage') + val})
2286
        },
2287

    
2288
        decrease: function(val) {
2289
            if (val === undefined) { val = 1};
2290
            this.set({'usage': this.get('usage') - val})
2291
        },
2292

    
2293
        can_consume: function() {
2294
            var usage, limit;
2295
            usage = this.get('usage');
2296
            limit = this.get('limit');
2297
            if (usage >= limit) {
2298
                return false
2299
            } else {
2300
                return true
2301
            }
2302
        },
2303
        
2304
        is_bytes: function() {
2305
            return this.get('resource').get('unit') == 'bytes';
2306
        },
2307
        
2308
        get_available: function(active) {
2309
            suffix = '';
2310
            if (active) { suffix = '_active'}
2311
            var value = this.get('limit'+suffix) - this.get('usage'+suffix);
2312
            if (active) {
2313
              if (this.get('available') <= value) {
2314
                value = this.get('available');
2315
              }
2316
            }
2317
            if (value < 0) { return value }
2318
            return value
2319
        },
2320

    
2321
        get_readable: function(key, active) {
2322
            var value;
2323
            if (key == 'available') {
2324
                value = this.get_available(active);
2325
            } else {
2326
                value = this.get(key)
2327
            }
2328
            if (value <= 0) { value = 0 }
2329
            if (!this.is_bytes()) {
2330
              return value + "";
2331
            }
2332
            return snf.util.readablizeBytes(value);
2333
        }
2334
    });
2335

    
2336
    models.Quotas = models.Collection.extend({
2337
        model: models.Quota,
2338
        api_type: 'accounts',
2339
        path: 'quotas',
2340
        parse: function(resp) {
2341
            filtered = _.map(resp.system, function(value, key) {
2342
                var available = (value.limit - value.usage) || 0;
2343
                var available_active = available;
2344
                var keysplit = key.split(".");
2345
                var limit_active = value.limit;
2346
                var usage_active = value.usage;
2347
                keysplit[keysplit.length-1] = "active_" + keysplit[keysplit.length-1];
2348
                var activekey = keysplit.join(".");
2349
                var exists = resp.system[activekey];
2350
                if (exists) {
2351
                    available_active = exists.limit - exists.usage;
2352
                    limit_active = exists.limit;
2353
                    usage_active = exists.usage;
2354
                }
2355
                return _.extend(value, {'name': key, 'id': key, 
2356
                          'available': available,
2357
                          'available_active': available_active,
2358
                          'limit_active': limit_active,
2359
                          'usage_active': usage_active,
2360
                          'resource': snf.storage.resources.get(key)});
2361
            });
2362
            return filtered;
2363
        },
2364
        
2365
        get_by_id: function(k) {
2366
          return this.filter(function(q) { return q.get('name') == k})[0]
2367
        },
2368

    
2369
        get_available_for_vm: function(options) {
2370
          var quotas = synnefo.storage.quotas;
2371
          var key = 'available';
2372
          var available_quota = {};
2373
          _.each(['cyclades.ram', 'cyclades.cpu', 'cyclades.disk'], 
2374
            function (key) {
2375
              var value = quotas.get(key).get_available(true);
2376
              available_quota[key.replace('cyclades.', '')] = value;
2377
          });
2378
          return available_quota;
2379
        }
2380
    })
2381

    
2382
    models.Resource = models.Model.extend({
2383
        api_type: 'accounts',
2384
        path: 'resources'
2385
    });
2386

    
2387
    models.Resources = models.Collection.extend({
2388
        api_type: 'accounts',
2389
        path: 'resources',
2390
        model: models.Network,
2391

    
2392
        parse: function(resp) {
2393
            return _.map(resp, function(value, key) {
2394
                return _.extend(value, {'name': key, 'id': key});
2395
            })
2396
        }
2397
    });
2398
    
2399
    // storage initialization
2400
    snf.storage.images = new models.Images();
2401
    snf.storage.flavors = new models.Flavors();
2402
    snf.storage.vms = new models.VMS();
2403
    snf.storage.keys = new models.PublicKeys();
2404
    snf.storage.resources = new models.Resources();
2405
    snf.storage.quotas = new models.Quotas();
2406
    snf.storage.public_pools = new models.PublicPools();
2407

    
2408
})(this);