Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (85.1 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_to_resolve = function(store, val, key) {
167
                  var retries = 0;
168
                  var retry = window.setInterval(function(){
169
                    retries++;
170
                    if (retries > 200) {
171
                      clearInterval(retry);
172
                    }
173
                    var obj = store.get(val);
174
                    if (obj) {
175
                      data[key] = obj;
176
                      self.set(data, {silent:false});
177
                      clearInterval(retry);
178
                    }
179
                  }, 500);
180
                  return retry
181
                }
182
                retry_to_resolve(store, val, key);
183
              }
184
            }
185
            
186
            var self = this;
187
            function init_bindings(instance, store, key, attr, attr_resolver) {
188
              instance.bind('change:' + attr, function(model) {
189
                resolve_related_instance.call(model, store, key, attr_resolver(model, attr));
190
              }, this);
191

    
192
              instance.bind('add', function(model) {
193
                resolve_related_instance.call(model, store, key, attr_resolver(model, attr));
194
              }, this);
195
            }
196

    
197
            init_bindings(this, store, key, attr, attr_resolver);
198
            resolve_related_instance.call(this, store, key, attr_resolver(this, attr));
199
          }, this);
200
        },
201
        
202
        _proxy_model_cache: {},
203
        
204
        _bind_model: function(model, attr, check_attr, cb) {
205
          var proxy_cache_key = attr + '_' + check_attr;
206
          if (this._proxy_model_cache[proxy_cache_key]) {
207
            var proxy = this._proxy_model_cache[proxy_cache_key];
208
            proxy[0].unbind('change', proxy[1]);
209
          }
210
          var data = {};
211
          var changebind = _.bind(function() {
212
            data[attr] = cb.call(this, this.get(check_attr));
213
            this.set(data);
214
          }, this);
215
          model.bind('change', changebind);
216
          this._proxy_model_cache[proxy_cache_key] = [model, changebind];
217
        },
218

    
219
        _bind_attr: function(attr, check_attr, cb) {
220
          this.bind('change:' + check_attr, function() {
221
            if (this.get(check_attr) instanceof models.Model) {
222
              var model = this.get(check_attr);
223
              this._bind_model(model, attr, check_attr, cb);
224
            }
225
            var val = cb.call(this, this.get(check_attr));
226
            var data = {};
227
            if (this.get(attr) !== val) {
228
              data[attr] = val;
229
              this.set(data);
230
            }
231
          }, this);
232
        },
233

    
234
        _set_proxy_attr: function(attr, check_attr, cb) {
235
          // initial set
236
          var data = {};
237
          data[attr] = cb.call(this, this.get(check_attr));
238
          if (data[attr] !== undefined) {
239
            this.set(data, {silent:true});
240
          }
241
          if(this.get(check_attr) instanceof models.Model) {
242
            this._bind_model(this.get(check_attr), attr, check_attr, cb);
243
          }
244
          this._bind_attr(attr, check_attr, cb);
245
        },
246

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

    
281
        url: function(options) {
282
            return getUrl.call(this, this.base_url) + "/" + this.id;
283
        },
284

    
285
        api_path: function(options) {
286
            return this.path + "/" + this.id;
287
        },
288

    
289
        parse: function(resp, xhr) {
290
        },
291

    
292
        remove: function(complete, error, success) {
293
            this.api_call(this.api_path(), "delete", undefined, complete, error, success);
294
        },
295

    
296
        changedKeys: function() {
297
            return _.keys(this.changedAttributes() || {});
298
        },
299
            
300
        // return list of changed attributes that included in passed list
301
        // argument
302
        getKeysChanged: function(keys) {
303
            return _.intersection(keys, this.changedKeys());
304
        },
305
        
306
        // boolean check of keys changed
307
        keysChanged: function(keys) {
308
            return this.getKeysChanged(keys).length > 0;
309
        },
310

    
311
        // check if any of the passed attribues has changed
312
        hasOnlyChange: function(keys) {
313
            var ret = false;
314
            _.each(keys, _.bind(function(key) {
315
                if (this.changedKeys().length == 1 && this.changedKeys().indexOf(key) > -1) { ret = true};
316
            }, this));
317
            return ret;
318
        }
319

    
320
    })
321
    
322
    // Base object for all our model collections
323
    models.Collection = bb.Collection.extend({
324
        sync: snf.api.sync,
325
        api: snf.api,
326
        api_type: 'compute',
327
        supportIncUpdates: true,
328

    
329
        initialize: function() {
330
            models.Collection.__super__.initialize.apply(this, arguments);
331
            this.api_call = _.bind(this.api.call, this);
332
            if (this.sortFields) {
333
              _.each(this.sortFields, function(f) {
334
                this.bind("change:" + f, _.bind(this.resort, this));
335
              }, this);
336
            }
337
        },
338
          
339
        resort: function() {
340
          this.sort();
341
        },
342

    
343
        url: function(options, method) {
344
            return getUrl.call(this, this.base_url) + (
345
                    options.details || this.details && method != 'create' ? '/detail' : '');
346
        },
347

    
348
        fetch: function(options) {
349
            if (!options) { options = {} };
350
            // default to update
351
            if (!this.noUpdate) {
352
                if (options.update === undefined) { options.update = true };
353
                if (!options.removeMissing && options.refresh) { 
354
                  options.removeMissing = true;
355
                };
356
                // for collections which associated models don't support 
357
                // deleted state identification through attributes, resolve  
358
                // deleted entries by checking for missing objects in fetch 
359
                // responses.
360
                if (this.updateEntries && options.removeMissing === undefined) {
361
                  options.removeMissing = true;
362
                }
363
            } else {
364
                if (options.refresh === undefined) {
365
                    options.refresh = true;
366
                    if (this.updateEntries) {
367
                      options.update = true;
368
                      options.removeMissing = true;
369
                    }
370
                }
371
            }
372
            // custom event foreach fetch
373
            return bb.Collection.prototype.fetch.call(this, options)
374
        },
375

    
376
        create: function(model, options) {
377
            var coll = this;
378
            options || (options = {});
379
            model = this._prepareModel(model, options);
380
            if (!model) return false;
381
            var success = options.success;
382
            options.success = function(nextModel, resp, xhr) {
383
                if (coll.add_on_create) {
384
                  coll.add(nextModel, options);
385
                }
386
                if (success) success(nextModel, resp, xhr);
387
            };
388
            model.save(null, options);
389
            return model;
390
        },
391

    
392
        get_fetcher: function(interval, increase, fast, increase_after_calls, max, initial_call, params) {
393
            var fetch_params = params || {};
394
            var handler_options = {};
395

    
396
            fetch_params.skips_timeouts = true;
397
            handler_options.interval = interval;
398
            handler_options.increase = increase;
399
            handler_options.fast = fast;
400
            handler_options.increase_after_calls = increase_after_calls;
401
            handler_options.max= max;
402
            handler_options.id = "collection id";
403

    
404
            var last_ajax = undefined;
405
            var callback = _.bind(function() {
406
                // clone to avoid referenced objects
407
                var params = _.clone(fetch_params);
408
                updater._ajax = last_ajax;
409
                
410
                // wait for previous request to finish
411
                if (last_ajax && last_ajax.readyState < 4 && last_ajax.statusText != "timeout") {
412
                    // opera readystate for 304 responses is 0
413
                    if (!($.browser.opera && last_ajax.readyState == 0 && last_ajax.status == 304)) {
414
                        return;
415
                    }
416
                }
417
                last_ajax = this.fetch(params);
418
            }, this);
419
            handler_options.callback = callback;
420

    
421
            var updater = new snf.api.updateHandler(_.clone(_.extend(handler_options, fetch_params)));
422
            snf.api.bind("call", _.throttle(_.bind(function(){ updater.faster(true)}, this)), 1000);
423
            return updater;
424
        }
425
    });
426
    
427
    // Image model
