Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (87.2 kB)

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

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

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

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

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

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

    
75

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

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

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

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

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

    
105
        },
106
        
107
        // Initialize model actions object
108
        // For each entry in model's model_action object register the relevant 
109
        // model proxy `can_<actionname>` attributes.
110
        init_model_actions: function() {
111
          var actions = _.keys(this.model_actions);
112
          this.set({
113
            "actions": new models._ActionsModel({}, {
114
              actions: actions,
115
              model: this
116
            })
117
          });
118
          this.actions = this.get("actions");
119
          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
        initialize: function() {
339
            models.Collection.__super__.initialize.apply(this, arguments);
340
            this.api_call = _.bind(this.api.call, this);
341
            if (this.sortFields) {
342
              _.each(this.sortFields, function(f) {
343
                this.bind("change:" + f, _.bind(this.resort, this));
344
              }, this);
345
            }
346
        },
347
          
348
        resort: function() {
349
          this.sort();
350
        },
351

    
352
        url: function(options, method) {
353
            return getUrl.call(this, this.base_url) + (
354
                    options.details || this.details && method != 'create' ? '/detail' : '');
355
        },
356

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

    
385
        create: function(model, options) {
386
            var coll = this;
387
            options || (options = {});
388
            model = this._prepareModel(model, options);
389
            if (!model) return false;
390
            var success = options.success;
391
            options.success = function(nextModel, resp, xhr) {
392
                if (coll.add_on_create) {
393
                  coll.add(nextModel, options);
394
                }
395
                if (success) success(nextModel, resp, xhr);
396
            };
397
            model.save(null, options);
398
            return model;
399
        },
400

    
401
        get_fetcher: function(interval, increase, fast, increase_after_calls, 
402
                              max, initial_call, params, fetcher_id) {
403
            var fetch_params = params || {};
404
            var handler_options = {};
405

    
406
            fetch_params.skips_timeouts = true;
407
            handler_options.interval = interval;
408
            handler_options.increase = increase;
409
            handler_options.fast = fast;
410
            handler_options.increase_after_calls = increase_after_calls;
411
            handler_options.max= max;
412
            handler_options.id = fetcher_id || "collection";
413

    
414
            var last_ajax = undefined;
415
            var callback = _.bind(function() {
416
                // clone to avoid referenced objects
417
                var params = _.clone(fetch_params);
418
                updater._ajax = last_ajax;
419
                
420
                // wait for previous request to finish
421
                if (last_ajax && last_ajax.readyState < 4 && last_ajax.statusText != "timeout") {
422
                    // opera readystate for 304 responses is 0
423
                    if (!($.browser.opera && last_ajax.readyState == 0 && last_ajax.status == 304)) {
424
                        return;
425
                    }
426
                }
427
                last_ajax = this.fetch(params);
428
            }, this);
429
            handler_options.callback = callback;
430

    
431
            var updater = new snf.api.updateHandler(_.clone(_.extend(handler_options, fetch_params)));
432
            snf.api.bind("call", _.throttle(_.bind(function(){ updater.faster(true)}, this)), 1000);
433
            return updater;
434
        }
435
    });
436
    
437
    // Image model
438
    models.Image = models.Model.extend({
439
        path: 'images',
440
        
441
        get_size: function() {
442
            return parseInt(this.get('metadata') ? this.get('metadata').size : -1)
443
        },
444

    
445
        get_description: function(escape) {
446
            if (escape == undefined) { escape = true };
447
            if (escape) { return this.escape('description') || "No description available"}
448
            return this.get('description') || "No description available."
449
        },
450

    
451
        get_meta: function(key) {
452
            if (this.get('metadata') && this.get('metadata')) {
453
                if (!this.get('metadata')[key]) { return null }
454
                return _.escape(this.get('metadata')[key]);
455
            } else {
456
                return null;
457
            }
458
        },
459

    
460
        get_meta_keys: function() {
461
            if (this.get('metadata') && this.get('metadata')) {
462
                return _.keys(this.get('metadata'));
463
            } else {
464
                return [];
465
            }
466
        },
467

    
468
        get_owner: function() {
469
            return this.get('owner') || _.keys(synnefo.config.system_images_owners)[0];
470
        },
471

    
472
        get_owner_uuid: function() {
473
            return this.get('owner_uuid');
474
        },
475

    
476
        is_system_image: function() {
477
          var owner = this.get_owner();
478
          return _.include(_.keys(synnefo.config.system_images_owners), owner)
479
        },
480

    
481
        owned_by: function(user) {
482
          if (!user) { user = synnefo.user }
483
          return user.get_username() == this.get('owner_uuid');
484
        },
485

    
486
        display_owner: function() {
487
            var owner = this.get_owner();
488
            if (_.include(_.keys(synnefo.config.system_images_owners), owner)) {
489
                return synnefo.config.system_images_owners[owner];
490
            } else {
491
                return owner;
492
            }
493
        },
494
    
495
        get_readable_size: function() {
496
            if (this.is_deleted()) {
497
                return synnefo.config.image_deleted_size_title || '(none)';
498
            }
499
            return this.get_size() > 0 ? util.readablizeBytes(this.get_size() * 1024 * 1024) : '(none)';
500
        },
501

    
502
        get_os: function() {
503
            return this.get_meta('OS');
504
        },
505

    
506
        get_gui: function() {
507
            return this.get_meta('GUI');
508
        },
509

    
510
        get_created_users: function() {
511
            try {
512
              var users = this.get_meta('users').split(" ");
513
            } catch (err) { users = null }
514
            if (!users) {
515
                var osfamily = this.get_meta('osfamily');
516
                if (osfamily == 'windows') { 
517
                  users = ['Administrator'];
518
                } else {
519
                  users = ['root'];
520
                }
521
            }
522
            return users;
523
        },
524

    
525
        get_sort_order: function() {
526
            return parseInt(this.get('metadata') ? this.get('metadata').sortorder : -1)
527
        },
528

    
529
        get_vm: function() {
530
            var vm_id = this.get("serverRef");
531
            var vm = undefined;
532
            vm = storage.vms.get(vm_id);
533
            return vm;
534
        },
535

    
536
        is_public: function() {
537
            return this.get('is_public') == undefined ? true : this.get('is_public');
538
        },
539

    
540
        is_deleted: function() {
541
            return this.get('status') == "DELETED"
542
        },
543
        
544
        ssh_keys_paths: function() {
545
            return _.map(this.get_created_users(), function(username) {
546
                prepend = '';
547
                if (username != 'root') {
548
                    prepend = '/home'
549
                }
550
                return {'user': username, 'path': '{1}/{0}/.ssh/authorized_keys'.format(username, 
551
                                                             prepend)};
552
            });
553
        },
554

    
555
        _supports_ssh: function() {
556
            var exclude_list = synnefo.config.ssh_support_osfamily_exclude_list || [];
557
            var os = this.get_os();
558
            if (exclude_list.indexOf(os) > -1) {
559
                return false;
560
            }
561
            return true;
562
        },
563

    
564
        supports: function(feature) {
565
            if (feature == "ssh") {
566
                return this._supports_ssh()
567
            }
568
            return false;
569
        },
570

    
571
        personality_data_for_keys: function(keys) {
572
            return _.map(this.ssh_keys_paths(), function(pathinfo) {
573
                var contents = '';
574
                _.each(keys, function(key){
575
                    contents = contents + key.get("content") + "\n"
576
                });
577
                contents = $.base64.encode(contents);
578

    
579
                return {
580
                    path: pathinfo.path,
581
                    contents: contents,
582
                    mode: 0600,
583
                    owner: pathinfo.user
584
                }
585
            });
586
        }
587
    });
588

    
589
    // Flavor model
590
    models.Flavor = models.Model.extend({
591
        path: 'flavors',
592

    
593
        details_string: function() {
594
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
595
        },
596

    
597
        get_disk_size: function() {
598
            return parseInt(this.get("disk") * 1024)
599
        },
600

    
601
        get_ram_size: function() {
602
            return parseInt(this.get("ram"))
603
        },
604

    
605
        get_disk_template_info: function() {
606
            var info = snf.config.flavors_disk_templates_info[this.get("disk_template")];
607
            if (!info) {
608
                info = { name: this.get("disk_template"), description:'' };
609
            }
610
            return info
611
        },
612

    
613
        disk_to_bytes: function() {
614
            return parseInt(this.get("disk")) * 1024 * 1024 * 1024;
615
        },
616

    
617
        ram_to_bytes: function() {
618
            return parseInt(this.get("ram")) * 1024 * 1024;
619
        },
620

    
621
    });
622
    
