Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (95.8 kB)

1
// Copyright 2014 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
          this.actions.bind("set-pending", function(action) {
120
            this.trigger("action:set-pending", action, this.actions, this);
121
          }, this);
122
          this.actions.bind("unset-pending", function(action) {
123
            this.trigger("action:unset-pending", action, this.actions, this);
124
          }, this);
125
          this.actions.bind("reset-pending", function() {
126
            this.trigger("action:reset-pending", this.actions, this);
127
          }, this);
128

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

    
158
              if (!val) { 
159
                // update with undefined and return
160
                data[key] = undefined;
161
                this.set(data);
162
                return;
163
              };
164
            
165
              // retrieve related object (check if its a Model??)
166
              var obj = store.get(val);
167
              
168
              if (obj) {
169
                // set related object
170
                data[attr_name] = obj;
171
                this.set(data, {silent:true})
172
                this.trigger("change:" + attr_name, obj);
173
              } else {
174
                var self = this;
175
                var retry_to_resolve = function(store, val, key) {
176
                  var retries = 0;
177
                  var retry = window.setInterval(function(){
178
                    retries++;
179
                    if (retries > 200) {
180
                      clearInterval(retry);
181
                    }
182
                    var obj = store.get(val);
183
                    if (obj) {
184
                      data[key] = obj;
185
                      self.set(data, {silent:false});
186
                      clearInterval(retry);
187
                    }
188
                  }, 500);
189
                  return retry
190
                }
191
                retry_to_resolve(store, val, key);
192
              }
193
            }
194
            
195
            var self = this;
196
            function init_bindings(instance, store, key, attr, attr_resolver) {
197
              instance.bind('change:' + attr, function(model) {
198
                resolve_related_instance.call(model, store, key, attr_resolver(model, attr));
199
              }, this);
200

    
201
              instance.bind('add', function(model) {
202
                resolve_related_instance.call(model, store, key, attr_resolver(model, attr));
203
              }, this);
204
            }
205

    
206
            init_bindings(this, store, key, attr, attr_resolver);
207
            resolve_related_instance.call(this, store, key, attr_resolver(this, attr));
208
          }, this);
209
        },
210
        
211
        _proxy_model_cache: {},
212
        
213
        _bind_model: function(model, attr, check_attr, cb) {
214
          var proxy_cache_key = attr + '_' + check_attr;
215
          if (this._proxy_model_cache[proxy_cache_key]) {
216
            var proxy = this._proxy_model_cache[proxy_cache_key];
217
            proxy[0].unbind('change', proxy[1]);
218
          }
219
          var data = {};
220
          var changebind = _.bind(function() {
221
            data[attr] = cb.call(this, this.get(check_attr));
222
            this.set(data);
223
          }, this);
224
          model.bind('change', changebind);
225
          this._proxy_model_cache[proxy_cache_key] = [model, changebind];
226
        },
227

    
228
        _bind_attr: function(attr, check_attr, cb) {
229
          this.bind('change:' + check_attr, function() {
230
            if (this.get(check_attr) instanceof models.Model) {
231
              var model = this.get(check_attr);
232
              this._bind_model(model, attr, check_attr, cb);
233
            }
234
            var val = cb.call(this, this.get(check_attr));
235
            var data = {};
236
            if (this.get(attr) !== val) {
237
              data[attr] = val;
238
              this.set(data);
239
            }
240
          }, this);
241
        },
242

    
243
        _set_proxy_attr: function(attr, check_attr, cb) {
244
          // initial set
245
          var data = {};
246
          data[attr] = cb.call(this, this.get(check_attr));
247
          if (data[attr] !== undefined) {
248
            this.set(data, {silent:true});
249
          }
250
          if(this.get(check_attr) instanceof models.Model) {
251
            this._bind_model(this.get(check_attr), attr, check_attr, cb);
252
          }
253
          this._bind_attr(attr, check_attr, cb);
254
        },
255

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

    
290
        url: function(options) {
291
            return getUrl.call(this, this.base_url) + "/" + this.id;
292
        },
293

    
294
        api_path: function(options) {
295
            return this.path + "/" + this.id;
296
        },
297

    
298
        parse: function(resp, xhr) {
299
        },
300

    
301
        remove: function(complete, error, success) {
302
            this.api_call(this.api_path(), "delete", undefined, complete, error, success);
303
        },
304

    
305
        changedKeys: function() {
306
            return _.keys(this.changedAttributes() || {});
307
        },
308
            
309
        // return list of changed attributes that included in passed list
310
        // argument
311
        getKeysChanged: function(keys) {
312
            return _.intersection(keys, this.changedKeys());
313
        },
314
        
315
        // boolean check of keys changed
316
        keysChanged: function(keys) {
317
            return this.getKeysChanged(keys).length > 0;
318
        },
319

    
320
        // check if any of the passed attribues has changed
321
        hasOnlyChange: function(keys) {
322
            var ret = false;
323
            _.each(keys, _.bind(function(key) {
324
                if (this.changedKeys().length == 1 && this.changedKeys().indexOf(key) > -1) { ret = true};
325
            }, this));
326
            return ret;
327
        }
328

    
329
    })
330
    
331
    // Base object for all our model collections
332
    models.Collection = bb.Collection.extend({
333
        sync: snf.api.sync,
334
        api: snf.api,
335
        api_type: 'compute',
336
        supportIncUpdates: true,
337
        
338
        mapAttrs: function() {
339
          var params = _.toArray(arguments);
340
          return this.map(function(i) { 
341
            return _.map(params, function(attr) { 
342
              return i.get(attr);
343
            });
344
          })
345
        },
346

    
347
        mapAttr: function(attr) {
348
          return this.map(function(i) { return i.get(attr)})
349
        },
350

    
351
        filterAttr: function(attr, eq) {
352
          return this.filter(function(i) { return i.get(attr) === eq})
353
        },
354

    
355
        initialize: function() {
356
            models.Collection.__super__.initialize.apply(this, arguments);
357
            this.api_call = _.bind(this.api.call, this);
358
            if (this.sortFields) {
359
              _.each(this.sortFields, function(f) {
360
                this.bind("change:" + f, _.bind(this.resort, this));
361
              }, this);
362
            }
363
        },
364
          
365
        resort: function() {
366
          this.sort();
367
        },
368

    
369
        url: function(options, method) {
370
            return getUrl.call(this, this.base_url) + (
371
                    options.details || this.details && method != 'create' ? '/detail' : '');
372
        },
373
        
374
        delay_fetch: function(delay, options) {
375
          window.setTimeout(_.bind(function() {
376
            this.fetch(options)
377
          }, this), delay);
378
        },
379

    
380
        fetch: function(options) {
381
            if (!options) { options = {} };
382
            // default to update
383
            if (!this.noUpdate) {
384
                if (options.update === undefined) { options.update = true };
385
                if (!options.removeMissing && options.refresh) { 
386
                  options.removeMissing = true;
387
                };
388
                // for collections which associated models don't support 
389
                // deleted state identification through attributes, resolve  
390
                // deleted entries by checking for missing objects in fetch 
391
                // responses.
392
                if (this.updateEntries && options.removeMissing === undefined) {
393
                  options.removeMissing = true;
394
                }
395
            } else {
396
                if (options.refresh === undefined) {
397
                    options.refresh = true;
398
                    if (this.updateEntries) {
399
                      options.update = true;
400
                      options.removeMissing = true;
401
                    }
402
                }
403
            }
404
            // custom event foreach fetch
405
            return bb.Collection.prototype.fetch.call(this, options)
406
        },
407

    
408
        create: function(model, options) {
409
            var coll = this;
410
            options || (options = {});
411
            model = this._prepareModel(model, options);
412
            if (!model) return false;
413
            var success = options.success;
414
            options.success = function(nextModel, resp, xhr) {
415
                if (coll.add_on_create) {
416
                  coll.add(nextModel, options);
417
                }
418
                synnefo.api.trigger("quota:update");
419
                if (success) success(nextModel, resp, xhr);
420
            };
421
            model.save(null, options);
422
            return model;
423
        },
424

    
425
        get_fetcher: function(interval, increase, fast, increase_after_calls, 
426
                              max, initial_call, params, fetcher_id) {
427
            var fetch_params = params || {};
428
            var handler_options = {};
429

    
430
            fetch_params.skips_timeouts = true;
431
            handler_options.interval = interval;
432
            handler_options.increase = increase;
433
            handler_options.fast = fast;
434
            handler_options.increase_after_calls = increase_after_calls;
435
            handler_options.max= max;
436
            handler_options.id = fetcher_id || "collection";
437

    
438
            var last_ajax = undefined;
439
            var callback = _.bind(function() {
440
                // clone to avoid referenced objects
441
                var params = _.clone(fetch_params);
442
                updater._ajax = last_ajax;
443
                
444
                // wait for previous request to finish
445
                if (last_ajax && last_ajax.readyState < 4 && last_ajax.statusText != "timeout") {
446
                    // opera readystate for 304 responses is 0
447
                    if (!($.browser.opera && last_ajax.readyState == 0 && last_ajax.status == 304)) {
448
                        return;
449
                    }
450
                }
451
                last_ajax = this.fetch(params);
452
            }, this);
453
            handler_options.callback = callback;
454

    
455
            var updater = new snf.api.updateHandler(_.clone(_.extend(handler_options, fetch_params)));
456
            snf.api.bind("call", _.throttle(_.bind(function(){ updater.faster(true)}, this)), 1000);
457
            return updater;
458
        }
459
    });
460
    
461
    // Image model