428
    models.Image = models.Model.extend({
429
        path: 'images',
430
        
431
        get_size: function() {
432
            return parseInt(this.get('metadata') ? this.get('metadata').size : -1)
433
        },
434

    
435
        get_description: function(escape) {
436
            if (escape == undefined) { escape = true };
437
            if (escape) { return this.escape('description') || "No description available"}
438
            return this.get('description') || "No description available."
439
        },
440

    
441
        get_meta: function(key) {
442
            if (this.get('metadata') && this.get('metadata')) {
443
                if (!this.get('metadata')[key]) { return null }
444
                return _.escape(this.get('metadata')[key]);
445
            } else {
446
                return null;
447
            }
448
        },
449

    
450
        get_meta_keys: function() {
451
            if (this.get('metadata') && this.get('metadata')) {
452
                return _.keys(this.get('metadata'));
453
            } else {
454
                return [];
455
            }
456
        },
457

    
458
        get_owner: function() {
459
            return this.get('owner') || _.keys(synnefo.config.system_images_owners)[0];
460
        },
461

    
462
        get_owner_uuid: function() {
463
            return this.get('owner_uuid');
464
        },
465

    
466
        is_system_image: function() {
467
          var owner = this.get_owner();
468
          return _.include(_.keys(synnefo.config.system_images_owners), owner)
469
        },
470

    
471
        owned_by: function(user) {
472
          if (!user) { user = synnefo.user }
473
          return user.get_username() == this.get('owner_uuid');
474
        },
475

    
476
        display_owner: function() {
477
            var owner = this.get_owner();
478
            if (_.include(_.keys(synnefo.config.system_images_owners), owner)) {
479
                return synnefo.config.system_images_owners[owner];
480
            } else {
481
                return owner;
482
            }
483
        },
484
    
485
        get_readable_size: function() {
486
            if (this.is_deleted()) {
487
                return synnefo.config.image_deleted_size_title || '(none)';
488
            }
489
            return this.get_size() > 0 ? util.readablizeBytes(this.get_size() * 1024 * 1024) : '(none)';
490
        },
491

    
492
        get_os: function() {
493
            return this.get_meta('OS');
494
        },
495

    
496
        get_gui: function() {
497
            return this.get_meta('GUI');
498
        },
499

    
500
        get_created_users: function() {
501
            try {
502
              var users = this.get_meta('users').split(" ");
503
            } catch (err) { users = null }
504
            if (!users) {
505
                var osfamily = this.get_meta('osfamily');
506
                if (osfamily == 'windows') { 
507
                  users = ['Administrator'];
508
                } else {
509
                  users = ['root'];
510
                }
511
            }
512
            return users;
513
        },
514

    
515
        get_sort_order: function() {
516
            return parseInt(this.get('metadata') ? this.get('metadata').sortorder : -1)
517
        },
518

    
519
        get_vm: function() {
520
            var vm_id = this.get("serverRef");
521
            var vm = undefined;
522
            vm = storage.vms.get(vm_id);
523
            return vm;
524
        },
525

    
526
        is_public: function() {
527
            return this.get('is_public') == undefined ? true : this.get('is_public');
528
        },
529

    
530
        is_deleted: function() {
531
            return this.get('status') == "DELETED"
532
        },
533
        
534
        ssh_keys_paths: function() {
535
            return _.map(this.get_created_users(), function(username) {
536
                prepend = '';
537
                if (username != 'root') {
538
                    prepend = '/home'
539
                }
540
                return {'user': username, 'path': '{1}/{0}/.ssh/authorized_keys'.format(username, 
541
                                                             prepend)};
542
            });
543
        },
544

    
545
        _supports_ssh: function() {
546
            if (synnefo.config.support_ssh_os_list.indexOf(this.get_os()) > -1) {
547
                return true;
548
            }
549
            if (this.get_meta('osfamily') == 'linux') {
550
              return true;
551
            }
552
            return false;
553
        },
554

    
555
        supports: function(feature) {
556
            if (feature == "ssh") {
557
                return this._supports_ssh()
558
            }
559
            return false;
560
        },
561

    
562
        personality_data_for_keys: function(keys) {
563
            return _.map(this.ssh_keys_paths(), function(pathinfo) {
564
                var contents = '';
565
                _.each(keys, function(key){
566
                    contents = contents + key.get("content") + "\n"
567
                });
568
                contents = $.base64.encode(contents);
569

    
570
                return {
571
                    path: pathinfo.path,
572
                    contents: contents,
573
                    mode: 0600,
574
                    owner: pathinfo.user
575
                }
576
            });
577
        }
578
    });
579

    
580
    // Flavor model
581
    models.Flavor = models.Model.extend({
582
        path: 'flavors',
583

    
584
        details_string: function() {
585
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
586
        },
587

    
588
        get_disk_size: function() {
589
            return parseInt(this.get("disk") * 1024)
590
        },
591

    
592
        get_ram_size: function() {
593
            return parseInt(this.get("ram"))
594
        },
595

    
596
        get_disk_template_info: function() {
597
            var info = snf.config.flavors_disk_templates_info[this.get("disk_template")];
598
            if (!info) {
599
                info = { name: this.get("disk_template"), description:'' };
600
            }
601
            return info
602
        },
603

    
604
        disk_to_bytes: function() {
605
            return parseInt(this.get("disk")) * 1024 * 1024 * 1024;
606
        },
607

    
608
        ram_to_bytes: function() {
609
            return parseInt(this.get("ram")) * 1024 * 1024;
610
        },
611

    
612
    });
613
    
614
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
615
    _.extend(models.ParamsList.prototype, bb.Events, {
616

    
617
        initialize: function(parent, param_name) {
618
            this.parent = parent;
619
            this.actions = {};
620
            this.param_name = param_name;
621
            this.length = 0;
622
        },
623
        
624
        has_action: function(action) {
625
            return this.actions[action] ? true : false;
626
        },
627
            
628
        _parse_params: function(arguments) {
629
            if (arguments.length <= 1) {
630
                return [];
631
            }
632

    
633
            var args = _.toArray(arguments);
634
            return args.splice(1);
635
        },
636

    
637
        contains: function(action, params) {
638
            params = this._parse_params(arguments);
639
            var has_action = this.has_action(action);
640
            if (!has_action) { return false };
641

    
642
            var paramsEqual = false;
643
            _.each(this.actions[action], function(action_params) {
644
                if (_.isEqual(action_params, params)) {
645
                    paramsEqual = true;
646
                }
647
            });
648
                
649
            return paramsEqual;
650
        },
651
        
652
        is_empty: function() {
653
            return _.isEmpty(this.actions);
654
        },
655

    
656
        add: function(action, params) {
657
            params = this._parse_params(arguments);
658
            if (this.contains.apply(this, arguments)) { return this };
659
            var isnew = false
660
            if (!this.has_action(action)) {
661
                this.actions[action] = [];
662
                isnew = true;
663
            };
664

    
665
            this.actions[action].push(params);
666
            this.parent.trigger("change:" + this.param_name, this.parent, this);
667
            if (isnew) {
668
                this.trigger("add", action, params);
669
            } else {
670
                this.trigger("change", action, params);
671
            }
672
            return this;
673
        },
674
        
675
        remove_all: function(action) {
676
            if (this.has_action(action)) {
677
                delete this.actions[action];
678
                this.parent.trigger("change:" + this.param_name, this.parent, this);
679
                this.trigger("remove", action);
680
            }
681
            return this;
682
        },
683

    
684
        reset: function() {
685
            this.actions = {};
686
            this.parent.trigger("change:" + this.param_name, this.parent, this);
687
            this.trigger("reset");
688
            this.trigger("remove");
689
        },
690

    
691
        remove: function(action, params) {
692
            params = this._parse_params(arguments);
693
            if (!this.has_action(action)) { return this };
694
            var index = -1;
695
            _.each(this.actions[action], _.bind(function(action_params) {
696
                if (_.isEqual(action_params, params)) {
697
                    index = this.actions[action].indexOf(action_params);
698
                }
699
            }, this));
700
            
701
            if (index > -1) {
702
                this.actions[action].splice(index, 1);
703
                if (_.isEmpty(this.actions[action])) {
704
                    delete this.actions[action];
705
                }
706
                this.parent.trigger("change:" + this.param_name, this.parent, this);
707
                this.trigger("remove", action, params);
708
            }
709
        }
710

    
711
    });
712

    
713
    // Virtualmachine model
714
    models.VM = models.Model.extend({
715

    
716
        path: 'servers',
717
        has_status: true,
718
        proxy_attrs: {
719
          'busy': [
720
            ['status', 'state'], function() {
721
              return !_.contains(['ACTIVE', 'STOPPED'], this.get('status'));
722
            }
723
          ]
724
        },
725

    
726
        initialize: function(params) {
727
            var self = this;
728
            this.ports = new Backbone.FilteredCollection(undefined, {
729
              collection: synnefo.storage.ports,
730
              collectionFilter: function(m) {
731
                return self.id == m.get('device_id')
732
            }});
733

    
734
            this.pending_firewalls = {};
735
            
736
            models.VM.__super__.initialize.apply(this, arguments);
737

    
738

    
739
            this.set({state: params.status || "ERROR"});
740
            this.log = new snf.logging.logger("VM " + this.id);
741
            this.pending_action = undefined;
742
            
743
            // init stats parameter
744
            this.set({'stats': undefined}, {silent: true});
745
            // defaults to not update the stats
746
            // each view should handle this vm attribute 
747
            // depending on if it displays stat images or not
748
            this.do_update_stats = false;
749
            
750
            // interval time
751
            // this will dynamicaly change if the server responds that
752
            // images get refreshed on different intervals
753
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
754
            this.stats_available = false;
755

    
756
            // initialize interval
757
            this.init_stats_intervals(this.stats_update_interval);
758
            
759
            // handle progress message on instance change
760
            this.bind("change", _.bind(this.update_status_message, this));
761
            this.bind("change:task_state", _.bind(this.update_status, this));
762
            // force update of progress message
763
            this.update_status_message(true);
764
            
765
            // default values
766
            this.bind("change:state", _.bind(function(){
767
                if (this.state() == "DESTROY") { 
768
                    this.handle_destroy() 
769
                }
770
            }, this));
771

    
772
        },
773
          
774
        has_public_ip: function() {
775
          return this.ports.filter(function(port) {
776
            return port.get("network") && 
777
                   port.get("network").get("is_public") && 
778
                   port.get("ips").length > 0;
779
          }).length > 0;
780
        },
781

    
782
        has_public_ipv6: function() {
783
          return this.has_ip_version("v6", true);
784
        },
785

    
786
        has_public_ipv4: function() {
787
          return this.has_ip_version("v4", true);
788
        },
789
        
790
        has_ip_version: function(ver, public) {
791
          var found = false;
792
          this.ports.each(function(port) {
793
            if (found) { return }
794
            if (public !== undefined) {
795
              if (port.get("network") && 
796
                  port.get("network").get("is_public") != public) {
797
                return
798
              }
799
            }
800
            port.get('ips').each(function(ip) {
801
              if (found) { return }
802
              if (ip.get("type") == ver) {
803
                found = true
804
              }
805
            })
806
          }, this)
807
          return found;
808
        },
809

    
810
        status: function(st) {
811
            if (!st) { return this.get("status")}
812
            return this.set({status:st});
813
        },
814
        
815
        update_status: function() {
816
            this.set_status(this.get('status'));
817
        },
818

    
819
        set_status: function(st) {
820
            var new_state = this.state_for_api_status(st);
821
            var transition = false;
822

    
823
            if (this.state() != new_state) {
824
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
825
                    transition = this.state();
826
                }
827
            }
828
            
829
            // call it silently to avoid double change trigger
830
            var state = this.state_for_api_status(st);
831
            this.set({'state': state}, {silent: true});
832
            
833
            // trigger transition
834
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
835
                this.trigger("transition", {from:transition, to:new_state}) 
836
            };
837
            return st;
838
        },