623
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
624
    _.extend(models.ParamsList.prototype, bb.Events, {
625

    
626
        initialize: function(parent, param_name) {
627
            this.parent = parent;
628
            this.actions = {};
629
            this.param_name = param_name;
630
            this.length = 0;
631
        },
632
        
633
        has_action: function(action) {
634
            return this.actions[action] ? true : false;
635
        },
636
            
637
        _parse_params: function(arguments) {
638
            if (arguments.length <= 1) {
639
                return [];
640
            }
641

    
642
            var args = _.toArray(arguments);
643
            return args.splice(1);
644
        },
645

    
646
        contains: function(action, params) {
647
            params = this._parse_params(arguments);
648
            var has_action = this.has_action(action);
649
            if (!has_action) { return false };
650

    
651
            var paramsEqual = false;
652
            _.each(this.actions[action], function(action_params) {
653
                if (_.isEqual(action_params, params)) {
654
                    paramsEqual = true;
655
                }
656
            });
657
                
658
            return paramsEqual;
659
        },
660
        
661
        is_empty: function() {
662
            return _.isEmpty(this.actions);
663
        },
664

    
665
        add: function(action, params) {
666
            params = this._parse_params(arguments);
667
            if (this.contains.apply(this, arguments)) { return this };
668
            var isnew = false
669
            if (!this.has_action(action)) {
670
                this.actions[action] = [];
671
                isnew = true;
672
            };
673

    
674
            this.actions[action].push(params);
675
            this.parent.trigger("change:" + this.param_name, this.parent, this);
676
            if (isnew) {
677
                this.trigger("add", action, params);
678
            } else {
679
                this.trigger("change", action, params);
680
            }
681
            return this;
682
        },
683
        
684
        remove_all: function(action) {
685
            if (this.has_action(action)) {
686
                delete this.actions[action];
687
                this.parent.trigger("change:" + this.param_name, this.parent, this);
688
                this.trigger("remove", action);
689
            }
690
            return this;
691
        },
692

    
693
        reset: function() {
694
            this.actions = {};
695
            this.parent.trigger("change:" + this.param_name, this.parent, this);
696
            this.trigger("reset");
697
            this.trigger("remove");
698
        },
699

    
700
        remove: function(action, params) {
701
            params = this._parse_params(arguments);
702
            if (!this.has_action(action)) { return this };
703
            var index = -1;
704
            _.each(this.actions[action], _.bind(function(action_params) {
705
                if (_.isEqual(action_params, params)) {
706
                    index = this.actions[action].indexOf(action_params);
707
                }
708
            }, this));
709
            
710
            if (index > -1) {
711
                this.actions[action].splice(index, 1);
712
                if (_.isEmpty(this.actions[action])) {
713
                    delete this.actions[action];
714
                }
715
                this.parent.trigger("change:" + this.param_name, this.parent, this);
716
                this.trigger("remove", action, params);
717
            }
718
        }
719

    
720
    });
721

    
722
    // Virtualmachine model
723
    models.VM = models.Model.extend({
724

    
725
        path: 'servers',
726
        has_status: true,
727
        proxy_attrs: {
728
          'busy': [
729
            ['status', 'state'], function() {
730
              return !_.contains(['ACTIVE', 'STOPPED'], this.get('status'));
731
            }
732
          ],
733
          'in_progress': [
734
            ['status', 'state'], function() {
735
              return this.in_transition();
736
            }
737
          ]
738
        },
739

    
740
        initialize: function(params) {
741
            var self = this;
742
            this.ports = new Backbone.FilteredCollection(undefined, {
743
              collection: synnefo.storage.ports,
744
              collectionFilter: function(m) {
745
                return self.id == m.get('device_id')
746
            }});
747

    
748
            this.pending_firewalls = {};
749
            
750
            models.VM.__super__.initialize.apply(this, arguments);
751

    
752

    
753
            this.set({state: params.status || "ERROR"});
754
            this.log = new snf.logging.logger("VM " + this.id);
755
            this.pending_action = undefined;
756
            
757
            // init stats parameter
758
            this.set({'stats': undefined}, {silent: true});
759
            // defaults to not update the stats
760
            // each view should handle this vm attribute 
761
            // depending on if it displays stat images or not
762
            this.do_update_stats = false;
763
            
764
            // interval time
765
            // this will dynamicaly change if the server responds that
766
            // images get refreshed on different intervals
767
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
768
            this.stats_available = false;
769

    
770
            // initialize interval
771
            this.init_stats_intervals(this.stats_update_interval);
772
            
773
            // handle progress message on instance change
774
            this.bind("change", _.bind(this.update_status_message, this));
775
            this.bind("change:task_state", _.bind(this.update_status, this));
776
            // force update of progress message
777
            this.update_status_message(true);
778
            
779
            // default values
780
            this.bind("change:state", _.bind(function(){
781
                if (this.state() == "DESTROY") { 
782
                    this.handle_destroy() 
783
                }
784
            }, this));
785

    
786
        },
787
        
788
        get_public_ips: function() {
789
          var ips = [];
790
          this.ports.filter(function(port) {
791
            if (port.get('network') && !port.get('network').get('is_public')) { return }
792
            if (!port.get("ips")) { return }
793
            port.get("ips").each(function(ip) {
794
              ips.push(ip);
795
            });
796
          });
797
          return ips;
798
        },
799

    
800
        has_public_ip: function() {
801
          return this.ports.filter(function(port) {
802
            return port.get("network") && 
803
                   port.get("network").get("is_public") && 
804
                   port.get("ips").length > 0;
805
          }).length > 0;
806
        },
807

    
808
        has_public_ipv6: function() {
809
          return this.has_ip_version("v6", true);
810
        },
811

    
812
        has_public_ipv4: function() {
813
          return this.has_ip_version("v4", true);
814
        },
815
        
816
        has_ip_version: function(ver, public) {
817
          var found = false;
818
          this.ports.each(function(port) {
819
            if (found) { return }
820
            if (public !== undefined) {
821
              if (port.get("network") && 
822
                  port.get("network").get("is_public") != public) {
823
                return
824
              }
825
            }
826
            port.get('ips').each(function(ip) {
827
              if (found) { return }
828
              if (ip.get("type") == ver) {
829
                found = true
830
              }
831
            })
832
          }, this)
833
          return found;
834
        },
835

    
836
        status: function(st) {
837
            if (!st) { return this.get("status")}
838
            return this.set({status:st});
839
        },
840
        
841
        update_status: function() {
842
            this.set_status(this.get('status'));
843
        },
844

    
845
        set_status: function(st) {
846
            var new_state = this.state_for_api_status(st);
847
            var transition = false;
848

    
849
            if (this.state() != new_state) {
850
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
851
                    transition = this.state();
852
                }
853
            }
854
            
855
            // call it silently to avoid double change trigger
856
            var state = this.state_for_api_status(st);
857
            this.set({'state': state}, {silent: true});
858
            
859
            // trigger transition
860
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
861
                this.trigger("transition", {from:transition, to:new_state}) 
862
            };
863
            return st;
864
        },
865
            
866
        get_diagnostics: function(success) {
867
            this.__make_api_call(this.get_diagnostics_url(),
868
                                 "read", // create so that sync later uses POST to make the call
869
                                 null, // payload
870
                                 function(data) {
871
                                     success(data);
872
                                 },  
873
                                 null, 'diagnostics');
874
        },
875

    
876
        has_diagnostics: function() {
877
            return this.get("diagnostics") && this.get("diagnostics").length;
878
        },
879

    
880
        get_progress_info: function() {
881
            // details about progress message
882
            // contains a list of diagnostic messages
883
            return this.get("status_messages");
884
        },
885

    
886
        get_status_message: function() {
887
            return this.get('status_message');
888
        },
889
        
890
        // extract status message from diagnostics
891
        status_message_from_diagnostics: function(diagnostics) {
892
            var valid_sources_map = synnefo.config.diagnostics_status_messages_map;
893
            var valid_sources = valid_sources_map[this.get('status')];
894
            if (!valid_sources) { return null };
895
            
896
            // filter messsages based on diagnostic source
897
            var messages = _.filter(diagnostics, function(diag) {
898
                return valid_sources.indexOf(diag.source) > -1;
899
            });
900

    
901
            var msg = messages[0];
902
            if (msg) {
903
              var message = msg.message;
904
              var message_tpl = snf.config.diagnostic_messages_tpls[msg.source];
905

    
906
              if (message_tpl) {
907
                  message = message_tpl.replace('MESSAGE', msg.message);
908
              }
909
              return message;
910
            }
911
            
912
            // no message to display, but vm in build state, display
913
            // finalizing message.
914
            if (this.is_building() == 'BUILD') {
915
                return synnefo.config.BUILDING_MESSAGES['FINAL'];
916
            }
917
            return null;
918
        },