462
    models.Image = models.Model.extend({
463
        path: 'images',
464
        
465
        get_size: function() {
466
            return parseInt(this.get('metadata') ? this.get('metadata').size : -1)
467
        },
468

    
469
        get_description: function(escape) {
470
            if (escape == undefined) { escape = true };
471
            if (escape) { return this.escape('description') || "No description available"}
472
            return this.get('description') || "No description available."
473
        },
474

    
475
        get_meta: function(key) {
476
            if (this.get('metadata') && this.get('metadata')) {
477
                if (!this.get('metadata')[key]) { return null }
478
                return _.escape(this.get('metadata')[key]);
479
            } else {
480
                return null;
481
            }
482
        },
483

    
484
        get_meta_keys: function() {
485
            if (this.get('metadata') && this.get('metadata')) {
486
                return _.keys(this.get('metadata'));
487
            } else {
488
                return [];
489
            }
490
        },
491

    
492
        get_owner: function() {
493
            return this.get('owner') || _.keys(synnefo.config.system_images_owners)[0];
494
        },
495

    
496
        get_owner_uuid: function() {
497
            return this.get('owner_uuid');
498
        },
499

    
500
        is_system_image: function() {
501
          var owner = this.get_owner();
502
          return _.include(_.keys(synnefo.config.system_images_owners), owner)
503
        },
504

    
505
        owned_by: function(user) {
506
          if (!user) { user = synnefo.user }
507
          return user.get_username() == this.get('owner_uuid');
508
        },
509

    
510
        display_owner: function() {
511
            var owner = this.get_owner();
512
            if (_.include(_.keys(synnefo.config.system_images_owners), owner)) {
513
                return synnefo.config.system_images_owners[owner];
514
            } else {
515
                return owner;
516
            }
517
        },
518
    
519
        get_readable_size: function() {
520
            if (this.is_deleted()) {
521
                return synnefo.config.image_deleted_size_title || '(none)';
522
            }
523
            return this.get_size() > 0 ? util.readablizeBytes(this.get_size() * 1024 * 1024) : '(none)';
524
        },
525

    
526
        get_os: function() {
527
            return this.get_meta('OS');
528
        },
529

    
530
        get_gui: function() {
531
            return this.get_meta('GUI');
532
        },
533

    
534
        get_created_users: function() {
535
            try {
536
              var users = this.get_meta('users').split(" ");
537
            } catch (err) { users = null }
538
            if (!users) {
539
                var osfamily = this.get_meta('osfamily');
540
                if (osfamily == 'windows') { 
541
                  users = ['Administrator'];
542
                } else {
543
                  users = ['root'];
544
                }
545
            }
546
            return users;
547
        },
548

    
549
        get_sort_order: function() {
550
            return parseInt(this.get('metadata') ? this.get('metadata').sortorder : -1)
551
        },
552

    
553
        get_vm: function() {
554
            var vm_id = this.get("serverRef");
555
            var vm = undefined;
556
            vm = storage.vms.get(vm_id);
557
            return vm;
558
        },
559

    
560
        is_public: function() {
561
            return this.get('is_public') == undefined ? true : this.get('is_public');
562
        },
563

    
564
        is_deleted: function() {
565
            return this.get('status') == "DELETED"
566
        },
567
        
568
        ssh_keys_paths: function() {
569
            return _.map(this.get_created_users(), function(username) {
570
                prepend = '';
571
                if (username != 'root') {
572
                    prepend = '/home'
573
                }
574
                return {'user': username, 'path': '{1}/{0}/.ssh/authorized_keys'.format(username, 
575
                                                             prepend)};
576
            });
577
        },
578

    
579
        _supports_ssh: function() {
580
            var exclude_list = synnefo.config.ssh_support_osfamily_exclude_list || [];
581
            var os = this.get_os();
582
            if (exclude_list.indexOf(os) > -1) {
583
                return false;
584
            }
585
            return true;
586
        },
587

    
588
        supports: function(feature) {
589
            if (feature == "ssh") {
590
                return this._supports_ssh()
591
            }
592
            return false;
593
        },
594

    
595
        personality_data_for_keys: function(keys) {
596
            return _.map(this.ssh_keys_paths(), function(pathinfo) {
597
                var contents = '';
598
                _.each(keys, function(key){
599
                    contents = contents + key.get("content") + "\n"
600
                });
601
                contents = $.base64.encode(contents);
602

    
603
                return {
604
                    path: pathinfo.path,
605
                    contents: contents,
606
                    mode: 0600,
607
                    owner: pathinfo.user
608
                }
609
            });
610
        }
611
    });
612

    
613
    // Flavor model
614
    models.Flavor = models.Model.extend({
615
        path: 'flavors',
616

    
617
        details_string: function() {
618
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
619
        },
620

    
621
        get_disk_size: function() {
622
            return parseInt(this.get("disk") * 1024)
623
        },
624

    
625
        get_ram_size: function() {
626
            return parseInt(this.get("ram"))
627
        },
628

    
629
        get_disk_template_info: function() {
630
            var info = snf.config.flavors_disk_templates_info[this.get("disk_template")];
631
            if (!info) {
632
                info = { name: this.get("disk_template"), description:'' };
633
            }
634
            return info
635
        },
636

    
637
        disk_to_bytes: function() {
638
            return parseInt(this.get("disk")) * 1024 * 1024 * 1024;
639
        },
640

    
641
        ram_to_bytes: function() {
642
            return parseInt(this.get("ram")) * 1024 * 1024;
643
        },
644

    
645
        get_readable: function(key) {
646
          var parser = function(v) { return v }
647
          var getter = this.get;
648
          if (key == 'ram') {
649
            parser = synnefo.util.readablizeBytes
650
            getter = this.ram_to_bytes
651
          }
652
          if (key == 'disk') {
653
            parser = synnefo.util.readablizeBytes
654
            getter = this.ram_to_bytes
655
          }
656
          if (key == 'cpu') {
657
            parser = function(v) { return v + 'x' }
658
          }
659

    
660
          var val = getter.call(this, key);
661
          return parser(val);
662
        },
663

    
664
        quotas: function() {
665
          return {
666
            'cyclades.vm': 1,
667
            'cyclades.disk': this.disk_to_bytes(), 
668
            'cyclades.ram': this.ram_to_bytes(),
669
            'cyclades.cpu': this.get('cpu')
670
          }
671
        }
672

    
673
    });
674
    
675
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
676
    _.extend(models.ParamsList.prototype, bb.Events, {
677

    
678
        initialize: function(parent, param_name) {
679
            this.parent = parent;
680
            this.actions = {};
681
            this.param_name = param_name;
682
            this.length = 0;
683
        },
684
        
685
        has_action: function(action) {
686
            return this.actions[action] ? true : false;
687
        },
688
            
689
        _parse_params: function(arguments) {
690
            if (arguments.length <= 1) {
691
                return [];
692
            }
693

    
694
            var args = _.toArray(arguments);
695
            return args.splice(1);
696
        },
697

    
698
        contains: function(action, params) {
699
            params = this._parse_params(arguments);
700
            var has_action = this.has_action(action);
701
            if (!has_action) { return false };
702

    
703
            var paramsEqual = false;
704
            _.each(this.actions[action], function(action_params) {
705
                if (_.isEqual(action_params, params)) {
706
                    paramsEqual = true;
707
                }
708
            });
709
                
710
            return paramsEqual;
711
        },
712
        
713
        is_empty: function() {
714
            return _.isEmpty(this.actions);
715
        },
716

    
717
        add: function(action, params) {
718
            params = this._parse_params(arguments);
719
            if (this.contains.apply(this, arguments)) { return this };
720
            var isnew = false
721
            if (!this.has_action(action)) {
722
                this.actions[action] = [];
723
                isnew = true;
724
            };
725

    
726
            this.actions[action].push(params);
727
            this.parent.trigger("change:" + this.param_name, this.parent, this);
728
            if (isnew) {
729
                this.trigger("add", action, params);
730
            } else {
731
                this.trigger("change", action, params);
732
            }
733
            return this;
734
        },
735
        
736
        remove_all: function(action) {
737
            if (this.has_action(action)) {
738
                delete this.actions[action];
739
                this.parent.trigger("change:" + this.param_name, this.parent, this);
740
                this.trigger("remove", action);
741
            }
742
            return this;
743
        },
744

    
745
        reset: function() {
746
            this.actions = {};
747
            this.parent.trigger("change:" + this.param_name, this.parent, this);
748
            this.trigger("reset");
749
            this.trigger("remove");
750
        },
751

    
752
        remove: function(action, params) {
753
            params = this._parse_params(arguments);
754
            if (!this.has_action(action)) { return this };
755
            var index = -1;
756
            _.each(this.actions[action], _.bind(function(action_params) {
757
                if (_.isEqual(action_params, params)) {
758
                    index = this.actions[action].indexOf(action_params);
759
                }
760
            }, this));
761
            
762
            if (index > -1) {
763
                this.actions[action].splice(index, 1);
764
                if (_.isEmpty(this.actions[action])) {
765
                    delete this.actions[action];
766
                }
767
                this.parent.trigger("change:" + this.param_name, this.parent, this);
768
                this.trigger("remove", action, params);
769
            }
770
        }
771

    
772
    });
773

    
774
    // Virtualmachine model
775
    models.VM = models.Model.extend({
776

    
777
        path: 'servers',
778
        has_status: true,
779
        proxy_attrs: {
780
          'busy': [
781
            ['status', 'state'], function() {
782
              return !_.contains(['ACTIVE', 'STOPPED'], this.get('status'));
783
            }
784
          ],
785
          'in_progress': [
786
            ['status', 'state'], function() {
787
              return this.in_transition();
788
            }
789
          ]
790
        },
791

    
792
        storage_attrs: {
793
          'tenant_id': ['projects', 'project']
794
        },
795

    
796
        initialize: function(params) {
797
            var self = this;
798
            this.ports = new Backbone.FilteredCollection(undefined, {
799
              collection: synnefo.storage.ports,
800
              collectionFilter: function(m) {
801
                return self.id == m.get('device_id')
802
            }});
803

    
804
            this.pending_firewalls = {};
805
            
806
            models.VM.__super__.initialize.apply(this, arguments);
807

    
808

    
809
            this.set({state: params.status || "ERROR"});
810
            this.log = new snf.logging.logger("VM " + this.id);
811
            this.pending_action = undefined;
812
            
813
            // init stats parameter
814
            this.set({'stats': undefined}, {silent: true});
815
            // defaults to not update the stats
816
            // each view should handle this vm attribute 
817
            // depending on if it displays stat images or not
818
            this.do_update_stats = false;
819
            
820
            // interval time
821
            // this will dynamicaly change if the server responds that
822
            // images get refreshed on different intervals
823
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
824
            this.stats_available = false;
825

    
826
            // initialize interval
827
            this.init_stats_intervals(this.stats_update_interval);
828
            
829
            // handle progress message on instance change
830
            this.bind("change", _.bind(this.update_status_message, this));
831
            this.bind("change:task_state", _.bind(this.update_status, this));
832
            // force update of progress message
833
            this.update_status_message(true);
834
            
835
            // default values
836
            this.bind("change:state", _.bind(function(){
837
                if (this.state() == "DESTROY") { 
838
                    this.handle_destroy() 
839
                }
840
            }, this));
841

    
842
        },
843
        
844
        get_public_ips: function() {
845
          var ips = [];
846
          this.ports.filter(function(port) {
847
            if (port.get('network') && !port.get('network').get('is_public')) { return }
848
            if (!port.get("ips")) { return }
849
            port.get("ips").each(function(ip) {
850
              ips.push(ip);
851
            });
852
          });
853
          return ips;
854
        },
855

    
856
        has_public_ip: function() {
857
          return this.ports.filter(function(port) {
858
            return port.get("network") && 
859
                   port.get("network").get("is_public") && 
860
                   port.get("ips").length > 0;
861
          }).length > 0;
862
        },
863

    
864
        has_public_ipv6: function() {
865
          return this.has_ip_version("v6", true);
866
        },
867

    
868
        has_public_ipv4: function() {
869
          return this.has_ip_version("v4", true);
870
        },
871
        
872
        has_ip_version: function(ver, public) {
873
          var found = false;
874
          this.ports.each(function(port) {
875
            if (found) { return }
876
            if (public !== undefined) {
877
              if (port.get("network") && 
878
                  port.get("network").get("is_public") != public) {
879
                return
880
              }
881
            }
882
            port.get('ips').each(function(ip) {
883
              if (found) { return }
884
              if (ip.get("type") == ver) {
885
                found = true
886
              }
887
            })
888
          }, this)
889
          return found;
890
        },
891

    
892
        status: function(st) {
893
            if (!st) { return this.get("status")}
894
            return this.set({status:st});
895
        },
896
        
897
        update_status: function() {
898
            this.set_status(this.get('status'));
899
        },
900

    
901
        set_status: function(st) {
902
            var new_state = this.state_for_api_status(st);
903
            var transition = false;
904

    
905
            if (this.state() != new_state) {
906
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
907
                    transition = this.state();
908
                }
909
            }
910
            
911
            // call it silently to avoid double change trigger
912
            var state = this.state_for_api_status(st);
913
            this.set({'state': state}, {silent: true});
914
            
915
            // trigger transition
916
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
917
                this.trigger("transition", {from:transition, to:new_state}) 
918
            };
919
            return st;
920
        },