839
            
840
        get_diagnostics: function(success) {
841
            this.__make_api_call(this.get_diagnostics_url(),
842
                                 "read", // create so that sync later uses POST to make the call
843
                                 null, // payload
844
                                 function(data) {
845
                                     success(data);
846
                                 },  
847
                                 null, 'diagnostics');
848
        },
849

    
850
        has_diagnostics: function() {
851
            return this.get("diagnostics") && this.get("diagnostics").length;
852
        },
853

    
854
        get_progress_info: function() {
855
            // details about progress message
856
            // contains a list of diagnostic messages
857
            return this.get("status_messages");
858
        },
859

    
860
        get_status_message: function() {
861
            return this.get('status_message');
862
        },
863
        
864
        // extract status message from diagnostics
865
        status_message_from_diagnostics: function(diagnostics) {
866
            var valid_sources_map = synnefo.config.diagnostics_status_messages_map;
867
            var valid_sources = valid_sources_map[this.get('status')];
868
            if (!valid_sources) { return null };
869
            
870
            // filter messsages based on diagnostic source
871
            var messages = _.filter(diagnostics, function(diag) {
872
                return valid_sources.indexOf(diag.source) > -1;
873
            });
874

    
875
            var msg = messages[0];
876
            if (msg) {
877
              var message = msg.message;
878
              var message_tpl = snf.config.diagnostic_messages_tpls[msg.source];
879

    
880
              if (message_tpl) {
881
                  message = message_tpl.replace('MESSAGE', msg.message);
882
              }
883
              return message;
884
            }
885
            
886
            // no message to display, but vm in build state, display
887
            // finalizing message.
888
            if (this.is_building() == 'BUILD') {
889
                return synnefo.config.BUILDING_MESSAGES['FINAL'];
890
            }
891
            return null;
892
        },
893

    
894
        update_status_message: function(force) {
895
            // update only if one of the specified attributes has changed
896
            if (
897
              !this.keysChanged(['diagnostics', 'progress', 'status', 'state'])
898
                && !force
899
            ) { return };
900
            
901
            // if user requested to destroy the vm set the appropriate 
902
            // message.
903
            if (this.get('state') == "DESTROY") { 
904
                message = "Terminating..."
905
                this.set({status_message: message})
906
                return;
907
            }
908
            
909
            // set error message, if vm has diagnostic message display it as
910
            // progress message
911
            if (this.in_error_state()) {
912
                var d = this.get('diagnostics');
913
                if (d && d.length) {
914
                    var message = this.status_message_from_diagnostics(d);
915
                    this.set({status_message: message});
916
                } else {
917
                    this.set({status_message: null});
918
                }
919
                return;
920
            }
921
            
922
            // identify building status message
923
            if (this.is_building()) {
924
                var self = this;
925
                var success = function(msg) {
926
                    self.set({status_message: msg});
927
                }
928
                this.get_building_status_message(success);
929
                return;
930
            }
931

    
932
            this.set({status_message:null});
933
        },
934
            
935
        // get building status message. Asynchronous function since it requires
936
        // access to vm image.
937
        get_building_status_message: function(callback) {
938
            // no progress is set, vm is in initial build status
939
            var progress = this.get("progress");
940
            if (progress == 0 || !progress) {
941
                return callback(BUILDING_MESSAGES['INIT']);
942
            }
943
            
944
            // vm has copy progress, display copy percentage
945
            if (progress > 0 && progress <= 99) {
946
                this.get_copy_details(true, undefined, _.bind(
947
                    function(details){
948
                        callback(BUILDING_MESSAGES['COPY'].format(details.copy, 
949
                                                           details.size, 
950
                                                           details.progress));
951
                }, this));
952
                return;
953
            }
954

    
955
            // copy finished display FINAL message or identify status message
956
            // from diagnostics.
957
            if (progress >= 100) {
958
                if (!this.has_diagnostics()) {
959
                        callback(BUILDING_MESSAGES['FINAL']);
960
                } else {
961
                        var d = this.get("diagnostics");
962
                        var msg = this.status_message_from_diagnostics(d);
963
                        if (msg) {
964
                              callback(msg);
965
                        }
966
                }
967
            }
968
        },
969

    
970
        get_copy_details: function(human, image, callback) {
971
            var human = human || false;
972
            var image = image || this.get_image(_.bind(function(image){
973
                var progress = this.get('progress');
974
                var size = image.get_size();
975
                var size_copied = (size * progress / 100).toFixed(2);
976
                
977
                if (human) {
978
                    size = util.readablizeBytes(size*1024*1024);
979
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
980
                }
981

    
982
                callback({'progress': progress, 'size': size, 'copy': size_copied})
983
            }, this));
984
        },
985

    
986
        start_stats_update: function(force_if_empty) {
987
            var prev_state = this.do_update_stats;
988

    
989
            this.do_update_stats = true;
990
            
991
            // fetcher initialized ??
992
            if (!this.stats_fetcher) {
993
                this.init_stats_intervals();
994
            }
995

    
996

    
997
            // fetcher running ???
998
            if (!this.stats_fetcher.running || !prev_state) {
999
                this.stats_fetcher.start();
1000
            }
1001

    
1002
            if (force_if_empty && this.get("stats") == undefined) {
1003
                this.update_stats(true);
1004
            }
1005
        },
1006

    
1007
        stop_stats_update: function(stop_calls) {
1008
            this.do_update_stats = false;
1009

    
1010
            if (stop_calls) {
1011
                this.stats_fetcher.stop();
1012
            }
1013
        },
1014

    
1015
        // clear and reinitialize update interval
1016
        init_stats_intervals: function (interval) {
1017
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
1018
            this.stats_fetcher.start();
1019
        },
1020
        
1021
        get_stats_fetcher: function(timeout) {
1022
            var cb = _.bind(function(data){
1023
                this.update_stats();
1024
            }, this);
1025
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
1026
            return fetcher;
1027
        },
1028

    
1029
        // do the api call
1030
        update_stats: function(force) {
1031
            // do not update stats if flag not set
1032
            if ((!this.do_update_stats && !force) || this.updating_stats) {
1033
                return;
1034
            }
1035

    
1036
            // make the api call, execute handle_stats_update on sucess
1037
            // TODO: onError handler ???
1038
            stats_url = this.url() + "/stats";
1039
            this.updating_stats = true;
1040
            this.sync("read", this, {
1041
                handles_error:true, 
1042
                url: stats_url, 
1043
                refresh:true, 
1044
                success: _.bind(this.handle_stats_update, this),
1045
                error: _.bind(this.handle_stats_error, this),
1046
                complete: _.bind(function(){this.updating_stats = false;}, this),
1047
                critical: false,
1048
                log_error: false,
1049
                skips_timeouts: true
1050
            });
1051
        },
1052

    
1053
        get_attachment: function(id) {
1054
          var attachment = undefined;
1055
          _.each(this.get("attachments"), function(a) {
1056
            if (a.id == id) {
1057
              attachment = a;
1058
            }
1059
          });
1060
          return attachment
1061
        },
1062

    
1063
        _set_stats: function(stats) {
1064
            var silent = silent === undefined ? false : silent;
1065
            // unavailable stats while building
1066
            if (this.get("status") == "BUILD") { 
1067
                this.stats_available = false;
1068
            } else { this.stats_available = true; }
1069

    
1070
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
1071
            
1072
            this.set({stats: stats}, {silent:true});
1073
            this.trigger("stats:update", stats);
1074
        },
1075

    
1076
        unbind: function() {
1077
            models.VM.__super__.unbind.apply(this, arguments);
1078
        },
1079
        
1080
        can_connect: function() {
1081
          return _.contains(["ACTIVE", "STOPPED"], this.get("status"))
1082
        },
1083

    
1084
        can_disconnect: function() {
1085
          return _.contains(["ACTIVE", "STOPPED"], this.get("status"))
1086
        },
1087

    
1088
        can_resize: function() {
1089
          return this.get('status') == 'STOPPED';
1090
        },