919

    
920
        update_status_message: function(force) {
921
            // update only if one of the specified attributes has changed
922
            if (
923
              !this.keysChanged(['diagnostics', 'progress', 'status', 'state'])
924
                && !force
925
            ) { return };
926
            
927
            // if user requested to destroy the vm set the appropriate 
928
            // message.
929
            if (this.get('state') == "DESTROY") { 
930
                message = "Terminating..."
931
                this.set({status_message: message})
932
                return;
933
            }
934
            
935
            // set error message, if vm has diagnostic message display it as
936
            // progress message
937
            if (this.in_error_state()) {
938
                var d = this.get('diagnostics');
939
                if (d && d.length) {
940
                    var message = this.status_message_from_diagnostics(d);
941
                    this.set({status_message: message});
942
                } else {
943
                    this.set({status_message: null});
944
                }
945
                return;
946
            }
947
            
948
            // identify building status message
949
            if (this.is_building()) {
950
                var self = this;
951
                var success = function(msg) {
952
                    self.set({status_message: msg});
953
                }
954
                this.get_building_status_message(success);
955
                return;
956
            }
957

    
958
            this.set({status_message:null});
959
        },
960
            
961
        // get building status message. Asynchronous function since it requires
962
        // access to vm image.
963
        get_building_status_message: function(callback) {
964
            // no progress is set, vm is in initial build status
965
            var progress = this.get("progress");
966
            if (progress == 0 || !progress) {
967
                return callback(BUILDING_MESSAGES['INIT']);
968
            }
969
            
970
            // vm has copy progress, display copy percentage
971
            if (progress > 0 && progress <= 99) {
972
                this.get_copy_details(true, undefined, _.bind(
973
                    function(details){
974
                        callback(BUILDING_MESSAGES['COPY'].format(details.copy, 
975
                                                           details.size, 
976
                                                           details.progress));
977
                }, this));
978
                return;
979
            }
980

    
981
            // copy finished display FINAL message or identify status message
982
            // from diagnostics.
983
            if (progress >= 100) {
984
                if (!this.has_diagnostics()) {
985
                        callback(BUILDING_MESSAGES['FINAL']);
986
                } else {
987
                        var d = this.get("diagnostics");
988
                        var msg = this.status_message_from_diagnostics(d);
989
                        if (msg) {
990
                              callback(msg);
991
                        }
992
                }
993
            }
994
        },
995

    
996
        get_copy_details: function(human, image, callback) {
997
            var human = human || false;
998
            var image = image || this.get_image(_.bind(function(image){
999
                var progress = this.get('progress');
1000
                var size = image.get_size();
1001
                var size_copied = (size * progress / 100).toFixed(2);
1002
                
1003
                if (human) {
1004
                    size = util.readablizeBytes(size*1024*1024);
1005
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
1006
                }
1007

    
1008
                callback({'progress': progress, 'size': size, 'copy': size_copied})
1009
            }, this));
1010
        },
1011

    
1012
        start_stats_update: function(force_if_empty) {
1013
            var prev_state = this.do_update_stats;
1014

    
1015
            this.do_update_stats = true;
1016
            
1017
            // fetcher initialized ??
1018
            if (!this.stats_fetcher) {
1019
                this.init_stats_intervals();
1020
            }
1021

    
1022

    
1023
            // fetcher running ???
1024
            if (!this.stats_fetcher.running || !prev_state) {
1025
                this.stats_fetcher.start();
1026
            }
1027

    
1028
            if (force_if_empty && this.get("stats") == undefined) {
1029
                this.update_stats(true);
1030
            }
1031
        },
1032

    
1033
        stop_stats_update: function(stop_calls) {
1034
            this.do_update_stats = false;
1035

    
1036
            if (stop_calls) {
1037
                this.stats_fetcher.stop();
1038
            }
1039
        },
1040

    
1041
        // clear and reinitialize update interval
1042
        init_stats_intervals: function (interval) {
1043
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
1044
            this.stats_fetcher.start();
1045
        },
1046
        
1047
        get_stats_fetcher: function(timeout) {
1048
            var cb = _.bind(function(data){
1049
                this.update_stats();
1050
            }, this);
1051
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
1052
            return fetcher;
1053
        },
1054

    
1055
        // do the api call
1056
        update_stats: function(force) {
1057
            // do not update stats if flag not set
1058
            if ((!this.do_update_stats && !force) || this.updating_stats) {
1059
                return;
1060
            }
1061

    
1062
            // make the api call, execute handle_stats_update on sucess
1063
            // TODO: onError handler ???
1064
            stats_url = this.url() + "/stats";
1065
            this.updating_stats = true;
1066
            this.sync("read", this, {
1067
                handles_error:true, 
1068
                url: stats_url, 
1069
                refresh:true, 
1070
                success: _.bind(this.handle_stats_update, this),
1071
                error: _.bind(this.handle_stats_error, this),
1072
                complete: _.bind(function(){this.updating_stats = false;}, this),
1073
                critical: false,
1074
                log_error: false,
1075
                skips_timeouts: true
1076
            });
1077
        },
1078

    
1079
        get_attachment: function(id) {
1080
          var attachment = undefined;
1081
          _.each(this.get("attachments"), function(a) {
1082
            if (a.id == id) {
1083
              attachment = a;
1084
            }
1085
          });
1086
          return attachment
1087
        },
1088

    
1089
        _set_stats: function(stats) {
1090
            var silent = silent === undefined ? false : silent;
1091
            // unavailable stats while building
1092
            if (this.get("status") == "BUILD") { 
1093
                this.stats_available = false;
1094
            } else { this.stats_available = true; }
1095

    
1096
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
1097
            
1098
            this.set({stats: stats}, {silent:true});
1099
            this.trigger("stats:update", stats);
1100
        },
1101

    
1102
        unbind: function() {
1103
            models.VM.__super__.unbind.apply(this, arguments);
1104
        },
1105
        
1106
        can_start: function(flv, count_current) {
1107
          var get_quota = function(key) {
1108
            return synnefo.storage.quotas.get(key).get('available');
1109
          }
1110
          var flavor = flv || this.get_flavor();
1111
          var vm_ram_current = 0, vm_cpu_current = 0;
1112
          if (flv && this.is_active() || flv && count_current) {
1113
            var current = this.get_flavor();
1114
            vm_ram_current = current.ram_to_bytes();
1115
            vm_cpu_current = parseInt(current.get('cpu'));
1116
          }
1117
          var vm_ram = flavor.ram_to_bytes();
1118
          var vm_cpu = parseInt(flavor.get('cpu'));
1119
          var available_cpu = get_quota('cyclades.cpu') + vm_cpu_current;
1120
          var available_ram = get_quota('cyclades.ram') + vm_ram_current;
1121
          if (vm_ram > available_ram || vm_cpu > available_cpu) { return false }
1122
          return true
1123
        },
1124

    
1125
        can_connect: function() {
1126
          if (!synnefo.config.hotplug_enabled && this.is_active()) { return false }
1127
          return _.contains(["ACTIVE", "STOPPED"], this.get("status")) && 
1128
                 !this.get('suspended')
1129
        },
1130

    
1131
        can_disconnect: function() {
1132
          return _.contains(["ACTIVE", "STOPPED"], this.get("status"))
1133
        },
1134

    
1135
        can_resize: function() {
1136
          return this.get('status') == 'STOPPED';
1137
        },
1138

    
1139
        handle_stats_error: function() {
1140
            stats = {};
1141
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1142
                stats[k] = false;
1143
            });
1144

    
1145
            this.set({'stats': stats});
1146
        },
1147

    
1148
        // this method gets executed after a successful vm stats api call
1149
        handle_stats_update: function(data) {
1150
            var self = this;
1151
            // avoid browser caching
1152
            
1153
            if (data.stats && _.size(data.stats) > 0) {
1154
                var ts = $.now();
1155
                var stats = data.stats;
1156
                var images_loaded = 0;
1157
                var images = {};
1158

    
1159
                function check_images_loaded() {
1160
                    images_loaded++;
1161

    
1162
                    if (images_loaded == 4) {
1163
                        self._set_stats(images);
1164
                    }
1165
                }
1166
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1167
                    
1168
                    stats[k] = stats[k] + "?_=" + ts;
1169
                    
1170
                    var stat = k.slice(0,3);
1171
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
1172
                    var img = $("<img />");
1173
                    var val = stats[k];
1174
                    
1175
                    // load stat image to a temporary dom element
1176
                    // update model stats on image load/error events
1177
                    img.load(function() {
1178
                        images[k] = val;
1179
                        check_images_loaded();
1180
                    });
1181

    
1182
                    img.error(function() {
1183
                        images[stat + type] = false;
1184
                        check_images_loaded();
1185
                    });
1186

    
1187
                    img.attr({'src': stats[k]});
1188
                })
1189
                data.stats = stats;
1190
            }
1191

    
1192
            // do we need to change the interval ??
1193
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
1194
                this.stats_update_interval = data.stats.refresh * 1000;
1195
                this.stats_fetcher.interval = this.stats_update_interval;
1196
                this.stats_fetcher.maximum_interval = this.stats_update_interval;
1197
                this.stats_fetcher.stop();
1198
                this.stats_fetcher.start(false);
1199
            }
1200
        },
1201

    
1202
        // helper method that sets the do_update_stats
1203
        // in the future this method could also make an api call