921
            
922
        get_diagnostics: function(success) {
923
            this.__make_api_call(this.get_diagnostics_url(),
924
                                 "read", // create so that sync later uses POST to make the call
925
                                 null, // payload
926
                                 function(data) {
927
                                     success(data);
928
                                 },  
929
                                 null, 'diagnostics');
930
        },
931

    
932
        has_diagnostics: function() {
933
            return this.get("diagnostics") && this.get("diagnostics").length;
934
        },
935

    
936
        get_progress_info: function() {
937
            // details about progress message
938
            // contains a list of diagnostic messages
939
            return this.get("status_messages");
940
        },
941

    
942
        get_status_message: function() {
943
            return this.get('status_message');
944
        },
945
        
946
        // extract status message from diagnostics
947
        status_message_from_diagnostics: function(diagnostics) {
948
            var valid_sources_map = synnefo.config.diagnostics_status_messages_map;
949
            var valid_sources = valid_sources_map[this.get('status')];
950
            if (!valid_sources) { return null };
951
            
952
            // filter messsages based on diagnostic source
953
            var messages = _.filter(diagnostics, function(diag) {
954
                return valid_sources.indexOf(diag.source) > -1;
955
            });
956

    
957
            var msg = messages[0];
958
            if (msg) {
959
              var message = msg.message;
960
              var message_tpl = snf.config.diagnostic_messages_tpls[msg.source];
961

    
962
              if (message_tpl) {
963
                  message = message_tpl.replace('MESSAGE', msg.message);
964
              }
965
              return message;
966
            }
967
            
968
            // no message to display, but vm in build state, display
969
            // finalizing message.
970
            if (this.is_building() == 'BUILD') {
971
                return synnefo.config.BUILDING_MESSAGES['FINAL'];
972
            }
973
            return null;
974
        },
975

    
976
        update_status_message: function(force) {
977
            // update only if one of the specified attributes has changed
978
            if (
979
              !this.keysChanged(['diagnostics', 'progress', 'status', 'state'])
980
                && !force
981
            ) { return };
982
            
983
            // if user requested to destroy the vm set the appropriate 
984
            // message.
985
            if (this.get('state') == "DESTROY") { 
986
                message = "Terminating..."
987
                this.set({status_message: message})
988
                return;
989
            }
990
            
991
            // set error message, if vm has diagnostic message display it as
992
            // progress message
993
            if (this.in_error_state()) {
994
                var d = this.get('diagnostics');
995
                if (d && d.length) {
996
                    var message = this.status_message_from_diagnostics(d);
997
                    this.set({status_message: message});
998
                } else {
999
                    this.set({status_message: null});
1000
                }
1001
                return;
1002
            }
1003
            
1004
            // identify building status message
1005
            if (this.is_building()) {
1006
                var self = this;
1007
                var success = function(msg) {
1008
                    self.set({status_message: msg});
1009
                }
1010
                this.get_building_status_message(success);
1011
                return;
1012
            }
1013

    
1014
            this.set({status_message:null});
1015
        },
1016
            
1017
        // get building status message. Asynchronous function since it requires
1018
        // access to vm image.
1019
        get_building_status_message: function(callback) {
1020
            // no progress is set, vm is in initial build status
1021
            var progress = this.get("progress");
1022
            if (progress == 0 || !progress) {
1023
                return callback(BUILDING_MESSAGES['INIT']);
1024
            }
1025
            
1026
            // vm has copy progress, display copy percentage
1027
            if (progress > 0 && progress <= 99) {
1028
                this.get_copy_details(true, undefined, _.bind(
1029
                    function(details){
1030
                        callback(BUILDING_MESSAGES['COPY'].format(details.copy, 
1031
                                                           details.size, 
1032
                                                           details.progress));
1033
                }, this));
1034
                return;
1035
            }
1036

    
1037
            // copy finished display FINAL message or identify status message
1038
            // from diagnostics.
1039
            if (progress >= 100) {
1040
                if (!this.has_diagnostics()) {
1041
                        callback(BUILDING_MESSAGES['FINAL']);
1042
                } else {
1043
                        var d = this.get("diagnostics");
1044
                        var msg = this.status_message_from_diagnostics(d);
1045
                        if (msg) {
1046
                              callback(msg);
1047
                        }
1048
                }
1049
            }
1050
        },
1051

    
1052
        get_copy_details: function(human, image, callback) {
1053
            var human = human || false;
1054
            var image = image || this.get_image(_.bind(function(image){
1055
                var progress = this.get('progress');
1056
                var size = image.get_size();
1057
                var size_copied = (size * progress / 100).toFixed(2);
1058
                
1059
                if (human) {
1060
                    size = util.readablizeBytes(size*1024*1024);
1061
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
1062
                }
1063

    
1064
                callback({'progress': progress, 'size': size, 'copy': size_copied})
1065
            }, this));
1066
        },
1067

    
1068
        start_stats_update: function(force_if_empty) {
1069
            var prev_state = this.do_update_stats;
1070

    
1071
            this.do_update_stats = true;
1072
            
1073
            // fetcher initialized ??
1074
            if (!this.stats_fetcher) {
1075
                this.init_stats_intervals();
1076
            }
1077

    
1078

    
1079
            // fetcher running ???
1080
            if (!this.stats_fetcher.running || !prev_state) {
1081
                this.stats_fetcher.start();
1082
            }
1083

    
1084
            if (force_if_empty && this.get("stats") == undefined) {
1085
                this.update_stats(true);
1086
            }
1087
        },
1088

    
1089
        stop_stats_update: function(stop_calls) {
1090
            this.do_update_stats = false;
1091

    
1092
            if (stop_calls) {
1093
                this.stats_fetcher.stop();
1094
            }
1095
        },
1096

    
1097
        // clear and reinitialize update interval
1098
        init_stats_intervals: function (interval) {
1099
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
1100
            this.stats_fetcher.start();
1101
        },
1102
        
1103
        get_stats_fetcher: function(timeout) {
1104
            var cb = _.bind(function(data){
1105
                this.update_stats();
1106
            }, this);
1107
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
1108
            return fetcher;
1109
        },
1110

    
1111
        // do the api call
1112
        update_stats: function(force) {
1113
            // do not update stats if flag not set
1114
            if ((!this.do_update_stats && !force) || this.updating_stats) {
1115
                return;
1116
            }
1117

    
1118
            // make the api call, execute handle_stats_update on sucess
1119
            // TODO: onError handler ???
1120
            stats_url = this.url() + "/stats";
1121
            this.updating_stats = true;
1122
            this.sync("read", this, {
1123
                handles_error:true, 
1124
                url: stats_url, 
1125
                refresh:true, 
1126
                success: _.bind(this.handle_stats_update, this),
1127
                error: _.bind(this.handle_stats_error, this),
1128
                complete: _.bind(function(){this.updating_stats = false;}, this),
1129
                critical: false,
1130
                log_error: false,
1131
                skips_timeouts: true
1132
            });
1133
        },
1134

    
1135
        get_attachment: function(id) {
1136
          var attachment = undefined;
1137
          _.each(this.get("attachments"), function(a) {
1138
            if (a.id == id) {
1139
              attachment = a;
1140
            }
1141
          });
1142
          return attachment
1143
        },
1144

    
1145
        _set_stats: function(stats) {
1146
            var silent = silent === undefined ? false : silent;
1147
            // unavailable stats while building
1148
            if (this.get("status") == "BUILD") { 
1149
                this.stats_available = false;
1150
            } else { this.stats_available = true; }
1151

    
1152
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
1153
            
1154
            this.set({stats: stats}, {silent:true});
1155
            this.trigger("stats:update", stats);
1156
        },
1157

    
1158
        unbind: function() {
1159
            models.VM.__super__.unbind.apply(this, arguments);
1160
        },
1161
        
1162
        can_start: function(flv, count_current) {
1163
          var self = this;
1164
          var get_quota = function(key) {
1165
            if (!self.get('project')) { return false }
1166
            return self.get('project').quotas.get(key).get('available');
1167
          }
1168
          var flavor = flv || this.get_flavor();
1169
          var vm_ram_current = 0, vm_cpu_current = 0;
1170
          if (flv && this.is_active() || flv && count_current) {
1171
            var current = this.get_flavor();
1172
            vm_ram_current = current.ram_to_bytes();
1173
            vm_cpu_current = parseInt(current.get('cpu'));
1174
          }
1175
          var vm_ram = flavor.ram_to_bytes();
1176
          var vm_cpu = parseInt(flavor.get('cpu'));
1177
          var available_cpu = get_quota('cyclades.cpu') + vm_cpu_current;
1178
          var available_ram = get_quota('cyclades.ram') + vm_ram_current;
1179
          if (vm_ram > available_ram || vm_cpu > available_cpu) { return false }
1180
          return true
1181
        },
1182

    
1183
        can_connect: function() {
1184
          if (!synnefo.config.hotplug_enabled && this.is_active()) { return false }
1185
          return _.contains(["ACTIVE", "STOPPED"], this.get("status")) && 
1186
                 !this.get('suspended')
1187
        },
1188

    
1189
        can_disconnect: function() {
1190
          return _.contains(["ACTIVE", "STOPPED"], this.get("status"))
1191
        },