1091

    
1092
        handle_stats_error: function() {
1093
            stats = {};
1094
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1095
                stats[k] = false;
1096
            });
1097

    
1098
            this.set({'stats': stats});
1099
        },
1100

    
1101
        // this method gets executed after a successful vm stats api call
1102
        handle_stats_update: function(data) {
1103
            var self = this;
1104
            // avoid browser caching
1105
            
1106
            if (data.stats && _.size(data.stats) > 0) {
1107
                var ts = $.now();
1108
                var stats = data.stats;
1109
                var images_loaded = 0;
1110
                var images = {};
1111

    
1112
                function check_images_loaded() {
1113
                    images_loaded++;
1114

    
1115
                    if (images_loaded == 4) {
1116
                        self._set_stats(images);
1117
                    }
1118
                }
1119
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1120
                    
1121
                    stats[k] = stats[k] + "?_=" + ts;
1122
                    
1123
                    var stat = k.slice(0,3);
1124
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
1125
                    var img = $("<img />");
1126
                    var val = stats[k];
1127
                    
1128
                    // load stat image to a temporary dom element
1129
                    // update model stats on image load/error events
1130
                    img.load(function() {
1131
                        images[k] = val;
1132
                        check_images_loaded();
1133
                    });
1134

    
1135
                    img.error(function() {
1136
                        images[stat + type] = false;
1137
                        check_images_loaded();
1138
                    });
1139

    
1140
                    img.attr({'src': stats[k]});
1141
                })
1142
                data.stats = stats;
1143
            }
1144

    
1145
            // do we need to change the interval ??
1146
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
1147
                this.stats_update_interval = data.stats.refresh * 1000;
1148
                this.stats_fetcher.interval = this.stats_update_interval;
1149
                this.stats_fetcher.maximum_interval = this.stats_update_interval;
1150
                this.stats_fetcher.stop();
1151
                this.stats_fetcher.start(false);
1152
            }
1153
        },
1154

    
1155
        // helper method that sets the do_update_stats
1156
        // in the future this method could also make an api call
1157
        // immediaetly if needed
1158
        enable_stats_update: function() {
1159
            this.do_update_stats = true;
1160
        },
1161
        
1162
        handle_destroy: function() {
1163
            this.stats_fetcher.stop();
1164
        },
1165

    
1166
        require_reboot: function() {
1167
            if (this.is_active()) {
1168
                this.set({'reboot_required': true});
1169
            }
1170
        },
1171
        
1172
        set_pending_action: function(data) {
1173
            this.pending_action = data;
1174
            return data;
1175
        },
1176

    
1177
        // machine has pending action
1178
        update_pending_action: function(action, force) {
1179
            this.set({pending_action: action});
1180
        },
1181

    
1182
        clear_pending_action: function() {
1183
            this.set({pending_action: undefined});
1184
        },
1185

    
1186
        has_pending_action: function() {
1187
            return this.get("pending_action") ? this.get("pending_action") : false;
1188
        },
1189
        
1190
        // machine is active
1191
        is_active: function() {
1192
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
1193
        },
1194
        
1195
        // machine is building 
1196
        is_building: function() {
1197
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
1198
        },
1199
        
1200
        is_rebooting: function() {
1201
            return this.state() == 'REBOOT';
1202
        },
1203

    
1204
        in_error_state: function() {
1205
            return this.state() === "ERROR"
1206
        },
1207

    
1208
        // user can connect to machine
1209
        is_connectable: function() {
1210
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1211
        },
1212
        
1213
        remove_meta: function(key, complete, error) {
1214
            var url = this.api_path() + "/metadata/" + key;
1215
            this.api_call(url, "delete", undefined, complete, error);
1216
        },
1217

    
1218
        save_meta: function(meta, complete, error) {
1219
            var url = this.api_path() + "/metadata/" + meta.key;
1220
            var payload = {meta:{}};
1221
            payload.meta[meta.key] = meta.value;
1222
            payload._options = {
1223
                critical:false, 
1224
                error_params: {
1225
                    title: "Machine metadata error",
1226
                    extra_details: {"Machine id": this.id}
1227
            }};
1228

    
1229
            this.api_call(url, "update", payload, complete, error);
1230
        },
1231

    
1232

    
1233
        // update/get the state of the machine
1234
        state: function() {
1235
            var args = slice.call(arguments);
1236
                
1237
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1238
                this.set({'state': args[0]});
1239
            }
1240

    
1241
            return this.get('state');
1242
        },
1243
        
1244
        // get the state that the api status corresponds to
1245
        state_for_api_status: function(status) {
1246
            return this.state_transition(this.state(), status);
1247
        },
1248
        
1249
        // get transition state for the corresponging api status
1250
        state_transition: function(state, new_status) {
1251
            var statuses = models.VM.STATES_TRANSITIONS[state];
1252
            if (statuses) {
1253
                if (statuses.indexOf(new_status) > -1) {
1254
                    return new_status;
1255
                } else {
1256
                    return state;
1257
                }
1258
            } else {
1259
                return new_status;
1260
            }
1261
        },
1262
        
1263
        // the current vm state is a transition state
1264
        in_transition: function() {
1265
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
1266
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
1267
        },
1268
        
1269
        // get image object
1270
        get_image: function(callback) {
1271
            if (callback == undefined) { callback = function(){} }
1272
            var image = storage.images.get(this.get('image'));
1273
            if (!image) {
1274
                storage.images.update_unknown_id(this.get('image'), callback);
1275
                return;
1276
            }
1277
            callback(image);
1278
            return image;
1279
        },
1280
        
1281
        // get flavor object
1282
        get_flavor: function() {
1283
            var flv = storage.flavors.get(this.get('flavor'));
1284
            if (!flv) {
1285
                storage.flavors.update_unknown_id(this.get('flavor'));
1286
                flv = storage.flavors.get(this.get('flavor'));
1287
            }
1288
            return flv;
1289
        },
1290

    
1291
        get_resize_flavors: function() {
1292
          var vm_flavor = this.get_flavor();
1293
          var flavors = synnefo.storage.flavors.filter(function(f){
1294
              return f.get('disk_template') ==
1295
              vm_flavor.get('disk_template') && f.get('disk') ==
1296
              vm_flavor.get('disk');
1297
          });
1298
          return flavors;
1299
        },
1300

    
1301
        get_flavor_quotas: function() {
1302
          var flavor = this.get_flavor();
1303
          return {
1304
            cpu: flavor.get('cpu') + 1, 
1305
            ram: flavor.get_ram_size() + 1, 
1306
            disk:flavor.get_disk_size() + 1
1307
          }
1308
        },
1309

    
1310
        get_meta: function(key, deflt) {
1311
            if (this.get('metadata') && this.get('metadata')) {
1312
                if (!this.get('metadata')[key]) { return deflt }
1313
                return _.escape(this.get('metadata')[key]);
1314
            } else {
1315
                return deflt;
1316
            }
1317
        },
1318

    
1319
        get_meta_keys: function() {
1320
            if (this.get('metadata') && this.get('metadata')) {
1321
                return _.keys(this.get('metadata'));
1322
            } else {
1323
                return [];
1324
            }
1325
        },
1326
        
1327
        // get metadata OS value
1328
        get_os: function() {
1329
            var image = this.get_image();
1330
            return this.get_meta('OS') || (image ? 
1331
                                            image.get_os() || "okeanos" : "okeanos");
1332
        },
1333

    
1334
        get_gui: function() {
1335
            return this.get_meta('GUI');
1336
        },
1337
        
1338
        get_hostname: function() {
1339
          var hostname = this.get_meta('hostname') || this.get('fqdn');
1340
          if (!hostname) {
1341
            if (synnefo.config.vm_hostname_format) {
1342
              hostname = synnefo.config.vm_hostname_format.format(this.id);
1343
            } else {
1344
              hostname = 'unknown';
1345
            }
1346
          }
1347
          return hostname;
1348
        },
1349

    
1350
        // get actions that the user can execute
1351
        // depending on the vm state/status
1352
        get_available_actions: function() {
1353
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1354
        },
1355

    
1356
        set_profile: function(profile, net_id) {
1357
        },
1358
        
1359
        // call rename api
1360
        rename: function(new_name) {
1361
            //this.set({'name': new_name});
1362
            this.sync("update", this, {
1363
                critical: true,
1364
                data: {
1365
                    'server': {
1366
                        'name': new_name
1367
                    }
1368
                }, 
1369
                success: _.bind(function(){
1370
                    snf.api.trigger("call");
1371
                }, this)
1372
            });
1373
        },
1374
        
1375
        get_console_url: function(data) {
1376
            var url_params = {
1377
                machine: this.get("name"),
1378
                host_ip: this.get_hostname(),
1379
                host_ip_v6: this.get_hostname(),
1380
                host: data.host,
1381
                port: data.port,
1382
                password: data.password
1383
            }
1384
            return synnefo.config.ui_console_url + '?' + $.param(url_params);
1385
        },
1386
        
1387
        set_firewall: function(nic, value, success_cb, error_cb) {
1388
          var success = function() { success_cb() }
1389
          var error = function() { error_cb() }
1390
          var data = {'nic': nic.id, 'profile': value, 'display': true};
1391
          var url = this.url() + "/action";
1392
          //var params = {skip_api_error: false, display: true};
1393
          this.call('firewallProfile', success, error, data);
1394
        },