1204
        // immediaetly if needed
1205
        enable_stats_update: function() {
1206
            this.do_update_stats = true;
1207
        },
1208
        
1209
        handle_destroy: function() {
1210
            this.stats_fetcher.stop();
1211
        },
1212

    
1213
        require_reboot: function() {
1214
            if (this.is_active()) {
1215
                this.set({'reboot_required': true});
1216
            }
1217
        },
1218
        
1219
        set_pending_action: function(data) {
1220
            this.pending_action = data;
1221
            return data;
1222
        },
1223

    
1224
        // machine has pending action
1225
        update_pending_action: function(action, force) {
1226
            this.set({pending_action: action});
1227
        },
1228

    
1229
        clear_pending_action: function() {
1230
            this.set({pending_action: undefined});
1231
        },
1232

    
1233
        has_pending_action: function() {
1234
            return this.get("pending_action") ? this.get("pending_action") : false;
1235
        },
1236
        
1237
        // machine is active
1238
        is_active: function() {
1239
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
1240
        },
1241
        
1242
        // machine is building 
1243
        is_building: function() {
1244
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
1245
        },
1246
        
1247
        is_rebooting: function() {
1248
            return this.state() == 'REBOOT';
1249
        },
1250

    
1251
        in_error_state: function() {
1252
            return this.state() === "ERROR"
1253
        },
1254

    
1255
        // user can connect to machine
1256
        is_connectable: function() {
1257
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1258
        },
1259
        
1260
        remove_meta: function(key, complete, error) {
1261
            var url = this.api_path() + "/metadata/" + key;
1262
            this.api_call(url, "delete", undefined, complete, error);
1263
        },
1264

    
1265
        save_meta: function(meta, complete, error) {
1266
            var url = this.api_path() + "/metadata/" + meta.key;
1267
            var payload = {meta:{}};
1268
            payload.meta[meta.key] = meta.value;
1269
            payload._options = {
1270
                critical:false, 
1271
                error_params: {
1272
                    title: "Machine metadata error",
1273
                    extra_details: {"Machine id": this.id}
1274
            }};
1275

    
1276
            this.api_call(url, "update", payload, complete, error);
1277
        },
1278

    
1279

    
1280
        // update/get the state of the machine
1281
        state: function() {
1282
            var args = slice.call(arguments);
1283
                
1284
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1285
                this.set({'state': args[0]});
1286
            }
1287

    
1288
            return this.get('state');
1289
        },
1290
        
1291
        // get the state that the api status corresponds to
1292
        state_for_api_status: function(status) {
1293
            return this.state_transition(this.state(), status);
1294
        },
1295
        
1296
        // get transition state for the corresponging api status
1297
        state_transition: function(state, new_status) {
1298
            var statuses = models.VM.STATES_TRANSITIONS[state];
1299
            if (statuses) {
1300
                if (statuses.indexOf(new_status) > -1) {
1301
                    return new_status;
1302
                } else {
1303
                    return state;
1304
                }
1305
            } else {
1306
                return new_status;
1307
            }
1308
        },
1309
        
1310
        // the current vm state is a transition state
1311
        in_transition: function() {
1312
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
1313
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
1314
        },
1315
        
1316
        // get image object
1317
        get_image: function(callback) {
1318
            if (callback == undefined) { callback = function(){} }
1319
            var image = storage.images.get(this.get('image'));
1320
            if (!image) {
1321
                storage.images.update_unknown_id(this.get('image'), callback);
1322
                return;
1323
            }
1324
            callback(image);
1325
            return image;
1326
        },
1327
        
1328
        // get flavor object
1329
        get_flavor: function() {
1330
            var flv = storage.flavors.get(this.get('flavor'));
1331
            if (!flv) {
1332
                storage.flavors.update_unknown_id(this.get('flavor'));
1333
                flv = storage.flavors.get(this.get('flavor'));
1334
            }
1335
            return flv;
1336
        },
1337

    
1338
        get_resize_flavors: function() {
1339
          var vm_flavor = this.get_flavor();
1340
          var flavors = synnefo.storage.flavors.filter(function(f){
1341
              return f.get('disk_template') ==
1342
              vm_flavor.get('disk_template') && f.get('disk') ==
1343
              vm_flavor.get('disk');
1344
          });
1345
          return flavors;
1346
        },
1347

    
1348
        get_flavor_quotas: function() {
1349
          var flavor = this.get_flavor();
1350
          return {
1351
            cpu: flavor.get('cpu'), 
1352
            ram: flavor.get_ram_size(), 
1353
            disk:flavor.get_disk_size()
1354
          }
1355
        },
1356

    
1357
        get_meta: function(key, deflt) {
1358
            if (this.get('metadata') && this.get('metadata')) {
1359
                if (!this.get('metadata')[key]) { return deflt }
1360
                return _.escape(this.get('metadata')[key]);
1361
            } else {
1362
                return deflt;
1363
            }
1364
        },
1365

    
1366
        get_meta_keys: function() {
1367
            if (this.get('metadata') && this.get('metadata')) {
1368
                return _.keys(this.get('metadata'));
1369
            } else {
1370
                return [];
1371
            }
1372
        },
1373
        
1374
        // get metadata OS value
1375
        get_os: function() {
1376
            var image = this.get_image();
1377
            return this.get_meta('OS') || (image ? 
1378
                                            image.get_os() || "okeanos" : "okeanos");
1379
        },
1380

    
1381
        get_gui: function() {
1382
            return this.get_meta('GUI');
1383
        },
1384
        
1385
        get_hostname: function() {
1386
          return this.get_meta('hostname') || this.get('fqdn') || synnefo.config.no_fqdn_message;
1387
        },
1388

    
1389
        // get actions that the user can execute
1390
        // depending on the vm state/status
1391
        get_available_actions: function() {
1392
            if (this.get('suspended')) { return [] }
1393
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1394
        },
1395

    
1396
        set_profile: function(profile, net_id) {
1397
        },
1398
        
1399
        // call rename api
1400
        rename: function(new_name) {
1401
            //this.set({'name': new_name});
1402
            this.sync("update", this, {
1403
                critical: true,
1404
                data: {
1405
                    'server': {
1406
                        'name': new_name
1407
                    }
1408
                }, 
1409
                success: _.bind(function(){
1410
                    snf.api.trigger("call");
1411
                }, this)
1412
            });
1413
        },
1414
        
1415
        get_console_url: function(data) {
1416
            var url_params = {
1417
                machine: this.get("name"),
1418
                host_ip: this.get_hostname(),
1419
                host_ip_v6: this.get_hostname(),
1420
                host: data.host,
1421
                port: data.port,
1422
                password: data.password
1423
            }
1424
            return synnefo.config.ui_console_url + '?' + $.param(url_params);
1425
        },
1426
        
1427
        set_firewall: function(nic, value, success_cb, error_cb) {
1428
          var self = this;
1429
          var success = function() { self.require_reboot(); success_cb() }
1430
          var error = function() { error_cb() }
1431
          var data = {'nic': nic.id, 'profile': value, 'display': true};
1432
          var url = this.url() + "/action";
1433
          //var params = {skip_api_error: false, display: true};
1434
          this.call('firewallProfile', success, error, data);
1435
        },
1436

    
1437
        connect_floating_ip: function(ip, cb, error) {
1438
          var self = this;
1439
          var from_status = this.get('status');
1440
          this.set({'status': 'CONNECTING'});
1441
          synnefo.storage.ports.create({
1442
            port: {
1443
              network_id: ip.get('floating_network_id'),
1444
              device_id: this.id,
1445
              fixed_ips: [{'ip_address': ip.get('floating_ip_address')}]
1446
            }
1447
          }, {
1448
            success: cb, 
1449
            error: function() { error && error() },
1450
            skip_api_error: false
1451
          });
1452
        },
1453

    
1454
        // action helper