1192

    
1193
        can_resize: function() {
1194
          return this.get('status') == 'STOPPED';
1195
        },
1196

    
1197
        can_reassign: function() {
1198
          return true;
1199
        },
1200

    
1201
        handle_stats_error: function() {
1202
            stats = {};
1203
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1204
                stats[k] = false;
1205
            });
1206

    
1207
            this.set({'stats': stats});
1208
        },
1209

    
1210
        // this method gets executed after a successful vm stats api call
1211
        handle_stats_update: function(data) {
1212
            var self = this;
1213
            // avoid browser caching
1214
            
1215
            if (data.stats && _.size(data.stats) > 0) {
1216
                var ts = $.now();
1217
                var stats = data.stats;
1218
                var images_loaded = 0;
1219
                var images = {};
1220

    
1221
                function check_images_loaded() {
1222
                    images_loaded++;
1223

    
1224
                    if (images_loaded == 4) {
1225
                        self._set_stats(images);
1226
                    }
1227
                }
1228
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1229
                    
1230
                    stats[k] = stats[k] + "?_=" + ts;
1231
                    
1232
                    var stat = k.slice(0,3);
1233
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
1234
                    var img = $("<img />");
1235
                    var val = stats[k];
1236
                    
1237
                    // load stat image to a temporary dom element
1238
                    // update model stats on image load/error events
1239
                    img.load(function() {
1240
                        images[k] = val;
1241
                        check_images_loaded();
1242
                    });
1243

    
1244
                    img.error(function() {
1245
                        images[stat + type] = false;
1246
                        check_images_loaded();
1247
                    });
1248

    
1249
                    img.attr({'src': stats[k]});
1250
                })
1251
                data.stats = stats;
1252
            }
1253

    
1254
            // do we need to change the interval ??
1255
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
1256
                this.stats_update_interval = data.stats.refresh * 1000;
1257
                this.stats_fetcher.interval = this.stats_update_interval;
1258
                this.stats_fetcher.maximum_interval = this.stats_update_interval;
1259
                this.stats_fetcher.stop();
1260
                this.stats_fetcher.start(false);
1261
            }
1262
        },
1263

    
1264
        // helper method that sets the do_update_stats
1265
        // in the future this method could also make an api call
1266
        // immediaetly if needed
1267
        enable_stats_update: function() {
1268
            this.do_update_stats = true;
1269
        },
1270
        
1271
        handle_destroy: function() {
1272
            this.stats_fetcher.stop();
1273
        },
1274

    
1275
        require_reboot: function() {
1276
            if (this.is_active()) {
1277
                this.set({'reboot_required': true});
1278
            }
1279
        },
1280
        
1281
        set_pending_action: function(data) {
1282
            this.pending_action = data;
1283
            return data;
1284
        },
1285

    
1286
        // machine has pending action
1287
        update_pending_action: function(action, force) {
1288
            this.set({pending_action: action});
1289
        },
1290

    
1291
        clear_pending_action: function() {
1292
            this.set({pending_action: undefined});
1293
        },
1294

    
1295
        has_pending_action: function() {
1296
            return this.get("pending_action") ? this.get("pending_action") : false;
1297
        },
1298
        
1299
        // machine is active
1300
        is_active: function() {
1301
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
1302
        },
1303
        
1304
        // machine is building 
1305
        is_building: function() {
1306
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
1307
        },
1308
        
1309
        is_rebooting: function() {
1310
            return this.state() == 'REBOOT';
1311
        },
1312

    
1313
        in_error_state: function() {
1314
            return this.state() === "ERROR"
1315
        },
1316

    
1317
        // user can connect to machine
1318
        is_connectable: function() {
1319
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1320
        },
1321
        
1322
        remove_meta: function(key, complete, error) {
1323
            var url = this.api_path() + "/metadata/" + key;
1324
            this.api_call(url, "delete", undefined, complete, error);
1325
        },
1326

    
1327
        save_meta: function(meta, complete, error) {
1328
            var url = this.api_path() + "/metadata/" + meta.key;
1329
            var payload = {meta:{}};
1330
            payload.meta[meta.key] = meta.value;
1331
            payload._options = {
1332
                critical:false, 
1333
                error_params: {
1334
                    title: "Machine metadata error",
1335
                    extra_details: {"Machine id": this.id}
1336
            }};
1337

    
1338
            this.api_call(url, "update", payload, complete, error);
1339
        },
1340

    
1341

    
1342
        // update/get the state of the machine
1343
        state: function() {
1344
            var args = slice.call(arguments);
1345
                
1346
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1347
                this.set({'state': args[0]});
1348
            }
1349

    
1350
            return this.get('state');
1351
        },
1352
        
1353
        // get the state that the api status corresponds to
1354
        state_for_api_status: function(status) {
1355
            return this.state_transition(this.state(), status);
1356
        },
1357
        
1358
        // get transition state for the corresponging api status
1359
        state_transition: function(state, new_status) {
1360
            var statuses = models.VM.STATES_TRANSITIONS[state];
1361
            if (statuses) {
1362
                if (statuses.indexOf(new_status) > -1) {
1363
                    return new_status;
1364
                } else {
1365
                    return state;
1366
                }
1367
            } else {
1368
                return new_status;
1369
            }
1370
        },
1371
        
1372
        // the current vm state is a transition state
1373
        in_transition: function() {
1374
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
1375
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
1376
        },
1377
        
1378
        // get image object
1379
        get_image: function(callback) {
1380
            if (callback == undefined) { callback = function(){} }
1381
            var image = storage.images.get(this.get('image'));
1382
            if (!image) {
1383
                storage.images.update_unknown_id(this.get('image'), callback);
1384
                return;
1385
            }
1386
            callback(image);
1387
            return image;
1388
        },
1389
        
1390
        // get flavor object
1391
        get_flavor: function() {
1392
            var flv = storage.flavors.get(this.get('flavor'));
1393
            if (!flv) {
1394
                storage.flavors.update_unknown_id(this.get('flavor'));
1395
                flv = storage.flavors.get(this.get('flavor'));
1396
            }
1397
            return flv;
1398
        },
1399

    
1400
        get_resize_flavors: function() {
1401
          var vm_flavor = this.get_flavor();
1402
          var flavors = synnefo.storage.flavors.filter(function(f){
1403
              return f.get('disk_template') ==
1404
              vm_flavor.get('disk_template') && f.get('disk') ==
1405
              vm_flavor.get('disk');
1406
          });
1407
          return flavors;
1408
        },
1409

    
1410
        get_flavor_quotas: function() {
1411
          var flavor = this.get_flavor();
1412
          return {
1413
            cpu: flavor.get('cpu'), 
1414
            ram: flavor.get_ram_size(), 
1415
            disk:flavor.get_disk_size()
1416
          }
1417
        },
1418

    
1419
        get_meta: function(key, deflt) {
1420
            if (this.get('metadata') && this.get('metadata')) {
1421
                if (!this.get('metadata')[key]) { return deflt }
1422
                return _.escape(this.get('metadata')[key]);
1423
            } else {
1424
                return deflt;
1425
            }
1426
        },
1427

    
1428
        get_meta_keys: function() {
1429
            if (this.get('metadata') && this.get('metadata')) {
1430
                return _.keys(this.get('metadata'));
1431
            } else {
1432
                return [];
1433
            }
1434
        },
1435
        
1436
        // get metadata OS value
1437
        get_os: function() {
1438
            var image = this.get_image();
1439
            return this.get_meta('OS') || (image ? 
1440
                                            image.get_os() || "okeanos" : "okeanos");
1441
        },
1442

    
1443
        get_gui: function() {
1444
            return this.get_meta('GUI');
1445
        },
1446
        
1447
        get_hostname: function() {
1448
          return this.get_meta('hostname') || this.get('fqdn') || synnefo.config.no_fqdn_message;
1449
        },
1450

    
1451
        // get actions that the user can execute
1452
        // depending on the vm state/status
1453
        get_available_actions: function() {
1454
            if (this.get('suspended')) { return [] }
1455
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1456
        },
1457

    
1458
        set_profile: function(profile, net_id) {
1459
        },
1460
        
1461
        // call rename api
1462
        rename: function(new_name) {
1463
            //this.set({'name': new_name});
1464
            var self = this;
1465
            this.sync("update", this, {
1466
                critical: true,
1467
                data: {
1468
                    'server': {
1469
                        'name': new_name
1470
                    }
1471
                }, 
1472
                success: _.bind(function(){
1473
                    snf.api.trigger("call");
1474
                    this.set({'name': new_name});
1475
                }, this)
1476
            });
1477
        },
1478
        
1479
        get_console_url: function(data) {
1480
            var url_params = {
1481
                machine: this.get("name"),
1482
                host_ip: this.get_hostname(),
1483
                host_ip_v6: this.get_hostname(),
1484
                host: data.host,
1485
                port: data.port,
1486
                password: data.password
1487
            }
1488
            return synnefo.config.ui_console_url + '?' + $.param(url_params);
1489
        },
1490
        
1491
        set_firewall: function(nic, value, success_cb, error_cb) {
1492
          var self = this;
1493
          var success = function() { self.require_reboot(); success_cb() }
1494
          var error = function() { error_cb() }
1495
          var data = {'nic': nic.id, 'profile': value, 'display': true};
1496
          var url = this.url() + "/action";
1497
          //var params = {skip_api_error: false, display: true};
1498
          this.call('firewallProfile', success, error, data);
1499
        },
1500

    
1501
        connect_floating_ip: function(ip, cb, error) {
1502
          var self = this;
1503
          var from_status = this.get('status');
1504
          this.set({'status': 'CONNECTING'});
1505
          synnefo.storage.ports.create({
1506
            port: {
1507
              network_id: ip.get('floating_network_id'),
1508
              device_id: this.id,
1509
              fixed_ips: [{'ip_address': ip.get('floating_ip_address')}]
1510
            }
1511
          }, {
1512
            success: cb, 
1513
            error: function() { error && error() },
1514
            skip_api_error: false
1515
          });
1516
        },
1517

    
1518
        // action helper