1395

    
1396
        connect_floating_ip: function(ip, cb) {
1397
          this.set({'status': 'CONNECTING'});
1398
          synnefo.storage.ports.create({
1399
            port: {
1400
              network_id: ip.get('floating_network_id'),
1401
              device_id: this.id,
1402
              fixed_ips: [{'ip_address': ip.get('floating_ip_address')}]
1403
            }
1404
          }, {complete: cb, skip_api_error: false})
1405
        },
1406
        
1407
        create_snapshot: function(snapshot_params, callback) {
1408
          var volume = this.get('volumes') && this.get('volumes').length ? 
1409
                       this.get('volumes')[0] : undefined;
1410
          var params = _.extend({
1411
            'metadata': {},
1412
            'volume_id': volume
1413
          }, snapshot_params);
1414

    
1415
          snf.api.sync('create', undefined, {
1416
              url: synnefo.config.api_urls.volume + '/snapshots/',
1417
              data: JSON.stringify({snapshot:params}),
1418
              success: callback, 
1419
              skip_api_error: false,
1420
              contentType: 'application/json'
1421
          });
1422
        },
1423

    
1424
        // action helper
1425
        call: function(action_name, success, error, params) {
1426
            var id_param = [this.id];
1427
            
1428
            params = params || {};
1429
            success = success || function() {};
1430
            error = error || function() {};
1431

    
1432
            var self = this;
1433

    
1434
            switch(action_name) {
1435
                case 'start':
1436
                    this.__make_api_call(this.get_action_url(), // vm actions url
1437
                                         "create", // create so that sync later uses POST to make the call
1438
                                         {start:{}}, // payload
1439
                                         function() {
1440
                                             // set state after successful call
1441
                                             self.state("START"); 
1442
                                             success.apply(this, arguments);
1443
                                             snf.api.trigger("call");
1444
                                         },  
1445
                                         error, 'start', params);
1446
                    break;
1447
                case 'reboot':
1448
                    this.__make_api_call(this.get_action_url(), // vm actions url
1449
                                         "create", // create so that sync later uses POST to make the call
1450
                                         {reboot:{}}, // payload
1451
                                         function() {
1452
                                             // set state after successful call
1453
                                             self.state("REBOOT"); 
1454
                                             success.apply(this, arguments)
1455
                                             snf.api.trigger("call");
1456
                                             self.set({'reboot_required': false});
1457
                                         },
1458
                                         error, 'reboot', params);
1459
                    break;
1460
                case 'shutdown':
1461
                    this.__make_api_call(this.get_action_url(), // vm actions url
1462
                                         "create", // create so that sync later uses POST to make the call
1463
                                         {shutdown:{}}, // payload
1464
                                         function() {
1465
                                             // set state after successful call
1466
                                             self.state("SHUTDOWN"); 
1467
                                             success.apply(this, arguments)
1468
                                             snf.api.trigger("call");
1469
                                         },  
1470
                                         error, 'shutdown', params);
1471
                    break;
1472
                case 'console':
1473
                    this.__make_api_call(this.url() + "/action", "create", 
1474
                                         {'console': {'type':'vnc'}}, 
1475
                                         function(data) {
1476
                        var cons_data = data.console;
1477
                        success.apply(this, [cons_data]);
1478
                    }, undefined, 'console', params)
1479
                    break;
1480
                case 'destroy':
1481
                    this.__make_api_call(this.url(), // vm actions url
1482
                                         "delete", // create so that sync later uses POST to make the call
1483
                                         undefined, // payload
1484
                                         function() {
1485
                                             // set state after successful call
1486
                                             self.state('DESTROY');
1487
                                             success.apply(this, arguments);
1488
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1489

    
1490
                                         },  
1491
                                         error, 'destroy', params);
1492
                    break;
1493
                case 'resize':
1494
                    this.__make_api_call(this.get_action_url(), // vm actions url
1495
                                         "create", // create so that sync later uses POST to make the call
1496
                                         {resize: {flavorRef:params.flavor}}, // payload
1497
                                         function() {
1498
                                             self.state('RESIZE');
1499
                                             success.apply(this, arguments);
1500
                                             snf.api.trigger("call");
1501
                                         },  
1502
                                         error, 'resize', params);
1503
                    break;
1504
                case 'destroy':
1505
                    this.__make_api_call(this.url(), // vm actions url
1506
                                         "delete", // create so that sync later uses POST to make the call
1507
                                         undefined, // payload
1508
                                         function() {
1509
                                             // set state after successful call
1510
                                             self.state('DESTROY');
1511
                                             success.apply(this, arguments);
1512
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1513

    
1514
                                         },  
1515
                                         error, 'destroy', params);
1516
                    break;
1517
                case 'firewallProfile':
1518
                    this.__make_api_call(this.get_action_url(), // vm actions url
1519
                                         "create",
1520
                                         {firewallProfile:{nic:params.nic, profile:params.profile}}, // payload
1521
                                         function() {
1522
                                             success.apply(this, arguments);
1523
                                             snf.api.trigger("call");
1524
                                         },  
1525
                                         error, 'start', params);
1526
                    break;
1527
                default:
1528
                    throw "Invalid VM action ("+action_name+")";
1529
            }
1530
        },
1531
        
1532
        __make_api_call: function(url, method, data, success, error, action, 
1533
                                  extra_params) {
1534
            var self = this;
1535
            error = error || function(){};
1536
            success = success || function(){};
1537

    
1538
            var params = {
1539
                url: url,
1540
                data: data,
1541
                success: function() { 
1542
                  self.handle_action_succeed.apply(self, arguments); 
1543
                  success.apply(this, arguments)
1544
                },
1545
                error: function() { 
1546
                  self.handle_action_fail.apply(self, arguments);
1547
                  error.apply(this, arguments)
1548
                },
1549
                error_params: { ns: "Machines actions", 
1550
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1551
                                extra_details: {
1552
                                  'Machine ID': this.id, 
1553
                                  'URL': url, 
1554
                                  'Action': action || "undefined" },
1555
                                allow_reload: false
1556
                              },
1557
                display: false,
1558
                critical: false
1559
            }
1560
            _.extend(params, extra_params);
1561
            this.sync(method, this, params);
1562
        },
1563

    
1564
        handle_action_succeed: function() {
1565
            this.trigger("action:success", arguments);
1566
        },
1567
        
1568
        reset_action_error: function() {
1569
            this.action_error = false;
1570
            this.trigger("action:fail:reset", this.action_error);
1571
        },
1572

    
1573
        handle_action_fail: function() {
1574
            this.action_error = arguments;
1575
            this.trigger("action:fail", arguments);
1576
        },
1577

    
1578
        get_action_url: function(name) {
1579
            return this.url() + "/action";
1580
        },
1581

    
1582
        get_diagnostics_url: function() {
1583
            return this.url() + "/diagnostics";
1584
        },
1585

    
1586
        get_users: function() {
1587
            var image;
1588
            var users = [];
1589
            try {
1590
              var users = this.get_meta('users').split(" ");
1591
            } catch (err) { users = null }
1592
            if (!users) {
1593
              image = this.get_image();
1594
              if (image) {
1595
                  users = image.get_created_users();
1596
              }
1597
            }
1598
            return users;
1599
        },
1600

    
1601
        get_connection_info: function(host_os, success, error) {
1602
            var url = synnefo.config.ui_connect_url;
1603
            var users = this.get_users();
1604

    
1605
            params = {
1606
                ip_address: this.get_hostname(),
1607
                hostname: this.get_hostname(),
1608
                os: this.get_os(),
1609
                host_os: host_os,
1610
                ports: JSON.stringify(this.get('SNF:port_forwarding') || {}),
1611
                srv: this.id
1612
            }
1613
            
1614
            if (users.length) { 
1615
                params['username'] = _.last(users)
1616
            }
1617

    
1618
            url = url + "?" + $.param(params);
1619

    
1620
            var ajax = snf.api.sync("read", undefined, { url: url, 
1621
                                                         error:error, 
1622
                                                         success:success, 
1623
                                                         handles_error:1});
1624
        }
1625
    });
1626
    
1627
    models.VM.ACTIONS = [
1628
        'start',
1629
        'shutdown',
1630
        'reboot',
1631
        'console',
1632
        'destroy',
1633
        'resize',
1634
        'snapshot'
1635
    ]
1636

    
1637
    models.VM.TASK_STATE_STATUS_MAP = {
1638
      'BULDING': 'BUILD',
1639
      'REBOOTING': 'REBOOT',
1640
      'STOPPING': 'SHUTDOWN',
1641
      'STARTING': 'START',
1642
      'RESIZING': 'RESIZE',
1643
      'CONNECTING': 'CONNECT',
1644
      'DISCONNECTING': 'DISCONNECT',
1645
      'DESTROYING': 'DESTROY'
1646
    }