1455
        call: function(action_name, success, error, params) {
1456
            var id_param = [this.id];
1457
            
1458
            params = params || {};
1459
            success = success || function() {};
1460
            error = error || function() {};
1461

    
1462
            var self = this;
1463

    
1464
            switch(action_name) {
1465
                case 'start':
1466
                    this.__make_api_call(this.get_action_url(), // vm actions url
1467
                                         "create", // create so that sync later uses POST to make the call
1468
                                         {start:{}}, // payload
1469
                                         function() {
1470
                                             // set state after successful call
1471
                                             self.state("START"); 
1472
                                             success.apply(this, arguments);
1473
                                             snf.api.trigger("call");
1474
                                         },  
1475
                                         error, 'start', params);
1476
                    break;
1477
                case 'reboot':
1478
                    this.__make_api_call(this.get_action_url(), // vm actions url
1479
                                         "create", // create so that sync later uses POST to make the call
1480
                                         {reboot:{}}, // payload
1481
                                         function() {
1482
                                             // set state after successful call
1483
                                             self.state("REBOOT"); 
1484
                                             success.apply(this, arguments)
1485
                                             snf.api.trigger("call");
1486
                                             self.set({'reboot_required': false});
1487
                                         },
1488
                                         error, 'reboot', params);
1489
                    break;
1490
                case 'shutdown':
1491
                    this.__make_api_call(this.get_action_url(), // vm actions url
1492
                                         "create", // create so that sync later uses POST to make the call
1493
                                         {shutdown:{}}, // payload
1494
                                         function() {
1495
                                             // set state after successful call
1496
                                             self.state("SHUTDOWN"); 
1497
                                             success.apply(this, arguments)
1498
                                             snf.api.trigger("call");
1499
                                         },  
1500
                                         error, 'shutdown', params);
1501
                    break;
1502
                case 'console':
1503
                    this.__make_api_call(this.url() + "/action", "create", 
1504
                                         {'console': {'type':'vnc'}}, 
1505
                                         function(data) {
1506
                        var cons_data = data.console;
1507
                        success.apply(this, [cons_data]);
1508
                    }, undefined, 'console', params)
1509
                    break;
1510
                case 'destroy':
1511
                    this.__make_api_call(this.url(), // vm actions url
1512
                                         "delete", // create so that sync later uses POST to make the call
1513
                                         undefined, // payload
1514
                                         function() {
1515
                                             // set state after successful call
1516
                                             self.state('DESTROY');
1517
                                             success.apply(this, arguments);
1518

    
1519
                                         },  
1520
                                         error, 'destroy', params);
1521
                    break;
1522
                case 'resize':
1523
                    this.__make_api_call(this.get_action_url(), // vm actions url
1524
                                         "create", // create so that sync later uses POST to make the call
1525
                                         {resize: {flavorRef:params.flavor}}, // payload
1526
                                         function() {
1527
                                             self.state('RESIZE');
1528
                                             success.apply(this, arguments);
1529
                                             snf.api.trigger("call");
1530
                                         },  
1531
                                         error, 'resize', params);
1532
                    break;
1533
                case 'destroy':
1534
                    this.__make_api_call(this.url(), // vm actions url
1535
                                         "delete", // create so that sync later uses POST to make the call
1536
                                         undefined, // payload
1537
                                         function() {
1538
                                             // set state after successful call
1539
                                             self.state('DESTROY');
1540
                                             success.apply(this, arguments);
1541
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1542

    
1543
                                         },  
1544
                                         error, 'destroy', params);
1545
                    break;
1546
                case 'firewallProfile':
1547
                    this.__make_api_call(this.get_action_url(), // vm actions url
1548
                                         "create",
1549
                                         {firewallProfile:{nic:params.nic, profile:params.profile}}, // payload
1550
                                         function() {
1551
                                             success.apply(this, arguments);
1552
                                             snf.api.trigger("call");
1553
                                         },  
1554
                                         error, 'start', params);
1555
                    break;
1556
                default:
1557
                    throw "Invalid VM action ("+action_name+")";
1558
            }
1559
        },
1560
        
1561
        __make_api_call: function(url, method, data, success, error, action, 
1562
                                  extra_params) {
1563
            var self = this;
1564
            error = error || function(){};
1565
            success = success || function(){};
1566

    
1567
            var params = {
1568
                url: url,
1569
                data: data,
1570
                success: function() { 
1571
                  self.handle_action_succeed.apply(self, arguments); 
1572
                  success.apply(this, arguments)
1573
                },
1574
                error: function() { 
1575
                  self.handle_action_fail.apply(self, arguments);
1576
                  error.apply(this, arguments)
1577
                },
1578
                error_params: { ns: "Machines actions", 
1579
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1580
                                extra_details: {
1581
                                  'Machine ID': this.id, 
1582
                                  'URL': url, 
1583
                                  'Action': action || "undefined" },
1584
                                allow_reload: false
1585
                              },
1586
                display: false,
1587
                critical: false
1588
            }
1589
            _.extend(params, extra_params);
1590
            this.sync(method, this, params);
1591
        },
1592

    
1593
        handle_action_succeed: function() {
1594
            this.trigger("action:success", arguments);
1595
        },
1596
        
1597
        reset_action_error: function() {
1598
            this.action_error = false;
1599
            this.trigger("action:fail:reset", this.action_error);
1600
        },
1601

    
1602
        handle_action_fail: function() {
1603
            this.action_error = arguments;
1604
            this.trigger("action:fail", arguments);
1605
        },
1606

    
1607
        get_action_url: function(name) {
1608
            return this.url() + "/action";
1609
        },
1610

    
1611
        get_diagnostics_url: function() {
1612
            return this.url() + "/diagnostics";
1613
        },
1614

    
1615
        get_users: function() {
1616
            var image;
1617
            var users = [];
1618
            try {
1619
              var users = this.get_meta('users').split(" ");
1620
            } catch (err) { users = null }
1621
            if (!users) {
1622
              image = this.get_image();
1623
              if (image) {
1624
                  users = image.get_created_users();
1625
              }
1626
            }
1627
            return users;
1628
        },
1629

    
1630
        get_connection_info: function(host_os, success, error) {
1631
            var url = synnefo.config.ui_connect_url;
1632
            var users = this.get_users();
1633

    
1634
            params = {
1635
                ip_address: this.get_hostname(),
1636
                hostname: this.get_hostname(),
1637
                os: this.get_os(),
1638
                host_os: host_os,
1639
                ports: JSON.stringify(this.get('SNF:port_forwarding') || {}),
1640
                srv: this.id
1641
            }
1642
            
1643
            if (users.length) { 
1644
                params['username'] = _.last(users)
1645
            }
1646

    
1647
            url = url + "?" + $.param(params);
1648

    
1649
            var ajax = snf.api.sync("read", undefined, { url: url, 
1650
                                                         error:error, 
1651
                                                         success:success, 
1652
                                                         handles_error:1});
1653
        }
1654
    });
1655
    
1656
    models.VM.ACTIONS = [
1657
        'start',
1658
        'shutdown',
1659
        'reboot',
1660
        'console',
1661
        'destroy',
1662
        'resize',
1663
        'snapshot'
1664
    ]
1665

    
1666
    models.VM.TASK_STATE_STATUS_MAP = {
1667
      'BULDING': 'BUILD',
1668
      'REBOOTING': 'REBOOT',
1669
      'STOPPING': 'SHUTDOWN',
1670
      'STARTING': 'START',
1671
      'RESIZING': 'RESIZE',
1672
      'CONNECTING': 'CONNECT',
1673
      'DISCONNECTING': 'DISCONNECT',
1674
      'DESTROYING': 'DESTROY'
1675
    }
1676

    
1677
    models.VM.AVAILABLE_ACTIONS = {
1678
        'UNKNWON'       : ['destroy'],
1679
        'BUILD'         : ['destroy'],
1680
        'REBOOT'        : ['destroy'],
1681
        'STOPPED'       : ['start', 'destroy', 'resize', 'snapshot'],
1682
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console', 'resize', 'snapshot'],
1683
        'ERROR'         : ['destroy'],
1684
        'DELETED'       : ['destroy'],
1685
        'DESTROY'       : ['destroy'],
1686
        'SHUTDOWN'      : ['destroy'],
1687
        'START'         : ['destroy'],
1688
        'CONNECT'       : ['destroy'],
1689
        'DISCONNECT'    : ['destroy'],
1690
        'RESIZE'        : ['destroy']
1691
    }
1692
    
1693
    models.VM.AVAILABLE_ACTIONS_INACTIVE = {}
1694

    
1695
    // api status values
1696
    models.VM.STATUSES = [
1697
        'UNKNWON',
1698
        'BUILD',
1699
        'REBOOT',
1700
        'STOPPED',
1701
        'ACTIVE',
1702
        'ERROR',
1703
        'DELETED',
1704
        'RESIZE'
1705
    ]
1706

    
1707
    // api status values
1708
    models.VM.CONNECT_STATES = [
1709
        'ACTIVE',
1710
        'REBOOT',
1711
        'SHUTDOWN'
1712
    ]
1713

    
1714
    // vm states
1715
    models.VM.STATES = models.VM.STATUSES.concat([
1716
        'DESTROY',
1717
        'SHUTDOWN',
1718
        'START',
1719
        'CONNECT',
1720
        'DISCONNECT',
1721
        'FIREWALL',
1722
        'RESIZE'
1723
    ]);
1724
    
1725
    models.VM.STATES_TRANSITIONS = {
1726
        'DESTROY' : ['DELETED'],
1727
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1728
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1729
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1730
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1731
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1732
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1733
        'RESIZE': ['ERROR', 'STOPPED']
1734
    }
1735

    
1736
    models.VM.TRANSITION_STATES = [
1737
        'DESTROY',
1738
        'SHUTDOWN',
1739
        'START',
1740
        'REBOOT',
1741
        'BUILD',
1742
        'RESIZE',
1743
        'DISCONNECT',
1744
        'CONNECT'
1745
    ]