1519
        call: function(action_name, success, error, params) {
1520
            var id_param = [this.id];
1521
            
1522
            params = params || {};
1523
            success = success || function() {};
1524
            error = error || function() {};
1525

    
1526
            var self = this;
1527

    
1528
            switch(action_name) {
1529
                case 'start':
1530
                    this.__make_api_call(this.get_action_url(), // vm actions url
1531
                                         "create", // create so that sync later uses POST to make the call
1532
                                         {start:{}}, // payload
1533
                                         function() {
1534
                                             // set state after successful call
1535
                                             self.state("START"); 
1536
                                             success.apply(this, arguments);
1537
                                             snf.api.trigger("call");
1538
                                         },  
1539
                                         error, 'start', params);
1540
                    break;
1541
                case 'reboot':
1542
                    this.__make_api_call(this.get_action_url(), // vm actions url
1543
                                         "create", // create so that sync later uses POST to make the call
1544
                                         {reboot:{}}, // payload
1545
                                         function() {
1546
                                             // set state after successful call
1547
                                             self.state("REBOOT"); 
1548
                                             success.apply(this, arguments)
1549
                                             snf.api.trigger("call");
1550
                                             self.set({'reboot_required': false});
1551
                                         },
1552
                                         error, 'reboot', params);
1553
                    break;
1554
                case 'shutdown':
1555
                    this.__make_api_call(this.get_action_url(), // vm actions url
1556
                                         "create", // create so that sync later uses POST to make the call
1557
                                         {shutdown:{}}, // payload
1558
                                         function() {
1559
                                             // set state after successful call
1560
                                             self.state("SHUTDOWN"); 
1561
                                             success.apply(this, arguments)
1562
                                             snf.api.trigger("call");
1563
                                         },  
1564
                                         error, 'shutdown', params);
1565
                    break;
1566
                case 'console':
1567
                    this.__make_api_call(this.url() + "/action", "create", 
1568
                                         {'console': {'type':'vnc'}}, 
1569
                                         function(data) {
1570
                        var cons_data = data.console;
1571
                        success.apply(this, [cons_data]);
1572
                    }, undefined, 'console', params)
1573
                    break;
1574
                case 'destroy':
1575
                    this.__make_api_call(this.url(), // vm actions url
1576
                                         "delete", // create so that sync later uses POST to make the call
1577
                                         undefined, // payload
1578
                                         function() {
1579
                                             // set state after successful call
1580
                                             self.state('DESTROY');
1581
                                             success.apply(this, arguments);
1582
                                             synnefo.api.trigger("quotas:call", 20);
1583
                                         },  
1584
                                         error, 'destroy', params);
1585
                    break;
1586
                case 'reassign':
1587
                    this.__make_api_call(this.get_action_url(), // vm actions url
1588
                                         "create", // create so that sync later uses POST to make the call
1589
                                         {reassign: {project:params.project_id}}, // payload
1590
                                         function() {
1591
                                             self.state('reassign');
1592
                                             self.set({'tenant_id': params.project_id});
1593
                                             success.apply(this, arguments);
1594
                                             snf.api.trigger("call");
1595
                                         },  
1596
                                         error, 'reassign', params);
1597
                    break;
1598
                case 'resize':
1599
                    this.__make_api_call(this.get_action_url(), // vm actions url
1600
                                         "create", // create so that sync later uses POST to make the call
1601
                                         {resize: {flavorRef:params.flavor}}, // payload
1602
                                         function() {
1603
                                             self.state('RESIZE');
1604
                                             success.apply(this, arguments);
1605
                                             snf.api.trigger("call");
1606
                                         },  
1607
                                         error, 'resize', params);
1608
                    break;
1609
                case 'destroy':
1610
                    this.__make_api_call(this.url(), // vm actions url
1611
                                         "delete", // create so that sync later uses POST to make the call
1612
                                         undefined, // payload
1613
                                         function() {
1614
                                             // set state after successful call
1615
                                             self.state('DESTROY');
1616
                                             success.apply(this, arguments);
1617
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1618

    
1619
                                         },  
1620
                                         error, 'destroy', params);
1621
                    break;
1622
                case 'firewallProfile':
1623
                    this.__make_api_call(this.get_action_url(), // vm actions url
1624
                                         "create",
1625
                                         {firewallProfile:{nic:params.nic, profile:params.profile}}, // payload
1626
                                         function() {
1627
                                             success.apply(this, arguments);
1628
                                             snf.api.trigger("call");
1629
                                         },  
1630
                                         error, 'start', params);
1631
                    break;
1632
                default:
1633
                    throw "Invalid VM action ("+action_name+")";
1634
            }
1635
        },
1636
        
1637
        __make_api_call: function(url, method, data, success, error, action, 
1638
                                  extra_params) {
1639
            var self = this;
1640
            error = error || function(){};
1641
            success = success || function(){};
1642

    
1643
            var params = {
1644
                url: url,
1645
                data: data,
1646
                success: function() { 
1647
                  self.handle_action_succeed.apply(self, arguments); 
1648
                  success.apply(this, arguments)
1649
                },
1650
                error: function() { 
1651
                  self.handle_action_fail.apply(self, arguments);
1652
                  error.apply(this, arguments)
1653
                },
1654
                error_params: { ns: "Machines actions", 
1655
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1656
                                extra_details: {
1657
                                  'Machine ID': this.id, 
1658
                                  'URL': url, 
1659
                                  'Action': action || "undefined" },
1660
                                allow_reload: false
1661
                              },
1662
                display: false,
1663
                critical: false
1664
            }
1665
            _.extend(params, extra_params);
1666
            this.sync(method, this, params);
1667
        },
1668

    
1669
        handle_action_succeed: function() {
1670
            this.trigger("action:success", arguments);
1671
        },
1672
        
1673
        reset_action_error: function() {
1674
            this.action_error = false;
1675
            this.trigger("action:fail:reset", this.action_error);
1676
        },
1677

    
1678
        handle_action_fail: function() {
1679
            this.action_error = arguments;
1680
            this.trigger("action:fail", arguments);
1681
        },
1682

    
1683
        get_action_url: function(name) {
1684
            return this.url() + "/action";
1685
        },
1686

    
1687
        get_diagnostics_url: function() {
1688
            return this.url() + "/diagnostics";
1689
        },
1690

    
1691
        get_users: function() {
1692
            var image;
1693
            var users = [];
1694
            try {
1695
              var users = this.get_meta('users').split(" ");
1696
            } catch (err) { users = null }
1697
            if (!users) {
1698
              image = this.get_image();
1699
              if (image) {
1700
                  users = image.get_created_users();
1701
              }
1702
            }
1703
            return users;
1704
        },
1705

    
1706
        get_connection_info: function(host_os, success, error) {
1707
            var url = synnefo.config.ui_connect_url;
1708
            var users = this.get_users();
1709

    
1710
            params = {
1711
                ip_address: this.get_hostname(),
1712
                hostname: this.get_hostname(),
1713
                os: this.get_os(),
1714
                host_os: host_os,
1715
                ports: JSON.stringify(this.get('SNF:port_forwarding') || {}),
1716
                srv: this.id
1717
            }
1718
            
1719
            if (users.length) { 
1720
                params['username'] = _.last(users)
1721
            }
1722

    
1723
            url = url + "?" + $.param(params);
1724

    
1725
            var ajax = snf.api.sync("read", undefined, { url: url, 
1726
                                                         error:error, 
1727
                                                         success:success, 
1728
                                                         handles_error:1});
1729
        }
1730
    });
1731
    
1732
    models.VM.ACTIONS = [
1733
        'start',
1734
        'shutdown',
1735
        'reboot',
1736
        'console',
1737
        'destroy',
1738
        'resize',
1739
        'reassign',
1740
        'snapshot'
1741
    ]
1742

    
1743
    models.VM.TASK_STATE_STATUS_MAP = {
1744
      'BUILDING': 'BUILD',
1745
      'REBOOTING': 'REBOOT',
1746
      'STOPPING': 'SHUTDOWN',
1747
      'STARTING': 'START',
1748
      'RESIZING': 'RESIZE',
1749
      'REASSIGNING': 'REASSIGN',
1750
      'CONNECTING': 'CONNECT',
1751
      'DISCONNECTING': 'DISCONNECT',
1752
      'DESTROYING': 'DESTROY'
1753
    }
1754

    
1755
    models.VM.AVAILABLE_ACTIONS = {
1756
        'UNKNWON'       : ['destroy'],
1757
        'BUILD'         : ['destroy'],
1758
        'REBOOT'        : ['destroy'],
1759
        'STOPPED'       : ['start', 'destroy', 'reassign', 'resize', 'snapshot'],
1760
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console', 'reassign', 'resize', 'snapshot'],
1761
        'ERROR'         : ['destroy'],
1762
        'DELETED'       : ['destroy'],
1763
        'DESTROY'       : ['destroy'],
1764
        'SHUTDOWN'      : ['destroy'],
1765
        'START'         : ['destroy'],
1766
        'CONNECT'       : ['destroy'],
1767
        'DISCONNECT'    : ['destroy'],
1768
        'RESIZE'        : ['destroy'],
1769
        'REASSIGN'      : ['destroy']
1770
    }
1771
    
1772
    models.VM.AVAILABLE_ACTIONS_INACTIVE = {}
1773

    
1774
    // api status values
1775
    models.VM.STATUSES = [
1776
        'UNKNWON',
1777
        'BUILD',
1778
        'REBOOT',
1779
        'STOPPED',
1780
        'ACTIVE',
1781
        'ERROR',
1782
        'DELETED',
1783
        'REASSIGN',
1784
        'RESIZE'
1785
    ]
1786

    
1787
    // api status values
1788
    models.VM.CONNECT_STATES = [
1789
        'ACTIVE',
1790
        'REBOOT',
1791
        'SHUTDOWN'
1792
    ]
1793

    
1794
    // vm states
1795
    models.VM.STATES = models.VM.STATUSES.concat([
1796
        'DESTROY',
1797
        'SHUTDOWN',
1798
        'START',
1799
        'CONNECT',
1800
        'DISCONNECT',
1801
        'FIREWALL',
1802
        'REASSIGN',
1803
        'RESIZE'
1804
    ]);
1805
    
1806
    models.VM.STATES_TRANSITIONS = {
1807
        'DESTROY' : ['DELETED'],
1808
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1809
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY', 'RESIZE', 'REASSIGN'],
1810
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY', 'REASSIGN'],
1811
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1812
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1813
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1814
        'RESIZE': ['ERROR', 'STOPPED'],
1815
        'REASSIGN': ['ERROR', 'STOPPED', 'ACTIVE']
1816
    }
1817

    
1818
    models.VM.TRANSITION_STATES = [
1819
        'DESTROY',
1820
        'SHUTDOWN',
1821
        'START',
1822
        'REBOOT',
1823
        'BUILD',
1824
        'RESIZE',
1825
        'REASSIGN',
1826
        'DISCONNECT',
1827
        'CONNECT'
1828
    ]
1829

    
1830
    models.VM.ACTIVE_STATES = [
1831
        'BUILD', 'REBOOT', 'ACTIVE',
1832
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1833
    ]
1834

    
1835
    models.VM.BUILDING_STATES = [
1836
        'BUILD'
1837
    ]