1647

    
1648
    models.VM.AVAILABLE_ACTIONS = {
1649
        'UNKNWON'       : ['destroy'],
1650
        'BUILD'         : ['destroy'],
1651
        'REBOOT'        : ['destroy'],
1652
        'STOPPED'       : ['start', 'destroy', 'resize', 'snapshot'],
1653
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console', 'resize', 'snapshot'],
1654
        'ERROR'         : ['destroy'],
1655
        'DELETED'       : ['destroy'],
1656
        'DESTROY'       : ['destroy'],
1657
        'SHUTDOWN'      : ['destroy'],
1658
        'START'         : ['destroy'],
1659
        'CONNECT'       : ['destroy'],
1660
        'DISCONNECT'    : ['destroy'],
1661
        'RESIZE'        : ['destroy']
1662
    }
1663
    
1664
    models.VM.AVAILABLE_ACTIONS_INACTIVE = {
1665
      'resize': ['STOPPED']
1666
    }
1667

    
1668
    // api status values
1669
    models.VM.STATUSES = [
1670
        'UNKNWON',
1671
        'BUILD',
1672
        'REBOOT',
1673
        'STOPPED',
1674
        'ACTIVE',
1675
        'ERROR',
1676
        'DELETED',
1677
        'RESIZE'
1678
    ]
1679

    
1680
    // api status values
1681
    models.VM.CONNECT_STATES = [
1682
        'ACTIVE',
1683
        'REBOOT',
1684
        'SHUTDOWN'
1685
    ]
1686

    
1687
    // vm states
1688
    models.VM.STATES = models.VM.STATUSES.concat([
1689
        'DESTROY',
1690
        'SHUTDOWN',
1691
        'START',
1692
        'CONNECT',
1693
        'DISCONNECT',
1694
        'FIREWALL',
1695
        'RESIZE'
1696
    ]);
1697
    
1698
    models.VM.STATES_TRANSITIONS = {
1699
        'DESTROY' : ['DELETED'],
1700
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1701
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1702
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1703
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1704
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1705
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1706
        'RESIZE': ['ERROR', 'STOPPED']
1707
    }
1708

    
1709
    models.VM.TRANSITION_STATES = [
1710
        'DESTROY',
1711
        'SHUTDOWN',
1712
        'START',
1713
        'REBOOT',
1714
        'BUILD',
1715
        'RESIZE'
1716
    ]
1717

    
1718
    models.VM.ACTIVE_STATES = [
1719
        'BUILD', 'REBOOT', 'ACTIVE',
1720
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1721
    ]
1722

    
1723
    models.VM.BUILDING_STATES = [
1724
        'BUILD'
1725
    ]
1726

    
1727
    models.Images = models.Collection.extend({
1728
        model: models.Image,
1729
        path: 'images',
1730
        details: true,
1731
        noUpdate: true,
1732
        supportIncUpdates: false,
1733
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1734
        meta_labels: {},
1735
        read_method: 'read',
1736

    
1737
        // update collection model with id passed
1738
        // making a direct call to the image
1739
        // api url
1740
        update_unknown_id: function(id, callback) {
1741
            var url = getUrl.call(this) + "/" + id;
1742
            this.api_call(this.path + "/" + id, this.read_method, {
1743
              _options:{
1744
                async:true, 
1745
                skip_api_error:true}
1746
              }, undefined, 
1747
            _.bind(function() {
1748
                if (!this.get(id)) {
1749
                            if (this.fallback_service) {
1750
                        // if current service has fallback_service attribute set
1751
                        // use this service to retrieve the missing image model
1752
                        var tmpservice = new this.fallback_service();
1753
                        tmpservice.update_unknown_id(id, _.bind(function(img){
1754
                            img.attributes.status = "DELETED";
1755
                            this.add(img.attributes);
1756
                            callback(this.get(id));
1757
                        }, this));
1758
                    } else {
1759
                        var title = synnefo.config.image_deleted_title || 'Deleted';
1760
                        // else add a dummy DELETED state image entry
1761
                        this.add({id:id, name:title, size:-1, 
1762
                                  progress:100, status:"DELETED"});
1763
                        callback(this.get(id));
1764
                    }   
1765
                } else {
1766
                    callback(this.get(id));
1767
                }
1768
            }, this), _.bind(function(image, msg, xhr) {
1769
                if (!image) {
1770
                    var title = synnefo.config.image_deleted_title || 'Deleted';
1771
                    this.add({id:id, name:title, size:-1, 
1772
                              progress:100, status:"DELETED"});
1773
                    callback(this.get(id));
1774
                    return;
1775
                }
1776
                var img_data = this._read_image_from_request(image, msg, xhr);
1777
                this.add(img_data);
1778
                callback(this.get(id));
1779
            }, this));
1780
        },
1781

    
1782
        _read_image_from_request: function(image, msg, xhr) {
1783
            return image.image;
1784
        },
1785

    
1786
        parse: function (resp, xhr) {
1787
            var parsed = _.map(resp.images, _.bind(this.parse_meta, this));
1788
            parsed = this.fill_owners(parsed);
1789
            return parsed;
1790
        },
1791

    
1792
        fill_owners: function(images) {
1793
            // do translate uuid->displayname if needed
1794
            // store display name in owner attribute for compatibility
1795
            var uuids = [];
1796

    
1797
            var images = _.map(images, function(img, index) {
1798
                if (synnefo.config.translate_uuids) {
1799
                    uuids.push(img['owner']);
1800
                }
1801
                img['owner_uuid'] = img['owner'];
1802
                return img;
1803
            });
1804
            
1805
            if (uuids.length > 0) {
1806
                var handle_results = function(data) {
1807
                    _.each(images, function (img) {
1808
                        img['owner'] = data.uuid_catalog[img['owner_uuid']];
1809
                    });
1810
                }
1811
                // notice the async false
1812
                var uuid_map = this.translate_uuids(uuids, false, 
1813
                                                    handle_results)
1814
            }
1815
            return images;
1816
        },
1817

    
1818
        translate_uuids: function(uuids, async, cb) {
1819
            var url = synnefo.config.user_catalog_url;
1820
            var data = JSON.stringify({'uuids': uuids});
1821
          
1822
            // post to user_catalogs api
1823
            snf.api.sync('create', undefined, {
1824
                url: url,
1825
                data: data,
1826
                async: async,
1827
                success:  cb
1828
            });
1829
        },
1830

    
1831
        get_meta_key: function(img, key) {
1832
            if (img.metadata && img.metadata && img.metadata[key]) {
1833
                return _.escape(img.metadata[key]);
1834
            }
1835
            return undefined;
1836
        },
1837

    
1838
        comparator: function(img) {
1839
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1840
        },
1841

    
1842
        parse_meta: function(img) {
1843
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1844
                if (img[key]) { return };
1845
                img[key] = this.get_meta_key(img, key) || "";
1846
            }, this));
1847
            return img;
1848
        },
1849

    
1850
        active: function() {
1851
            return this.filter(function(img){return img.get('status') != "DELETED"});
1852
        },
1853

    
1854
        predefined: function() {
1855
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
1856
        },
1857
        
1858
        fetch_for_type: function(type, complete, error) {
1859
            this.fetch({update:true, 
1860
                        success: complete, 
1861
                        error: error, 
1862
                        skip_api_error: true });
1863
        },
1864
        
1865
        get_images_for_type: function(type) {
1866
            if (this['get_{0}_images'.format(type)]) {
1867
                return this['get_{0}_images'.format(type)]();
1868
            }
1869

    
1870
            return this.active();
1871
        },
1872

    
1873
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
1874
            var load = false;
1875
            error = onError || function() {};
1876
            function complete(collection) { 
1877
                onComplete(collection.get_images_for_type(type)); 
1878
            }
1879
            
1880
            // do we need to fetch/update current collection entries
1881
            if (load) {
1882
                onStart();
1883
                this.fetch_for_type(type, complete, error);
1884
            } else {
1885
                // fallback to complete
1886
                complete(this);
1887
            }
1888
        }
1889
    })