1746

    
1747
    models.VM.ACTIVE_STATES = [
1748
        'BUILD', 'REBOOT', 'ACTIVE',
1749
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1750
    ]
1751

    
1752
    models.VM.BUILDING_STATES = [
1753
        'BUILD'
1754
    ]
1755

    
1756
    models.Images = models.Collection.extend({
1757
        model: models.Image,
1758
        path: 'images',
1759
        details: true,
1760
        noUpdate: true,
1761
        supportIncUpdates: false,
1762
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1763
        meta_labels: {},
1764
        read_method: 'read',
1765

    
1766
        // update collection model with id passed
1767
        // making a direct call to the image
1768
        // api url
1769
        update_unknown_id: function(id, callback) {
1770
            var url = getUrl.call(this) + "/" + id;
1771
            this.api_call(this.path + "/" + id, this.read_method, {
1772
              _options:{
1773
                async:true, 
1774
                skip_api_error:true}
1775
              }, undefined, 
1776
            _.bind(function() {
1777
                if (!this.get(id)) {
1778
                            if (this.fallback_service) {
1779
                        // if current service has fallback_service attribute set
1780
                        // use this service to retrieve the missing image model
1781
                        var tmpservice = new this.fallback_service();
1782
                        tmpservice.update_unknown_id(id, _.bind(function(img){
1783
                            img.attributes.status = "DELETED";
1784
                            this.add(img.attributes);
1785
                            callback(this.get(id));
1786
                        }, this));
1787
                    } else {
1788
                        var title = synnefo.config.image_deleted_title || 'Deleted';
1789
                        // else add a dummy DELETED state image entry
1790
                        this.add({id:id, name:title, size:-1, 
1791
                                  progress:100, status:"DELETED"});
1792
                        callback(this.get(id));
1793
                    }   
1794
                } else {
1795
                    callback(this.get(id));
1796
                }
1797
            }, this), _.bind(function(image, msg, xhr) {
1798
                if (!image) {
1799
                    var title = synnefo.config.image_deleted_title || 'Deleted';
1800
                    this.add({id:id, name:title, size:-1, 
1801
                              progress:100, status:"DELETED"});
1802
                    callback(this.get(id));
1803
                    return;
1804
                }
1805
                var img_data = this._read_image_from_request(image, msg, xhr);
1806
                this.add(img_data);
1807
                callback(this.get(id));
1808
            }, this));
1809
        },
1810

    
1811
        _read_image_from_request: function(image, msg, xhr) {
1812
            return image.image;
1813
        },
1814

    
1815
        parse: function (resp, xhr) {
1816
            var parsed = _.map(resp.images, _.bind(this.parse_meta, this));
1817
            parsed = this.fill_owners(parsed);
1818
            return parsed;
1819
        },
1820

    
1821
        fill_owners: function(images) {
1822
            // do translate uuid->displayname if needed
1823
            // store display name in owner attribute for compatibility
1824
            var uuids = [];
1825

    
1826
            var images = _.map(images, function(img, index) {
1827
                if (synnefo.config.translate_uuids) {
1828
                    uuids.push(img['owner']);
1829
                }
1830
                img['owner_uuid'] = img['owner'];
1831
                return img;
1832
            });
1833
            
1834
            if (uuids.length > 0) {
1835
                var handle_results = function(data) {
1836
                    _.each(images, function (img) {
1837
                        img['owner'] = data.uuid_catalog[img['owner_uuid']];
1838
                    });
1839
                }
1840
                // notice the async false
1841
                var uuid_map = this.translate_uuids(uuids, false, 
1842
                                                    handle_results)
1843
            }
1844
            return images;
1845
        },
1846

    
1847
        translate_uuids: function(uuids, async, cb) {
1848
            var url = synnefo.config.user_catalog_url;
1849
            var data = JSON.stringify({'uuids': uuids});
1850
          
1851
            // post to user_catalogs api
1852
            snf.api.sync('create', undefined, {
1853
                url: url,
1854
                data: data,
1855
                async: async,
1856
                success:  cb
1857
            });
1858
        },
1859

    
1860
        get_meta_key: function(img, key) {
1861
            if (img.metadata && img.metadata && img.metadata[key]) {
1862
                return _.escape(img.metadata[key]);
1863
            }
1864
            return undefined;
1865
        },
1866

    
1867
        comparator: function(img) {
1868
            return -img.get_sort_order("sortorder") || 0;
1869
        },
1870

    
1871
        parse_meta: function(img) {
1872
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1873
                if (img[key]) { return };
1874
                img[key] = this.get_meta_key(img, key) || "";
1875
            }, this));
1876
            return img;
1877
        },
1878

    
1879
        active: function() {
1880
            return this.filter(function(img){return img.get('status') != "DELETED"});
1881
        },
1882

    
1883
        predefined: function() {
1884
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
1885
        },
1886
        
1887
        fetch_for_type: function(type, complete, error) {
1888
            this.fetch({update:true, 
1889
                        success: complete, 
1890
                        error: error, 
1891
                        skip_api_error: true });
1892
        },
1893
        
1894
        get_images_for_type: function(type) {
1895
            if (this['get_{0}_images'.format(type)]) {
1896
                return this['get_{0}_images'.format(type)]();
1897
            }
1898

    
1899
            return this.active();
1900
        },
1901

    
1902
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
1903
            var load = false;
1904
            error = onError || function() {};
1905
            function complete(collection) { 
1906
                onComplete(collection.get_images_for_type(type)); 
1907
            }
1908
            
1909
            // do we need to fetch/update current collection entries
1910
            if (load) {
1911
                onStart();
1912
                this.fetch_for_type(type, complete, error);
1913
            } else {
1914
                // fallback to complete
1915
                complete(this);
1916
            }
1917
        }
1918
    })
1919

    
1920
    models.Flavors = models.Collection.extend({
1921
        model: models.Flavor,
1922
        path: 'flavors',
1923
        details: true,
1924
        noUpdate: true,
1925
        supportIncUpdates: false,
1926
        // update collection model with id passed
1927
        // making a direct call to the flavor
1928
        // api url
1929
        update_unknown_id: function(id, callback) {
1930
            var url = getUrl.call(this) + "/" + id;
1931
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
1932
            _.bind(function() {
1933
                this.add({id:id, cpu:"Unknown", ram:"Unknown", disk:"Unknown", name: "Unknown", status:"DELETED"})
1934
            }, this), _.bind(function(flv) {
1935
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1936
                this.add(flv.flavor);
1937
            }, this));
1938
        },
1939

    
1940
        parse: function (resp, xhr) {
1941
            return _.map(resp.flavors, function(o) {
1942
              o.cpu = o['vcpus'];
1943
              o.disk_template = o['SNF:disk_template'];
1944
              return o
1945
            });
1946
        },
1947

    
1948
        comparator: function(flv) {
1949
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1950
        },
1951
          
1952
        unavailable_values_for_quotas: function(quotas, flavors, extra) {
1953
            var flavors = flavors || this.active();
1954
            var index = {cpu:[], disk:[], ram:[]};
1955
            var extra = extra == undefined ? {cpu:0, disk:0, ram:0} : extra;
1956
            
1957
            _.each(flavors, function(el) {
1958

    
1959
                var disk_available = quotas['disk'] + extra.disk;
1960
                var disk_size = el.get_disk_size();
1961
                if (index.disk.indexOf(disk_size) == -1) {
1962
                  var disk = el.disk_to_bytes();
1963
                  if (disk > disk_available) {
1964
                    index.disk.push(el.get('disk'));
1965
                  }
1966
                }
1967
                
1968
                var ram_available = quotas['ram'] + extra.ram * 1024 * 1024;
1969
                var ram_size = el.get_ram_size();
1970
                if (index.ram.indexOf(ram_size) == -1) {
1971
                  var ram = el.ram_to_bytes();
1972
                  if (ram > ram_available) {
1973
                    index.ram.push(el.get('ram'))
1974
                  }
1975
                }
1976

    
1977
                var cpu = el.get('cpu');
1978
                var cpu_available = quotas['cpu'] + extra.cpu;
1979
                if (index.cpu.indexOf(cpu) == -1) {
1980
                  if (cpu > cpu_available) {
1981
                    index.cpu.push(el.get('cpu'))
1982
                  }
1983
                }
1984
            });
1985
            return index;
1986
        },
1987

    
1988
        unavailable_values_for_image: function(img, flavors) {
1989
            var flavors = flavors || this.active();
1990
            var size = img.get_size();
1991
            
1992
            var index = {cpu:[], disk:[], ram:[]};
1993

    
1994
            _.each(this.active(), function(el) {
1995
                var img_size = size;
1996
                var flv_size = el.get_disk_size();
1997
                if (flv_size < img_size) {
1998
                    if (index.disk.indexOf(el.get("disk")) == -1) {
1999
                        index.disk.push(el.get("disk"));
2000
                    }
2001
                };
2002
            });
2003
            
2004
            return index;
2005
        },
2006

    
2007
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
2008
            if (!filter_list) { filter_list = this.models };
2009
            
2010
            return this.select(function(flv){
2011
                if (flv.get("cpu") == cpu + "" &&
2012
                   flv.get("ram") == mem + "" &&
2013
                   flv.get("disk") == disk + "" &&
2014
                   flv.get("disk_template") == disk_template &&
2015
                   filter_list.indexOf(flv) > -1) { return true; }
2016
            })[0];
2017
        },