1838

    
1839
    models.Images = models.Collection.extend({
1840
        model: models.Image,
1841
        path: 'images',
1842
        details: true,
1843
        noUpdate: true,
1844
        supportIncUpdates: false,
1845
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1846
        meta_labels: {},
1847
        read_method: 'read',
1848

    
1849
        // update collection model with id passed
1850
        // making a direct call to the image
1851
        // api url
1852
        update_unknown_id: function(id, callback) {
1853
            var url = getUrl.call(this) + "/" + id;
1854
            this.api_call(this.path + "/" + id, this.read_method, {
1855
              _options:{
1856
                async:true, 
1857
                skip_api_error:true}
1858
              }, undefined, 
1859
            _.bind(function() {
1860
                if (!this.get(id)) {
1861
                            if (this.fallback_service) {
1862
                        // if current service has fallback_service attribute set
1863
                        // use this service to retrieve the missing image model
1864
                        var tmpservice = new this.fallback_service();
1865
                        tmpservice.update_unknown_id(id, _.bind(function(img){
1866
                            img.attributes.status = "DELETED";
1867
                            this.add(img.attributes);
1868
                            callback(this.get(id));
1869
                        }, this));
1870
                    } else {
1871
                        var title = synnefo.config.image_deleted_title || 'Deleted';
1872
                        // else add a dummy DELETED state image entry
1873
                        this.add({id:id, name:title, size:-1, 
1874
                                  progress:100, status:"DELETED"});
1875
                        callback(this.get(id));
1876
                    }   
1877
                } else {
1878
                    callback(this.get(id));
1879
                }
1880
            }, this), _.bind(function(image, msg, xhr) {
1881
                if (!image) {
1882
                    var title = synnefo.config.image_deleted_title || 'Deleted';
1883
                    this.add({id:id, name:title, size:-1, 
1884
                              progress:100, status:"DELETED"});
1885
                    callback(this.get(id));
1886
                    return;
1887
                }
1888
                var img_data = this._read_image_from_request(image, msg, xhr);
1889
                this.add(img_data);
1890
                callback(this.get(id));
1891
            }, this));
1892
        },
1893

    
1894
        _read_image_from_request: function(image, msg, xhr) {
1895
            return image.image;
1896
        },
1897

    
1898
        parse: function (resp, xhr) {
1899
            var parsed = _.map(resp.images, _.bind(this.parse_meta, this));
1900
            parsed = this.fill_owners(parsed);
1901
            return parsed;
1902
        },
1903

    
1904
        fill_owners: function(images) {
1905
            // do translate uuid->displayname if needed
1906
            // store display name in owner attribute for compatibility
1907
            var uuids = [];
1908

    
1909
            var images = _.map(images, function(img, index) {
1910
                if (synnefo.config.translate_uuids) {
1911
                    uuids.push(img['owner']);
1912
                }
1913
                img['owner_uuid'] = img['owner'];
1914
                return img;
1915
            });
1916
            
1917
            if (uuids.length > 0) {
1918
                var handle_results = function(data) {
1919
                    _.each(images, function (img) {
1920
                        img['owner'] = data.uuid_catalog[img['owner_uuid']];
1921
                    });
1922
                }
1923
                // notice the async false
1924
                var uuid_map = this.translate_uuids(uuids, false, 
1925
                                                    handle_results)
1926
            }
1927
            return images;
1928
        },
1929

    
1930
        translate_uuids: function(uuids, async, cb) {
1931
            var url = synnefo.config.user_catalog_url;
1932
            var data = JSON.stringify({'uuids': uuids});
1933
          
1934
            // post to user_catalogs api
1935
            snf.api.sync('create', undefined, {
1936
                url: url,
1937
                data: data,
1938
                async: async,
1939
                success:  cb
1940
            });
1941
        },
1942

    
1943
        get_meta_key: function(img, key) {
1944
            if (img.metadata && img.metadata && img.metadata[key]) {
1945
                return _.escape(img.metadata[key]);
1946
            }
1947
            return undefined;
1948
        },
1949

    
1950
        comparator: function(img) {
1951
            return -img.get_sort_order("sortorder") || 0;
1952
        },
1953

    
1954
        parse_meta: function(img) {
1955
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1956
                if (img[key]) { return };
1957
                img[key] = this.get_meta_key(img, key) || "";
1958
            }, this));
1959
            return img;
1960
        },
1961

    
1962
        active: function() {
1963
            return this.filter(function(img){return img.get('status') != "DELETED"});
1964
        },
1965

    
1966
        predefined: function() {
1967
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
1968
        },
1969
        
1970
        fetch_for_type: function(type, complete, error) {
1971
            this.fetch({update:true, 
1972
                        success: complete, 
1973
                        error: error, 
1974
                        skip_api_error: true });
1975
        },
1976
        
1977
        get_images_for_type: function(type) {
1978
            if (this['get_{0}_images'.format(type)]) {
1979
                return this['get_{0}_images'.format(type)]();
1980
            }
1981

    
1982
            return this.active();
1983
        },
1984

    
1985
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
1986
            var load = false;
1987
            error = onError || function() {};
1988
            function complete(collection) { 
1989
                onComplete(collection.get_images_for_type(type)); 
1990
            }
1991
            
1992
            // do we need to fetch/update current collection entries
1993
            if (load) {
1994
                onStart();
1995
                this.fetch_for_type(type, complete, error);
1996
            } else {
1997
                // fallback to complete
1998
                complete(this);
1999
            }
2000
        }
2001
    })
2002

    
2003
    models.Flavors = models.Collection.extend({
2004
        model: models.Flavor,
2005
        path: 'flavors',
2006
        details: true,
2007
        noUpdate: true,
2008
        supportIncUpdates: false,
2009
        // update collection model with id passed
2010
        // making a direct call to the flavor
2011
        // api url
2012
        update_unknown_id: function(id, callback) {
2013
            var url = getUrl.call(this) + "/" + id;
2014
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
2015
            _.bind(function() {
2016
                this.add({id:id, cpu:"Unknown", ram:"Unknown", disk:"Unknown", name: "Unknown", status:"DELETED"})
2017
            }, this), _.bind(function(flv) {
2018
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
2019
                this.add(flv.flavor);
2020
            }, this));
2021
        },
2022

    
2023
        parse: function (resp, xhr) {
2024
            return _.map(resp.flavors, function(o) {
2025
              o.cpu = o['vcpus'];
2026
              o.disk_template = o['SNF:disk_template'];
2027
              return o
2028
            });
2029
        },
2030

    
2031
        comparator: function(flv) {
2032
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
2033
        },
2034
          
2035
        unavailable_values_for_quotas: function(quotas, flavors, extra) {
2036
            var flavors = flavors || this.active();
2037
            var index = {cpu:[], disk:[], ram:[]};
2038
            var extra = extra == undefined ? {cpu:0, disk:0, ram:0} : extra;
2039
            
2040
            _.each(flavors, function(el) {
2041

    
2042
                var disk_available = quotas['disk'] + extra.disk;
2043
                var disk_size = el.get_disk_size();
2044
                if (index.disk.indexOf(disk_size) == -1) {
2045
                  var disk = el.disk_to_bytes();
2046
                  if (disk > disk_available) {
2047
                    index.disk.push(el.get('disk'));
2048
                  }
2049
                }
2050
                
2051
                var ram_available = quotas['ram'] + extra.ram * 1024 * 1024;
2052
                var ram_size = el.get_ram_size();
2053
                if (index.ram.indexOf(ram_size) == -1) {
2054
                  var ram = el.ram_to_bytes();
2055
                  if (ram > ram_available) {
2056
                    index.ram.push(el.get('ram'))
2057
                  }
2058
                }
2059

    
2060
                var cpu = el.get('cpu');
2061
                var cpu_available = quotas['cpu'] + extra.cpu;
2062
                if (index.cpu.indexOf(cpu) == -1) {
2063
                  if (cpu > cpu_available) {
2064
                    index.cpu.push(el.get('cpu'))
2065
                  }
2066
                }
2067
            });
2068
            return index;
2069
        },
2070

    
2071
        unavailable_values_for_image: function(img, flavors) {
2072
            var flavors = flavors || this.active();
2073
            var size = img.get_size();
2074
            
2075
            var index = {cpu:[], disk:[], ram:[]};
2076

    
2077
            _.each(this.active(), function(el) {
2078
                var img_size = size;
2079
                var flv_size = el.get_disk_size();
2080
                if (flv_size < img_size) {
2081
                    if (index.disk.indexOf(el.get("disk")) == -1) {
2082
                        index.disk.push(el.get("disk"));
2083
                    }
2084
                };
2085
            });
2086
            
2087
            return index;
2088
        },
2089

    
2090
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
2091
            if (!filter_list) { filter_list = this.models };
2092
            
2093
            return this.select(function(flv){
2094
                if (flv.get("cpu") == cpu + "" &&
2095
                   flv.get("ram") == mem + "" &&
2096
                   flv.get("disk") == disk + "" &&
2097
                   flv.get("disk_template") == disk_template &&
2098
                   filter_list.indexOf(flv) > -1) { return true; }
2099
            })[0];
2100
        },
2101
        
2102
        get_data: function(lst) {
2103
            var data = {'cpu': [], 'mem':[], 'disk':[], 'disk_template':[]};
2104

    
2105
            _.each(lst, function(flv) {
2106
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
2107
                    data.cpu.push(flv.get("cpu"));
2108
                }
2109
                if (data.mem.indexOf(flv.get("ram")) == -1) {
2110
                    data.mem.push(flv.get("ram"));
2111
                }
2112
                if (data.disk.indexOf(flv.get("disk")) == -1) {
2113
                    data.disk.push(flv.get("disk"));
2114
                }
2115
                if (data.disk_template.indexOf(flv.get("disk_template")) == -1) {
2116
                    data.disk_template.push(flv.get("disk_template"));
2117
                }
2118
            })
2119
            
2120
            return data;
2121
        },
2122

    
2123
        active: function() {
2124
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
2125
        }
2126
            
2127
    })