1890

    
1891
    models.Flavors = models.Collection.extend({
1892
        model: models.Flavor,
1893
        path: 'flavors',
1894
        details: true,
1895
        noUpdate: true,
1896
        supportIncUpdates: false,
1897
        // update collection model with id passed
1898
        // making a direct call to the flavor
1899
        // api url
1900
        update_unknown_id: function(id, callback) {
1901
            var url = getUrl.call(this) + "/" + id;
1902
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
1903
            _.bind(function() {
1904
                this.add({id:id, cpu:"Unknown", ram:"Unknown", disk:"Unknown", name: "Unknown", status:"DELETED"})
1905
            }, this), _.bind(function(flv) {
1906
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1907
                this.add(flv.flavor);
1908
            }, this));
1909
        },
1910

    
1911
        parse: function (resp, xhr) {
1912
            return _.map(resp.flavors, function(o) {
1913
              o.cpu = o['vcpus'];
1914
              o.disk_template = o['SNF:disk_template'];
1915
              return o
1916
            });
1917
        },
1918

    
1919
        comparator: function(flv) {
1920
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1921
        },
1922
          
1923
        unavailable_values_for_quotas: function(quotas, flavors, extra) {
1924
            var flavors = flavors || this.active();
1925
            var index = {cpu:[], disk:[], ram:[]};
1926
            var extra = extra == undefined ? {cpu:0, disk:0, ram:0} : extra;
1927
            
1928
            _.each(flavors, function(el) {
1929

    
1930
                var disk_available = quotas['disk'] + extra.disk;
1931
                var disk_size = el.get_disk_size();
1932
                if (index.disk.indexOf(disk_size) == -1) {
1933
                  var disk = el.disk_to_bytes();
1934
                  if (disk > disk_available) {
1935
                    index.disk.push(disk_size);
1936
                  }
1937
                }
1938
                
1939
                var ram_available = quotas['ram'] + extra.ram * 1024 * 1024;
1940
                var ram_size = el.get_ram_size();
1941
                if (index.ram.indexOf(ram_size) == -1) {
1942
                  var ram = el.ram_to_bytes();
1943
                  if (ram > ram_available) {
1944
                    index.ram.push(el.get('ram'))
1945
                  }
1946
                }
1947

    
1948
                var cpu = el.get('cpu');
1949
                var cpu_available = quotas['cpu'] + extra.cpu;
1950
                if (index.cpu.indexOf(cpu) == -1) {
1951
                  if (cpu > cpu_available) {
1952
                    index.cpu.push(el.get('cpu'))
1953
                  }
1954
                }
1955
            });
1956
            return index;
1957
        },
1958

    
1959
        unavailable_values_for_image: function(img, flavors) {
1960
            var flavors = flavors || this.active();
1961
            var size = img.get_size();
1962
            
1963
            var index = {cpu:[], disk:[], ram:[]};
1964

    
1965
            _.each(this.active(), function(el) {
1966
                var img_size = size;
1967
                var flv_size = el.get_disk_size();
1968
                if (flv_size < img_size) {
1969
                    if (index.disk.indexOf(flv_size) == -1) {
1970
                        index.disk.push(flv_size);
1971
                    }
1972
                };
1973
            });
1974
            
1975
            return index;
1976
        },
1977

    
1978
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
1979
            if (!filter_list) { filter_list = this.models };
1980
            
1981
            return this.select(function(flv){
1982
                if (flv.get("cpu") == cpu + "" &&
1983
                   flv.get("ram") == mem + "" &&
1984
                   flv.get("disk") == disk + "" &&
1985
                   flv.get("disk_template") == disk_template &&
1986
                   filter_list.indexOf(flv) > -1) { return true; }
1987
            })[0];
1988
        },
1989
        
1990
        get_data: function(lst) {
1991
            var data = {'cpu': [], 'mem':[], 'disk':[], 'disk_template':[]};
1992

    
1993
            _.each(lst, function(flv) {
1994
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1995
                    data.cpu.push(flv.get("cpu"));
1996
                }
1997
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1998
                    data.mem.push(flv.get("ram"));
1999
                }
2000
                if (data.disk.indexOf(flv.get("disk")) == -1) {
2001
                    data.disk.push(flv.get("disk"));
2002
                }
2003
                if (data.disk_template.indexOf(flv.get("disk_template")) == -1) {
2004
                    data.disk_template.push(flv.get("disk_template"));
2005
                }
2006
            })
2007
            
2008
            return data;
2009
        },
2010

    
2011
        active: function() {
2012
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
2013
        }
2014
            
2015
    })
2016

    
2017
    models.VMS = models.Collection.extend({
2018
        model: models.VM,
2019
        path: 'servers',
2020
        details: true,
2021
        copy_image_meta: true,
2022

    
2023
        parse: function (resp, xhr) {
2024
            var data = resp;
2025
            if (!resp) { return [] };
2026
            data = _.filter(_.map(resp.servers, 
2027
                                  _.bind(this.parse_vm_api_data, this)), 
2028
                                  function(v){return v});
2029
            return data;
2030
        },
2031

    
2032
        parse_vm_api_data: function(data) {
2033
            // do not add non existing DELETED entries
2034
            if (data.status && data.status == "DELETED") {
2035
                if (!this.get(data.id)) {
2036
                    return false;
2037
                }
2038
            }
2039
            
2040
            if ('SNF:task_state' in data) { 
2041
                data['task_state'] = data['SNF:task_state'];
2042
                if (data['task_state']) {
2043
                    var status = models.VM.TASK_STATE_STATUS_MAP[data['task_state']];
2044
                    if (status) { data['status'] = status }
2045
                }
2046
            }
2047

    
2048
            // OS attribute
2049
            if (this.has_meta(data)) {
2050
                data['OS'] = data.metadata.OS || snf.config.unknown_os;
2051
            }
2052
            
2053
            if (!data.diagnostics) {
2054
                data.diagnostics = [];
2055
            }
2056

    
2057
            // network metadata
2058
            data['firewalls'] = {};
2059
            data['fqdn'] = data['SNF:fqdn'];
2060

    
2061
            // if vm has no metadata, no metadata object
2062
            // is in json response, reset it to force
2063
            // value update
2064
            if (!data['metadata']) {
2065
                data['metadata'] = {};
2066
            }
2067
            
2068
            // v2.0 API returns objects
2069
            data.image_obj = data.image;
2070
            data.image = data.image_obj.id;
2071
            data.flavor_obj = data.flavor;
2072
            data.flavor = data.flavor_obj.id;
2073

    
2074
            return data;
2075
        },
2076

    
2077
        get_reboot_required: function() {
2078
            return this.filter(function(vm){return vm.get("reboot_required") == true})
2079
        },
2080

    
2081
        has_pending_actions: function() {
2082
            return this.filter(function(vm){return vm.pending_action}).length > 0;
2083
        },
2084

    
2085
        reset_pending_actions: function() {
2086
            this.each(function(vm) {
2087
                vm.clear_pending_action();
2088
            })
2089
        },
2090

    
2091
        do_all_pending_actions: function(success, error) {
2092
            this.each(function(vm) {
2093
                if (vm.has_pending_action()) {
2094
                    vm.call(vm.pending_action, success, error);
2095
                    vm.clear_pending_action();
2096
                }
2097
            })
2098
        },
2099
        
2100
        do_all_reboots: function(success, error) {
2101
            this.each(function(vm) {
2102
                if (vm.get("reboot_required")) {
2103
                    vm.call("reboot", success, error);
2104
                }
2105
            });
2106
        },
2107

    
2108
        reset_reboot_required: function() {
2109
            this.each(function(vm) {
2110
                vm.set({'reboot_required': undefined});
2111
            })
2112
        },
2113
        
2114
        stop_stats_update: function(exclude) {
2115
            var exclude = exclude || [];
2116
            this.each(function(vm) {
2117
                if (exclude.indexOf(vm) > -1) {
2118
                    return;
2119
                }
2120
                vm.stop_stats_update();
2121
            })
2122
        },
2123
        
2124
        has_meta: function(vm_data) {
2125
            return vm_data.metadata && vm_data.metadata
2126
        },
2127

    
2128
        has_addresses: function(vm_data) {
2129
            return vm_data.metadata && vm_data.metadata
2130
        },
2131

    
2132
        create: function (name, image, flavor, meta, extra, callback) {
2133

    
2134
            if (this.copy_image_meta) {
2135
                if (synnefo.config.vm_image_common_metadata) {
2136
                    _.each(synnefo.config.vm_image_common_metadata, 
2137
                        function(key){
2138
                            if (image.get_meta(key)) {
2139
                                meta[key] = image.get_meta(key);
2140
                            }
2141
                    });
2142
                }
2143

    
2144
                if (image.get("OS")) {
2145
                    meta['OS'] = image.get("OS");
2146
                }
2147
            }
2148
            
2149
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, 
2150
                    metadata:meta}
2151
            opts = _.extend(opts, extra);
2152
            
2153
            var cb = function(data) {
2154
              synnefo.storage.quotas.get('cyclades.vm').increase();
2155
              callback(data);
2156
            }
2157

    
2158
            this.api_call(this.path, "create", {'server': opts}, undefined, 
2159
                          undefined, cb, {critical: true});
2160
        },
2161

    
2162
        load_missing_images: function(callback) {
2163
          var missing_ids = [];
2164
          var resolved = 0;
2165

    
2166
          // fill missing_ids
2167
          this.each(function(el) {
2168
            var imgid = el.get("image");
2169
            var existing = synnefo.storage.images.get(imgid);
2170
            if (!existing && missing_ids.indexOf(imgid) == -1) {
2171
              missing_ids.push(imgid);
2172
            }
2173
          });
2174
          var check = function() {
2175
            // once all missing ids where resolved continue calling the 
2176
            // callback
2177
            resolved++;
2178
            if (resolved == missing_ids.length) {
2179
              callback(missing_ids)
2180
            }
2181
          }
2182
          if (missing_ids.length == 0) {
2183
            callback(missing_ids);
2184
            return;
2185
          }
2186
          // start resolving missing image ids
2187
          _(missing_ids).each(function(imgid){
2188
            synnefo.storage.images.update_unknown_id(imgid, check);
2189
          });
2190
        },
2191

    
2192
        get_connectable: function() {
2193
            return storage.vms.filter(function(vm){
2194
                return !vm.in_error_state() && !vm.is_building();
2195
            });
2196
        }
2197
    })
2198
    