2018
        
2019
        get_data: function(lst) {
2020
            var data = {'cpu': [], 'mem':[], 'disk':[], 'disk_template':[]};
2021

    
2022
            _.each(lst, function(flv) {
2023
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
2024
                    data.cpu.push(flv.get("cpu"));
2025
                }
2026
                if (data.mem.indexOf(flv.get("ram")) == -1) {
2027
                    data.mem.push(flv.get("ram"));
2028
                }
2029
                if (data.disk.indexOf(flv.get("disk")) == -1) {
2030
                    data.disk.push(flv.get("disk"));
2031
                }
2032
                if (data.disk_template.indexOf(flv.get("disk_template")) == -1) {
2033
                    data.disk_template.push(flv.get("disk_template"));
2034
                }
2035
            })
2036
            
2037
            return data;
2038
        },
2039

    
2040
        active: function() {
2041
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
2042
        }
2043
            
2044
    })
2045

    
2046
    models.VMS = models.Collection.extend({
2047
        model: models.VM,
2048
        path: 'servers',
2049
        details: true,
2050
        copy_image_meta: true,
2051

    
2052
        parse: function (resp, xhr) {
2053
            var data = resp;
2054
            if (!resp) { return [] };
2055
            data = _.filter(_.map(resp.servers, 
2056
                                  _.bind(this.parse_vm_api_data, this)), 
2057
                                  function(v){return v});
2058
            return data;
2059
        },
2060

    
2061
        parse_vm_api_data: function(data) {
2062
            var status;
2063
            // do not add non existing DELETED entries
2064
            if (data.status && data.status == "DELETED") {
2065
                if (!this.get(data.id)) {
2066
                    return false;
2067
                }
2068
            }
2069
            
2070
            if ('SNF:task_state' in data) { 
2071
                data['task_state'] = data['SNF:task_state'];
2072
                // Update machine state based on task_state value
2073
                // Do not apply task_state logic when machine is in ERROR state.
2074
                // In that case only update from task_state only if equals to
2075
                // DESTROY
2076
                if (data['task_state']) {
2077
                    if (data['status'] != 'ERROR' && data['task_state'] != 'DESTROY') {
2078
                      status = models.VM.TASK_STATE_STATUS_MAP[data['task_state']];
2079
                      if (status) { data['status'] = status }
2080
                    }
2081
                }
2082
            }
2083

    
2084
            // OS attribute
2085
            if (this.has_meta(data)) {
2086
                data['OS'] = data.metadata.OS || snf.config.unknown_os;
2087
            }
2088
            
2089
            if (!data.diagnostics) {
2090
                data.diagnostics = [];
2091
            }
2092

    
2093
            // network metadata
2094
            data['firewalls'] = {};
2095
            data['fqdn'] = data['SNF:fqdn'];
2096

    
2097
            // if vm has no metadata, no metadata object
2098
            // is in json response, reset it to force
2099
            // value update
2100
            if (!data['metadata']) {
2101
                data['metadata'] = {};
2102
            }
2103
            
2104
            // v2.0 API returns objects
2105
            data.image_obj = data.image;
2106
            data.image = data.image_obj.id;
2107
            data.flavor_obj = data.flavor;
2108
            data.flavor = data.flavor_obj.id;
2109

    
2110
            return data;
2111
        },
2112

    
2113
        get_reboot_required: function() {
2114
            return this.filter(function(vm){return vm.get("reboot_required") == true})
2115
        },
2116

    
2117
        has_pending_actions: function() {
2118
            return this.filter(function(vm){return vm.pending_action}).length > 0;
2119
        },
2120

    
2121
        reset_pending_actions: function() {
2122
            this.each(function(vm) {
2123
                vm.clear_pending_action();
2124
            })
2125
        },
2126

    
2127
        do_all_pending_actions: function(success, error) {
2128
            this.each(function(vm) {
2129
                if (vm.has_pending_action()) {
2130
                    vm.call(vm.pending_action, success, error);
2131
                    vm.clear_pending_action();
2132
                }
2133
            })
2134
        },
2135
        
2136
        do_all_reboots: function(success, error) {
2137
            this.each(function(vm) {
2138
                if (vm.get("reboot_required")) {
2139
                    vm.call("reboot", success, error);
2140
                }
2141
            });
2142
        },
2143

    
2144
        reset_reboot_required: function() {
2145
            this.each(function(vm) {
2146
                vm.set({'reboot_required': undefined});
2147
            })
2148
        },
2149
        
2150
        stop_stats_update: function(exclude) {
2151
            var exclude = exclude || [];
2152
            this.each(function(vm) {
2153
                if (exclude.indexOf(vm) > -1) {
2154
                    return;
2155
                }
2156
                vm.stop_stats_update();
2157
            })
2158
        },
2159
        
2160
        has_meta: function(vm_data) {
2161
            return vm_data.metadata && vm_data.metadata
2162
        },
2163

    
2164
        has_addresses: function(vm_data) {
2165
            return vm_data.metadata && vm_data.metadata
2166
        },
2167

    
2168
        create: function (name, image, flavor, meta, extra, callback) {
2169

    
2170
            if (this.copy_image_meta) {
2171
                if (synnefo.config.vm_image_common_metadata) {
2172
                    _.each(synnefo.config.vm_image_common_metadata, 
2173
                        function(key){
2174
                            if (image.get_meta(key)) {
2175
                                meta[key] = image.get_meta(key);
2176
                            }
2177
                    });
2178
                }
2179

    
2180
                if (image.get("OS")) {
2181
                    meta['OS'] = image.get("OS");
2182
                }
2183
            }
2184
            
2185
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, 
2186
                    metadata:meta}
2187
            opts = _.extend(opts, extra);
2188
            
2189
            var cb = function(data) {
2190
              synnefo.storage.quotas.get('cyclades.vm').increase();
2191
              callback(data);
2192
            }
2193

    
2194
            this.api_call(this.path, "create", {'server': opts}, undefined, 
2195
                          undefined, cb, {critical: true});
2196
        },
2197

    
2198
        load_missing_images: function(callback) {
2199
          var missing_ids = [];
2200
          var resolved = 0;
2201

    
2202
          // fill missing_ids
2203
          this.each(function(el) {
2204
            var imgid = el.get("image");
2205
            var existing = synnefo.storage.images.get(imgid);
2206
            if (!existing && missing_ids.indexOf(imgid) == -1) {
2207
              missing_ids.push(imgid);
2208
            }
2209
          });
2210
          var check = function() {
2211
            // once all missing ids where resolved continue calling the 
2212
            // callback
2213
            resolved++;
2214
            if (resolved == missing_ids.length) {
2215
              callback(missing_ids)
2216
            }
2217
          }
2218
          if (missing_ids.length == 0) {
2219
            callback(missing_ids);
2220
            return;
2221
          }
2222
          // start resolving missing image ids
2223
          _(missing_ids).each(function(imgid){
2224
            synnefo.storage.images.update_unknown_id(imgid, check);
2225
          });
2226
        },
2227

    
2228
        get_connectable: function() {
2229
            return storage.vms.filter(function(vm){
2230
                return !vm.in_error_state() && !vm.is_building();
2231
            });
2232
        }
2233
    })
2234
    
2235
    models.PublicKey = models.Model.extend({
2236
        path: 'keys',
2237
        api_type: 'userdata',
2238
        detail: false,
2239
        model_actions: {
2240
          'remove': [['name'], function() {
2241
            return true;
2242
          }]
2243
        },
2244

    
2245
        get_public_key: function() {
2246
            return cryptico.publicKeyFromString(this.get("content"));
2247
        },
2248

    
2249
        get_filename: function() {
2250
            return "{0}.pub".format(this.get("name"));
2251
        },
2252

    
2253
        identify_type: function() {
2254
            try {
2255
                var cont = snf.util.validatePublicKey(this.get("content"));
2256
                var type = cont.split(" ")[0];
2257
                return synnefo.util.publicKeyTypesMap[type];
2258
            } catch (err) { return false };
2259
        },
2260

    
2261
        rename: function(new_name) {
2262
          //this.set({'name': new_name});
2263
          this.sync("update", this, {
2264
            critical: true,
2265
            data: {'name': new_name}, 
2266
            success: _.bind(function(){
2267
              snf.api.trigger("call");
2268
            }, this)
2269
          });
2270
        },
2271

    
2272
        do_remove: function() {
2273
          this.actions.reset_pending();
2274
          this.remove(function() {
2275
            synnefo.storage.keys.fetch();
2276
          });
2277
        }
2278
    })