2128

    
2129
    models.VMS = models.Collection.extend({
2130
        model: models.VM,
2131
        path: 'servers',
2132
        details: true,
2133
        copy_image_meta: true,
2134

    
2135
        parse: function (resp, xhr) {
2136
            var data = resp;
2137
            if (!resp) { return [] };
2138
            data = _.filter(_.map(resp.servers, 
2139
                                  _.bind(this.parse_vm_api_data, this)), 
2140
                                  function(v){return v});
2141
            return data;
2142
        },
2143

    
2144
        parse_vm_api_data: function(data) {
2145
            var status;
2146
            // do not add non existing DELETED entries
2147
            if (data.status && data.status == "DELETED") {
2148
                if (!this.get(data.id)) {
2149
                    return false;
2150
                }
2151
            }
2152
            
2153
            if ('SNF:task_state' in data) { 
2154
                data['task_state'] = data['SNF:task_state'];
2155
                // Update machine state based on task_state value
2156
                // Do not apply task_state logic when machine is in ERROR state.
2157
                // In that case only update from task_state only if equals to
2158
                // DESTROY
2159
                if (data['task_state']) {
2160
                    if (data['status'] != 'ERROR' && data['task_state'] != 'DESTROY') {
2161
                      status = models.VM.TASK_STATE_STATUS_MAP[data['task_state']];
2162
                      if (status) { data['status'] = status }
2163
                    }
2164
                }
2165
            }
2166

    
2167
            // OS attribute
2168
            if (this.has_meta(data)) {
2169
                data['OS'] = data.metadata.OS || snf.config.unknown_os;
2170
            }
2171
            
2172
            if (!data.diagnostics) {
2173
                data.diagnostics = [];
2174
            }
2175

    
2176
            // network metadata
2177
            data['firewalls'] = {};
2178
            data['fqdn'] = data['SNF:fqdn'];
2179

    
2180
            // if vm has no metadata, no metadata object
2181
            // is in json response, reset it to force
2182
            // value update
2183
            if (!data['metadata']) {
2184
                data['metadata'] = {};
2185
            }
2186
            
2187
            // v2.0 API returns objects
2188
            data.image_obj = data.image;
2189
            data.image = data.image_obj.id;
2190
            data.flavor_obj = data.flavor;
2191
            data.flavor = data.flavor_obj.id;
2192

    
2193
            return data;
2194
        },
2195

    
2196
        get_reboot_required: function() {
2197
            return this.filter(function(vm){return vm.get("reboot_required") == true})
2198
        },
2199

    
2200
        has_pending_actions: function() {
2201
            return this.filter(function(vm){return vm.pending_action}).length > 0;
2202
        },
2203

    
2204
        reset_pending_actions: function() {
2205
            this.each(function(vm) {
2206
                vm.clear_pending_action();
2207
            })
2208
        },
2209

    
2210
        do_all_pending_actions: function(success, error) {
2211
            this.each(function(vm) {
2212
                if (vm.has_pending_action()) {
2213
                    vm.call(vm.pending_action, success, error);
2214
                    vm.clear_pending_action();
2215
                }
2216
            })
2217
        },
2218
        
2219
        do_all_reboots: function(success, error) {
2220
            this.each(function(vm) {
2221
                if (vm.get("reboot_required")) {
2222
                    vm.call("reboot", success, error);
2223
                }
2224
            });
2225
        },
2226

    
2227
        reset_reboot_required: function() {
2228
            this.each(function(vm) {
2229
                vm.set({'reboot_required': undefined});
2230
            })
2231
        },
2232
        
2233
        stop_stats_update: function(exclude) {
2234
            var exclude = exclude || [];
2235
            this.each(function(vm) {
2236
                if (exclude.indexOf(vm) > -1) {
2237
                    return;
2238
                }
2239
                vm.stop_stats_update();
2240
            })
2241
        },
2242
        
2243
        has_meta: function(vm_data) {
2244
            return vm_data.metadata && vm_data.metadata
2245
        },
2246

    
2247
        has_addresses: function(vm_data) {
2248
            return vm_data.metadata && vm_data.metadata
2249
        },
2250

    
2251
        create: function (name, image, flavor, meta, project, extra, callback) {
2252

    
2253
            if (this.copy_image_meta) {
2254
                if (synnefo.config.vm_image_common_metadata) {
2255
                    _.each(synnefo.config.vm_image_common_metadata, 
2256
                        function(key){
2257
                            if (image.get_meta(key)) {
2258
                                meta[key] = image.get_meta(key);
2259
                            }
2260
                    });
2261
                }
2262

    
2263
                if (image.get("OS")) {
2264
                    meta['OS'] = image.get("OS");
2265
                }
2266
            }
2267
            
2268
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, 
2269
                    metadata:meta, project: project.id}
2270
            opts = _.extend(opts, extra);
2271
            
2272
            var cb = function(data) {
2273
              synnefo.api.trigger("quota:update");
2274
              callback(data);
2275
            }
2276

    
2277
            this.api_call(this.path, "create", {'server': opts}, undefined, 
2278
                          undefined, cb, {critical: true});
2279
        },
2280

    
2281
        load_missing_images: function(callback) {
2282
          var missing_ids = [];
2283
          var resolved = 0;
2284

    
2285
          // fill missing_ids
2286
          this.each(function(el) {
2287
            var imgid = el.get("image");
2288
            var existing = synnefo.storage.images.get(imgid);
2289
            if (!existing && missing_ids.indexOf(imgid) == -1) {
2290
              missing_ids.push(imgid);
2291
            }
2292
          });
2293
          var check = function() {
2294
            // once all missing ids where resolved continue calling the 
2295
            // callback
2296
            resolved++;
2297
            if (resolved == missing_ids.length) {
2298
              callback(missing_ids)
2299
            }
2300
          }
2301
          if (missing_ids.length == 0) {
2302
            callback(missing_ids);
2303
            return;
2304
          }
2305
          // start resolving missing image ids
2306
          _(missing_ids).each(function(imgid){
2307
            synnefo.storage.images.update_unknown_id(imgid, check);
2308
          });
2309
        },
2310

    
2311
        get_connectable: function() {
2312
            return storage.vms.filter(function(vm){
2313
                return !vm.in_error_state() && !vm.is_building();
2314
            });
2315
        }
2316
    })
2317
    
2318
    models.PublicKey = models.Model.extend({
2319
        path: 'keys',
2320
        api_type: 'userdata',
2321
        detail: false,
2322
        model_actions: {
2323
          'remove': [['name'], function() {
2324
            return true;
2325
          }]
2326
        },
2327

    
2328
        get_public_key: function() {
2329
            return cryptico.publicKeyFromString(this.get("content"));
2330
        },
2331

    
2332
        get_filename: function() {
2333
            return "{0}.pub".format(this.get("name"));
2334
        },
2335

    
2336
        identify_type: function() {
2337
            try {
2338
                var cont = snf.util.validatePublicKey(this.get("content"));
2339
                var type = cont.split(" ")[0];
2340
                return synnefo.util.publicKeyTypesMap[type];
2341
            } catch (err) { return false };
2342
        },
2343

    
2344
        rename: function(new_name) {
2345
          //this.set({'name': new_name});
2346
          var self = this;
2347
          this.sync("update", this, {
2348
            critical: true,
2349
            data: {'name': new_name}, 
2350
            success: _.bind(function(){
2351
              snf.api.trigger("call");
2352
              this.set({'name': new_name});
2353
            }, this)
2354
          });
2355
        },
2356

    
2357
        do_remove: function() {
2358
          this.actions.reset_pending();
2359
          this.remove(function() {
2360
            synnefo.storage.keys.fetch();
2361
          });
2362
        }
2363
    })
2364
    
2365
    models._ActionsModel = models.Model.extend({
2366
      defaults: { pending: null },
2367
      actions: [],
2368
      status: {
2369
        INACTIVE: 0,
2370
        PENDING: 1,
2371
        CALLED: 2
2372
      },
2373

    
2374
      initialize: function(attrs, opts) {
2375
        models._ActionsModel.__super__.initialize.call(this, attrs);
2376
        this.actions = opts.actions;
2377
        this.model = opts.model;
2378
        this.bind("change", function() {
2379
          this.set({'pending': this.get_pending()});
2380
        }, this);
2381
        this.clear();
2382
      },
2383
      
2384
      _in_status: function(st) {
2385
        var actions = null;
2386
        _.each(this.attributes, function(status, action){
2387
          if (status == st) {
2388
            if (!actions) {
2389
              actions = []
2390
            }
2391
            actions.push(action);
2392
          }
2393
        });
2394
        return actions;
2395
      },
2396

    
2397
      get_pending: function() {
2398
        return this._in_status(this.status.PENDING);
2399
      },
2400

    
2401
      unset_pending_action: function(action) {
2402
        var data = {};
2403
        data[action] = this.status.INACTIVE;
2404
        this.set(data);
2405
        this.trigger("unset-pending", action);
2406
      },
2407

    
2408
      set_pending_action: function(action, reset_pending) {
2409
        reset_pending = reset_pending === undefined ? true : reset_pending;
2410
        var data = {};
2411
        data[action] = this.status.PENDING;
2412
        if (reset_pending) {
2413
          this.reset_pending();
2414
        }
2415
        this.set(data);
2416
        this.trigger("set-pending", action);
2417
      },
2418
      
2419
      reset_pending: function() {
2420
        var data = {};
2421
        _.each(this.actions, function(action) {
2422
          data[action] = this.status.INACTIVE;
2423
        }, this);
2424
        this.set(data);
2425
        this.trigger("reset-pending");
2426
      }
2427
    });
2428

    
2429
    models.PublicPool = models.Model.extend({});
2430
    models.PublicPools = models.Collection.extend({
2431
      model: models.PublicPool,
2432
      path: 'os-floating-ip-pools',
2433
      api_type: 'compute',
2434
      noUpdate: true,
2435

    
2436
      parse: function(data) {
2437
        return _.map(data.floating_ip_pools, function(pool) {
2438
          pool.id = pool.name;
2439
          return pool;
2440
        });
2441
      }
2442
    });
2443

    
2444
    models.PublicKeys = models.Collection.extend({
2445
        model: models.PublicKey,
2446
        details: false,
2447
        path: 'keys',
2448
        api_type: 'userdata',
2449
        noUpdate: true,
2450
        updateEntries: true,
2451

    
2452
        generate_new: function(success, error) {
2453
            snf.api.sync('create', undefined, {
2454
                url: getUrl.call(this, this.base_url) + "/generate", 
2455
                success: success, 
2456
                error: error,
2457
                skip_api_error: true
2458
            });
2459
        },
2460
        
2461
        add_crypto_key: function(key, success, error, options) {
2462
            var options = options || {};
2463
            var m = new models.PublicKey();
2464

    
2465
            // guess a name
2466
            var name_tpl = "my generated public key";
2467
            var name = name_tpl;
2468
            var name_count = 1;
2469
            
2470
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
2471
                name = name_tpl + " " + name_count;
2472
                name_count++;
2473
            }
2474
            
2475
            m.set({name: name});
2476
            m.set({content: key});
2477
            
2478
            options.success = function () { return success(m) };
2479
            options.errror = error;
2480
            options.skip_api_error = true;
2481
            
2482
            this.create(m.attributes, options);
2483
        }
2484
    });
2485

    
2486
  