2199
    models.PublicKey = models.Model.extend({
2200
        path: 'keys',
2201
        api_type: 'userdata',
2202
        detail: false,
2203
        model_actions: {
2204
          'remove': [['name'], function() {
2205
            return true;
2206
          }]
2207
        },
2208

    
2209
        get_public_key: function() {
2210
            return cryptico.publicKeyFromString(this.get("content"));
2211
        },
2212

    
2213
        get_filename: function() {
2214
            return "{0}.pub".format(this.get("name"));
2215
        },
2216

    
2217
        identify_type: function() {
2218
            try {
2219
                var cont = snf.util.validatePublicKey(this.get("content"));
2220
                var type = cont.split(" ")[0];
2221
                return synnefo.util.publicKeyTypesMap[type];
2222
            } catch (err) { return false };
2223
        },
2224

    
2225
        rename: function(new_name) {
2226
          //this.set({'name': new_name});
2227
          this.sync("update", this, {
2228
            critical: true,
2229
            data: {'name': new_name}, 
2230
            success: _.bind(function(){
2231
              snf.api.trigger("call");
2232
            }, this)
2233
          });
2234
        }
2235
    })
2236
    
2237
    models._ActionsModel = models.Model.extend({
2238
      defaults: { pending: null },
2239
      actions: [],
2240
      status: {
2241
        INACTIVE: 0,
2242
        PENDING: 1,
2243
        CALLED: 2
2244
      },
2245

    
2246
      initialize: function(attrs, opts) {
2247
        models._ActionsModel.__super__.initialize.call(this, attrs);
2248
        this.actions = opts.actions;
2249
        this.model = opts.model;
2250
        this.bind("change", function() {
2251
          this.set({'pending': this.get_pending()});
2252
        }, this);
2253
        this.clear();
2254
      },
2255
      
2256
      _in_status: function(st) {
2257
        var actions = null;
2258
        _.each(this.attributes, function(status, action){
2259
          if (status == st) {
2260
            if (!actions) {
2261
              actions = []
2262
            }
2263
            actions.push(action);
2264
          }
2265
        });
2266
        return actions;
2267
      },
2268

    
2269
      get_pending: function() {
2270
        return this._in_status(this.status.PENDING);
2271
      },
2272

    
2273
      unset_pending_action: function(action) {
2274
        var data = {};
2275
        data[action] = this.status.INACTIVE;
2276
        this.set(data);
2277
      },
2278

    
2279
      set_pending_action: function(action, reset_pending) {
2280
        reset_pending = reset_pending === undefined ? true : reset_pending;
2281
        var data = {};
2282
        data[action] = this.status.PENDING;
2283
        if (reset_pending) {
2284
          this.reset_pending();
2285
        }
2286
        this.set(data);
2287
        this.trigger("set-pending", action);
2288
      },
2289
      
2290
      reset_pending: function() {
2291
        var data = {};
2292
        _.each(this.actions, function(action) {
2293
          data[action] = this.status.INACTIVE;
2294
        }, this);
2295
        this.set(data);
2296
        this.trigger("reset-pending");
2297
      }
2298
    });
2299

    
2300
    models.PublicPool = models.Model.extend({});
2301
    models.PublicPools = models.Collection.extend({
2302
      model: models.PublicPool,
2303
      path: 'os-floating-ip-pools',
2304
      api_type: 'compute',
2305
      noUpdate: true,
2306

    
2307
      parse: function(data) {
2308
        return _.map(data.floating_ip_pools, function(pool) {
2309
          pool.id = pool.name;
2310
          return pool;
2311
        });
2312
      }
2313
    });
2314

    
2315
    models.PublicKeys = models.Collection.extend({
2316
        model: models.PublicKey,
2317
        details: false,
2318
        path: 'keys',
2319
        api_type: 'userdata',
2320
        noUpdate: true,
2321
        updateEntries: true,
2322

    
2323
        generate_new: function(success, error) {
2324
            snf.api.sync('create', undefined, {
2325
                url: getUrl.call(this, this.base_url) + "/generate", 
2326
                success: success, 
2327
                error: error,
2328
                skip_api_error: true
2329
            });
2330
        },
2331
        
2332
        add_crypto_key: function(key, success, error, options) {
2333
            var options = options || {};
2334
            var m = new models.PublicKey();
2335

    
2336
            // guess a name
2337
            var name_tpl = "my generated public key";
2338
            var name = name_tpl;
2339
            var name_count = 1;
2340
            
2341
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
2342
                name = name_tpl + " " + name_count;
2343
                name_count++;
2344
            }
2345
            
2346
            m.set({name: name});
2347
            m.set({content: key});
2348
            
2349
            options.success = function () { return success(m) };
2350
            options.errror = error;
2351
            options.skip_api_error = true;
2352
            
2353
            this.create(m.attributes, options);
2354
        }
2355
    });
2356

    
2357
  
2358
    models.Quota = models.Model.extend({
2359

    
2360
        initialize: function() {
2361
            models.Quota.__super__.initialize.apply(this, arguments);
2362
            this.bind("change", this.check, this);
2363
            this.check();
2364
        },
2365
        
2366
        check: function() {
2367
            var usage, limit;
2368
            usage = this.get('usage');
2369
            limit = this.get('limit');
2370
            if (usage >= limit) {
2371
                this.trigger("available");
2372
            } else {
2373
                this.trigger("unavailable");
2374
            }
2375
        },
2376

    
2377
        increase: function(val) {
2378
            if (val === undefined) { val = 1};
2379
            this.set({'usage': this.get('usage') + val})
2380
        },
2381

    
2382
        decrease: function(val) {
2383
            if (val === undefined) { val = 1};
2384
            this.set({'usage': this.get('usage') - val})
2385
        },
2386

    
2387
        can_consume: function() {
2388
            var usage, limit;
2389
            usage = this.get('usage');
2390
            limit = this.get('limit');
2391
            if (usage >= limit) {
2392
                return false
2393
            } else {
2394
                return true
2395
            }
2396
        },
2397
        
2398
        is_bytes: function() {
2399
            return this.get('resource').get('unit') == 'bytes';
2400
        },
2401
        
2402
        get_available: function(active) {
2403
            suffix = '';
2404
            if (active) { suffix = '_active'}
2405
            var value = this.get('limit'+suffix) - this.get('usage'+suffix);
2406
            if (active) {
2407
              if (this.get('available') <= value) {
2408
                value = this.get('available');
2409
              }
2410
            }
2411
            if (value < 0) { return value }
2412
            return value
2413
        },
2414

    
2415
        get_readable: function(key, active) {
2416
            var value;
2417
            if (key == 'available') {
2418
                value = this.get_available(active);
2419
            } else {
2420
                value = this.get(key)
2421
            }
2422
            if (value <= 0) { value = 0 }
2423
            if (!this.is_bytes()) {
2424
              return value + "";
2425
            }
2426
            return snf.util.readablizeBytes(value);
2427
        }
2428
    });
2429

    
2430
    models.Quotas = models.Collection.extend({
2431
        model: models.Quota,
2432
        api_type: 'accounts',
2433
        path: 'quotas',
2434
        parse: function(resp) {
2435
            filtered = _.map(resp.system, function(value, key) {
2436
                var available = (value.limit - value.usage) || 0;
2437
                var available_active = available;
2438
                var keysplit = key.split(".");
2439
                var limit_active = value.limit;
2440
                var usage_active = value.usage;
2441
                keysplit[keysplit.length-1] = "total_" + keysplit[keysplit.length-1];
2442
                var activekey = keysplit.join(".");
2443
                var exists = resp.system[activekey];
2444
                if (exists) {
2445
                    available_active = exists.limit - exists.usage;
2446
                    limit_active = exists.limit;
2447
                    usage_active = exists.usage;
2448
                }
2449
                return _.extend(value, {'name': key, 'id': key, 
2450
                          'available': available,
2451
                          'available_active': available_active,
2452
                          'limit_active': limit_active,
2453
                          'usage_active': usage_active,
2454
                          'resource': snf.storage.resources.get(key)});
2455
            });
2456
            return filtered;
2457
        },
2458
        
2459
        get_by_id: function(k) {
2460
          return this.filter(function(q) { return q.get('name') == k})[0]
2461
        },
2462

    
2463
        get_available_for_vm: function(options) {
2464
          var quotas = synnefo.storage.quotas;
2465
          var key = 'available';
2466
          var available_quota = {};
2467
          _.each(['cyclades.ram', 'cyclades.cpu', 'cyclades.disk'], 
2468
            function (key) {
2469
              var value = quotas.get(key).get_available(true);
2470
              available_quota[key.replace('cyclades.', '')] = value;
2471
          });
2472
          return available_quota;
2473
        }
2474
    })
2475

    
2476
    models.Resource = models.Model.extend({
2477
        api_type: 'accounts',
2478
        path: 'resources'
2479
    });
2480

    
2481
    models.Resources = models.Collection.extend({
2482
        api_type: 'accounts',
2483
        path: 'resources',
2484
        model: models.Network,
2485

    
2486
        parse: function(resp) {
2487
            return _.map(resp, function(value, key) {
2488
                return _.extend(value, {'name': key, 'id': key});
2489
            })
2490
        }
2491
    });
2492
    
2493
    // storage initialization
2494
    snf.storage.images = new models.Images();
2495
    snf.storage.flavors = new models.Flavors();
2496
    snf.storage.vms = new models.VMS();
2497
    snf.storage.keys = new models.PublicKeys();
2498
    snf.storage.resources = new models.Resources();
2499
    snf.storage.quotas = new models.Quotas();
2500
    snf.storage.public_pools = new models.PublicPools();
2501

    
2502
})(this);