2279
    
2280
    models._ActionsModel = models.Model.extend({
2281
      defaults: { pending: null },
2282
      actions: [],
2283
      status: {
2284
        INACTIVE: 0,
2285
        PENDING: 1,
2286
        CALLED: 2
2287
      },
2288

    
2289
      initialize: function(attrs, opts) {
2290
        models._ActionsModel.__super__.initialize.call(this, attrs);
2291
        this.actions = opts.actions;
2292
        this.model = opts.model;
2293
        this.bind("change", function() {
2294
          this.set({'pending': this.get_pending()});
2295
        }, this);
2296
        this.clear();
2297
      },
2298
      
2299
      _in_status: function(st) {
2300
        var actions = null;
2301
        _.each(this.attributes, function(status, action){
2302
          if (status == st) {
2303
            if (!actions) {
2304
              actions = []
2305
            }
2306
            actions.push(action);
2307
          }
2308
        });
2309
        return actions;
2310
      },
2311

    
2312
      get_pending: function() {
2313
        return this._in_status(this.status.PENDING);
2314
      },
2315

    
2316
      unset_pending_action: function(action) {
2317
        var data = {};
2318
        data[action] = this.status.INACTIVE;
2319
        this.set(data);
2320
        this.trigger("unset-pending", action);
2321
      },
2322

    
2323
      set_pending_action: function(action, reset_pending) {
2324
        reset_pending = reset_pending === undefined ? true : reset_pending;
2325
        var data = {};
2326
        data[action] = this.status.PENDING;
2327
        if (reset_pending) {
2328
          this.reset_pending();
2329
        }
2330
        this.set(data);
2331
        this.trigger("set-pending", action);
2332
      },
2333
      
2334
      reset_pending: function() {
2335
        var data = {};
2336
        _.each(this.actions, function(action) {
2337
          data[action] = this.status.INACTIVE;
2338
        }, this);
2339
        this.set(data);
2340
        this.trigger("reset-pending");
2341
      }
2342
    });
2343

    
2344
    models.PublicPool = models.Model.extend({});
2345
    models.PublicPools = models.Collection.extend({
2346
      model: models.PublicPool,
2347
      path: 'os-floating-ip-pools',
2348
      api_type: 'compute',
2349
      noUpdate: true,
2350

    
2351
      parse: function(data) {
2352
        return _.map(data.floating_ip_pools, function(pool) {
2353
          pool.id = pool.name;
2354
          return pool;
2355
        });
2356
      }
2357
    });
2358

    
2359
    models.PublicKeys = models.Collection.extend({
2360
        model: models.PublicKey,
2361
        details: false,
2362
        path: 'keys',
2363
        api_type: 'userdata',
2364
        noUpdate: true,
2365
        updateEntries: true,
2366

    
2367
        generate_new: function(success, error) {
2368
            snf.api.sync('create', undefined, {
2369
                url: getUrl.call(this, this.base_url) + "/generate", 
2370
                success: success, 
2371
                error: error,
2372
                skip_api_error: true
2373
            });
2374
        },
2375
        
2376
        add_crypto_key: function(key, success, error, options) {
2377
            var options = options || {};
2378
            var m = new models.PublicKey();
2379

    
2380
            // guess a name
2381
            var name_tpl = "my generated public key";
2382
            var name = name_tpl;
2383
            var name_count = 1;
2384
            
2385
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
2386
                name = name_tpl + " " + name_count;
2387
                name_count++;
2388
            }
2389
            
2390
            m.set({name: name});
2391
            m.set({content: key});
2392
            
2393
            options.success = function () { return success(m) };
2394
            options.errror = error;
2395
            options.skip_api_error = true;
2396
            
2397
            this.create(m.attributes, options);
2398
        }
2399
    });
2400

    
2401
  
2402
    models.Quota = models.Model.extend({
2403

    
2404
        initialize: function() {
2405
            models.Quota.__super__.initialize.apply(this, arguments);
2406
            this.bind("change", this.check, this);
2407
            this.check();
2408
        },
2409
        
2410
        check: function() {
2411
            var usage, limit;
2412
            usage = this.get('usage');
2413
            limit = this.get('limit');
2414
            if (usage >= limit) {
2415
                this.trigger("available");
2416
            } else {
2417
                this.trigger("unavailable");
2418
            }
2419
        },
2420

    
2421
        increase: function(val) {
2422
            if (val === undefined) { val = 1};
2423
            this.set({'usage': this.get('usage') + val})
2424
        },
2425

    
2426
        decrease: function(val) {
2427
            if (val === undefined) { val = 1};
2428
            this.set({'usage': this.get('usage') - val})
2429
        },
2430

    
2431
        can_consume: function() {
2432
            var usage, limit;
2433
            usage = this.get('usage');
2434
            limit = this.get('limit');
2435
            if (usage >= limit) {
2436
                return false
2437
            } else {
2438
                return true
2439
            }
2440
        },
2441
        
2442
        is_bytes: function() {
2443
            return this.get('resource').get('unit') == 'bytes';
2444
        },
2445
        
2446
        get_available: function(active) {
2447
            suffix = '';
2448
            if (active) { suffix = '_active'}
2449
            var value = this.get('limit'+suffix) - this.get('usage'+suffix);
2450
            if (active) {
2451
              if (this.get('available') <= value) {
2452
                value = this.get('available');
2453
              }
2454
            }
2455
            if (value < 0) { return value }
2456
            return value
2457
        },
2458

    
2459
        get_readable: function(key, active) {
2460
            var value;
2461
            if (key == 'available') {
2462
                value = this.get_available(active);
2463
            } else {
2464
                value = this.get(key)
2465
            }
2466
            if (value <= 0) { value = 0 }
2467
            // greater than max js int (assume infinite quota)
2468
            if (value > Math.pow(2, 53)) { 
2469
              return "Infinite"
2470
            }
2471

    
2472
            if (!this.is_bytes()) {
2473
              return value + "";
2474
            }
2475
            
2476
            return snf.util.readablizeBytes(value);
2477
        }
2478
    });
2479

    
2480
    models.Quotas = models.Collection.extend({
2481
        model: models.Quota,
2482
        api_type: 'accounts',
2483
        path: 'quotas',
2484
        supportIncUpdates: false,
2485
        parse: function(resp) {
2486
            filtered = _.map(resp.system, function(value, key) {
2487
                var available = (value.limit - value.usage) || 0;
2488
                var available_active = available;
2489
                var keysplit = key.split(".");
2490
                var limit_active = value.limit;
2491
                var usage_active = value.usage;
2492
                keysplit[keysplit.length-1] = "total_" + keysplit[keysplit.length-1];
2493
                var activekey = keysplit.join(".");
2494
                var exists = resp.system[activekey];
2495
                if (exists) {
2496
                    available_active = exists.limit - exists.usage;
2497
                    limit_active = exists.limit;
2498
                    usage_active = exists.usage;
2499
                }
2500
                return _.extend(value, {'name': key, 'id': key, 
2501
                          'available': available,
2502
                          'available_active': available_active,
2503
                          'limit_active': limit_active,
2504
                          'usage_active': usage_active,
2505
                          'resource': snf.storage.resources.get(key)});
2506
            });
2507
            return filtered;
2508
        },
2509
        
2510
        get_by_id: function(k) {
2511
          return this.filter(function(q) { return q.get('name') == k})[0]
2512
        },
2513

    
2514
        get_available_for_vm: function(options) {
2515
          var quotas = synnefo.storage.quotas;
2516
          var key = 'available';
2517
          var available_quota = {};
2518
          _.each(['cyclades.ram', 'cyclades.cpu', 'cyclades.disk'], 
2519
            function (key) {
2520
              var value = quotas.get(key).get_available(true);
2521
              available_quota[key.replace('cyclades.', '')] = value;
2522
          });
2523
          return available_quota;
2524
        }
2525
    })
2526

    
2527
    models.Resource = models.Model.extend({
2528
        api_type: 'accounts',
2529
        path: 'resources'
2530
    });
2531

    
2532
    models.Resources = models.Collection.extend({
2533
        api_type: 'accounts',
2534
        path: 'resources',
2535
        model: models.Network,
2536

    
2537
        parse: function(resp) {
2538
            return _.map(resp, function(value, key) {
2539
                return _.extend(value, {'name': key, 'id': key});
2540
            })
2541
        }
2542
    });
2543
    
2544
    // storage initialization
2545
    snf.storage.images = new models.Images();
2546
    snf.storage.flavors = new models.Flavors();
2547
    snf.storage.vms = new models.VMS();
2548
    snf.storage.keys = new models.PublicKeys();
2549
    snf.storage.resources = new models.Resources();
2550
    snf.storage.quotas = new models.Quotas();
2551
    snf.storage.public_pools = new models.PublicPools();
2552

    
2553
})(this);