2487
    models.Quota = models.Model.extend({
2488
        storage_attrs: {
2489
          'project_id': ['projects', 'project']
2490
        },
2491

    
2492
        initialize: function() {
2493
            models.Quota.__super__.initialize.apply(this, arguments);
2494
            this.bind("change", this.check, this);
2495
            this.check();
2496
        },
2497
        
2498
        check: function() {
2499
            var usage, limit;
2500
            usage = this.get('usage');
2501
            limit = this.get('limit');
2502
            if (usage >= limit) {
2503
                this.trigger("available");
2504
            } else {
2505
                this.trigger("unavailable");
2506
            }
2507
        },
2508

    
2509
        increase: function(val) {
2510
            if (val === undefined) { val = 1};
2511
            this.set({'usage': this.get('usage') + val})
2512
        },
2513

    
2514
        decrease: function(val) {
2515
            if (val === undefined) { val = 1};
2516
            this.set({'usage': this.get('usage') - val})
2517
        },
2518

    
2519
        can_consume: function() {
2520
            var usage, limit;
2521
            usage = this.get('usage');
2522
            limit = this.get('limit');
2523
            if (usage >= limit) {
2524
                return false
2525
            } else {
2526
                return true
2527
            }
2528
        },
2529
        
2530
        is_bytes: function() {
2531
            return this.get('resource').get('unit') == 'bytes';
2532
        },
2533
        
2534
        get_available: function(active) {
2535
            suffix = '';
2536
            if (active) { suffix = '_active'}
2537
            var value = this.get('limit'+suffix) - this.get('usage'+suffix);
2538
            if (active) {
2539
              if (this.get('available') <= value) {
2540
                value = this.get('available');
2541
              }
2542
            }
2543
            if (value < 0) { return value }
2544
            return value
2545
        },
2546

    
2547
        get_readable: function(key, active) {
2548
            var value;
2549
            if (key == 'available') {
2550
                value = this.get_available(active);
2551
            } else {
2552
                value = this.get(key)
2553
            }
2554
            if (value <= 0) { value = 0 }
2555
            // greater than max js int (assume infinite quota)
2556
            if (value > Math.pow(2, 53)) { 
2557
              return "Infinite"
2558
            }
2559

    
2560
            if (!this.is_bytes()) {
2561
              return value + "";
2562
            }
2563
            
2564
            return snf.util.readablizeBytes(value);
2565
        }
2566
    });
2567
    
2568
    models.Quotas = models.Collection.extend({
2569
        model: models.Quota,
2570
        api_type: 'accounts',
2571
        path: 'quotas',
2572
        supportIncUpdates: false,
2573

    
2574
        required_quota: {
2575
          'vm': {
2576
            'cyclades.vm': 1,
2577
            'cyclades.ram': 1,
2578
            'cyclades.cpu': 1,
2579
            'cyclades.disk': 1
2580
          },
2581
          'network': {
2582
            'cyclades.network.private': 1
2583
          },
2584
          'ip': {
2585
            'cyclades.floating_ip': 1
2586
          }
2587
        },
2588

    
2589
        parse: function(resp) {
2590
            var parsed = [];
2591
            _.each(resp, function(resources, uuid) {
2592
              parsed = _.union(parsed, _.map(resources, function(value, key) {
2593
                var quota_available = value.limit - value.usage || 0;
2594
                var total_available = quota_available;
2595
                var project_available = value.project_limit - value.project_usage || 0;
2596
                var limit = value.limit;
2597
                var usage = value.usage;
2598
                
2599
                var available = quota_available;
2600
                var total_available = available;
2601

    
2602
                // priority to project limits
2603
                if (project_available < available ) {
2604
                  available = project_available;
2605
                  limit = value.project_limit;
2606
                  usage = value.project_usage;
2607
                  total_available = available;
2608
                }
2609

    
2610
                var available_active = available;
2611

    
2612
                // corresponding total quota
2613
                var keysplit = key.split(".");
2614
                var last_part = keysplit.pop();
2615
                var activekey = keysplit.join(".") + "." + "total_" + last_part;
2616
                var total = resp[uuid][activekey];
2617
                if (total) {
2618
                  total_available = total.limit - total.usage;
2619
                  var total_project_available = total.project_limit - total.project_usage;
2620
                  var total_limit = total.limit;
2621
                  var total_usage = total.usage;
2622
                  if (total_project_available < total_available) {
2623
                    total_available = total_project_available;
2624
                    total_limit = total.project_limit;
2625
                    total_usage = total.project_usage;
2626
                  }
2627
                  if (total_available < available) {
2628
                    available = total_available;
2629
                    limit = total_limit;
2630
                    usage = total_usage;
2631
                  }
2632
                }
2633

    
2634
                var limit_active = limit;
2635
                var usage_active = usage;
2636

    
2637
                var id = uuid + ":" + key;
2638
                return _.extend(value, {
2639
                          'name': key, 
2640
                          'id': id, 
2641
                          'available': available,
2642
                          'available_active': available_active,
2643
                          'total_available': total_available,
2644
                          'limit_active': limit_active,
2645
                          'project_id': uuid,
2646
                          'usage_active': usage_active,
2647
                          'resource': snf.storage.resources.get(key)
2648
                });
2649
              }));
2650
            });
2651
            return parsed;
2652
        },
2653
        
2654
        project_key: function(project, key) {
2655
          return project + ":" + key;
2656
        },
2657

    
2658
        get_by_id: function(k) {
2659
          return this.filter(function(q) { return q.get('name') == k})[0]
2660
        },
2661

    
2662
        get_available_for_vm: function(options) {
2663
          var quotas = this;
2664
          var key = 'available';
2665
          var available_quota = {};
2666
          _.each(['cyclades.ram', 'cyclades.cpu', 'cyclades.disk'], 
2667
            function (key) {
2668
              var value = quotas.get(key).get_available(true);
2669
              available_quota[key.replace('cyclades.', '')] = value;
2670
          });
2671
          return available_quota;
2672
        },
2673
        
2674
        can_create: function(type) {
2675
          return this.get_available_projects(this.required_quota[type]).length > 0;
2676
        },
2677

    
2678
        get_available_projects: function(quotas) {
2679
          return synnefo.storage.projects.filter(function(project) {
2680
            return project.quotas.can_fit(quotas);
2681
          });
2682
        },
2683

    
2684
        can_fit: function(quotas, total, _issues) {
2685
          var issues = [];
2686
          if (total === undefined) { total = false }
2687
          _.each(quotas, function(value, key) {
2688
            var q = this.get(key);
2689
            if (!q) { issues.push(key); return }
2690
            var quota = q.get('available_active');
2691
            if (total) {
2692
              quota = q.get('available');
2693
            }
2694
            if (quota < value) {
2695
              issues.push(key);
2696
            }
2697
          }, this);
2698
          if (_issues) { return issues }
2699
          return issues.length === 0;
2700
        }
2701
    })
2702

    
2703
    models.Resource = models.Model.extend({
2704
        api_type: 'accounts',
2705
        path: 'resources'
2706
    });
2707

    
2708
    models.Resources = models.Collection.extend({
2709
        api_type: 'accounts',
2710
        path: 'resources',
2711
        model: models.Network,
2712
        display_name_map: {
2713
          'cyclades.vm': 'Machines',
2714
          'cyclades.ram': 'Memory size',
2715
          'cyclades.total_ram': 'Memory size (total)',
2716
          'cyclades.cpu': 'CPUs',
2717
          'cyclades.total_cpu': 'CPUs (total)',
2718
          'cyclades.floating_ip': 'IP Addresses',
2719
          'pithos.diskpace': 'Storage space',
2720
          'cyclades.disk': 'Disk size',
2721
          'cyclades.network.private': 'Private networks'
2722
        },
2723

    
2724
        parse: function(resp) {
2725
            return _.map(resp, function(value, key) {
2726
                var display_name = this.display_name_map[key] || key;
2727
                return _.extend(value, {
2728
                  'name': key, 
2729
                  'id': key,
2730
                  'display_name': display_name
2731
                });
2732
            }, this);
2733
        }
2734
    });
2735
    
2736
    models.ProjectQuotas = models.Quotas.extend({})
2737
    _.extend(models.ProjectQuotas.prototype, Backbone.FilteredCollection.prototype);
2738
    models.ProjectQuotas.prototype.get = function(key) {
2739
      key = this.project_id + ":" + key;
2740
      return models.ProjectQuotas.__super__.get.call(this, key);
2741
    }
2742

    
2743
    models.Project = models.Model.extend({
2744
        api_type: 'accounts',
2745
        path: 'projects',
2746

    
2747
        initialize: function() {
2748
          var self = this;
2749
          this.quotas = new models.ProjectQuotas(undefined, {
2750
            collection: synnefo.storage.quotas,
2751
            collectionFilter: function(m) {
2752
              return self.id == m.get('project_id')
2753
          }});
2754
          this.quotas.bind('change', function() {
2755
            self.trigger('change:_quotas');
2756
          });
2757
          this.quotas.project_id = this.id;
2758
          models.Project.__super__.initialize.apply(this, arguments);
2759
        }
2760
    });
2761

    
2762
    models.Projects = models.Collection.extend({
2763
        api_type: 'accounts',
2764
        path: 'projects',
2765
        model: models.Project,
2766
        supportIncUpdates: false,
2767
        user_project_uuid: null,
2768
        
2769
        url: function() {
2770
          var args = Array.prototype.splice.call(arguments, 0);
2771
          var url = models.Projects.__super__.url.apply(this, args);
2772
          return url + "?mode=member";
2773
        },
2774

    
2775
        parse: function(resp) {
2776
          _.each(resp, function(project){
2777
            if (project.base_project) {
2778
              this.user_project_uuid = project.id;
2779
            }
2780
            if (project.id == synnefo.user.get_username()) {
2781
              project.name = "User project"
2782
            }
2783
          }, this);
2784
          return resp;
2785
        },
2786

    
2787
        get_user_project: function() {
2788
          return this.get(synnefo.user.current_username);
2789
        },
2790

    
2791
        comparator: function(project) {
2792
            if (project.get('base_project')) { return -100 }
2793
            return project.get('name');
2794
        }
2795
    });
2796
    
2797
    // storage initialization
2798
    snf.storage.images = new models.Images();
2799
    snf.storage.flavors = new models.Flavors();
2800
    snf.storage.vms = new models.VMS();
2801
    snf.storage.keys = new models.PublicKeys();
2802
    snf.storage.resources = new models.Resources();
2803
    snf.storage.quotas = new models.Quotas();
2804
    snf.storage.projects = new models.Projects();
2805
    snf.storage.public_pools = new models.PublicPools();
2806

    
2807
})(this);