Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (100.1 kB)

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

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

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

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

    
64
    var NIC_REGEX = /^nic-([0-9]+)-([0-9]+)$/
65
    
66
    // i18n
67
    BUILDING_MESSAGES = window.BUILDING_MESSAGES || {'INIT': 'init', 'COPY': '{0}, {1}, {2}', 'FINAL': 'final'};
68

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

    
77

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

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

    
97
            if (this.storage_attrs) {
98
              this.init_storage_attrs();
99
            }
100

    
101
            if (this.model_actions) {
102
              this.init_model_actions();             
103
            }
104

    
105
            models.Model.__super__.initialize.apply(this, arguments);
106

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

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

    
145
              if (!val) { 
146
                // update with undefined and return
147
                data[key] = undefined;
148
                this.set(data);
149
                return;
150
              };
151
            
152
              // retrieve related object (check if its a Model??)
153
              var obj = store.get(val);
154
              
155
              if (obj) {
156
                // set related object
157
                data[attr_name] = obj;
158
                this.set(data, {silent:true})
159
                this.trigger("change:" + attr_name, obj);
160
              } else {
161
                var self = this;
162
                var retry = window.setInterval(function(){
163
                  var obj = store.get(val);
164
                  if (obj) {
165
                    data[key] = obj;
166
                    self.set(data, {silent:true})
167
                    self.trigger("change:" + attr_name, obj);
168
                    clearInterval(retry);
169
                  }
170
                }, 10);
171
              }
172
            }
173

    
174
            this.bind('change:' + attr, function() {
175
              resolve_related_instance.call(this, store, key, this.get(attr))
176
            });
177

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

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

    
253
        url: function(options) {
254
            return getUrl.call(this, this.base_url) + "/" + this.id;
255
        },
256

    
257
        api_path: function(options) {
258
            return this.path + "/" + this.id;
259
        },
260

    
261
        parse: function(resp, xhr) {
262
        },
263

    
264
        remove: function(complete, error, success) {
265
            this.api_call(this.api_path(), "delete", undefined, complete, error, success);
266
        },
267

    
268
        changedKeys: function() {
269
            return _.keys(this.changedAttributes() || {});
270
        },
271
            
272
        // return list of changed attributes that included in passed list
273
        // argument
274
        getKeysChanged: function(keys) {
275
            return _.intersection(keys, this.changedKeys());
276
        },
277
        
278
        // boolean check of keys changed
279
        keysChanged: function(keys) {
280
            return this.getKeysChanged(keys).length > 0;
281
        },
282

    
283
        // check if any of the passed attribues has changed
284
        hasOnlyChange: function(keys) {
285
            var ret = false;
286
            _.each(keys, _.bind(function(key) {
287
                if (this.changedKeys().length == 1 && this.changedKeys().indexOf(key) > -1) { ret = true};
288
            }, this));
289
            return ret;
290
        }
291

    
292
    })
293
    
294
    // Base object for all our model collections
295
    models.Collection = bb.Collection.extend({
296
        sync: snf.api.sync,
297
        api: snf.api,
298
        api_type: 'compute',
299
        supportIncUpdates: true,
300

    
301
        initialize: function() {
302
            models.Collection.__super__.initialize.apply(this, arguments);
303
            this.api_call = _.bind(this.api.call, this);
304
        },
305

    
306
        url: function(options, method) {
307
            return getUrl.call(this, this.base_url) + (
308
                    options.details || this.details && method != 'create' ? '/detail' : '');
309
        },
310

    
311
        fetch: function(options) {
312
            if (!options) { options = {} };
313
            // default to update
314
            if (!this.noUpdate) {
315
                if (options.update === undefined) { options.update = true };
316
                if (!options.removeMissing && options.refresh) { options.removeMissing = true };
317
            } else {
318
                if (options.refresh === undefined) {
319
                    options.refresh = true;
320
                }
321
            }
322
            // custom event foreach fetch
323
            return bb.Collection.prototype.fetch.call(this, options)
324
        },
325

    
326
        create: function(model, options) {
327
            var coll = this;
328
            options || (options = {});
329
            model = this._prepareModel(model, options);
330
            if (!model) return false;
331
            var success = options.success;
332
            options.success = function(nextModel, resp, xhr) {
333
                if (success) success(nextModel, resp, xhr);
334
            };
335
            model.save(null, options);
336
            return model;
337
        },
338

    
339
        get_fetcher: function(interval, increase, fast, increase_after_calls, max, initial_call, params) {
340
            var fetch_params = params || {};
341
            var handler_options = {};
342

    
343
            fetch_params.skips_timeouts = true;
344
            handler_options.interval = interval;
345
            handler_options.increase = increase;
346
            handler_options.fast = fast;
347
            handler_options.increase_after_calls = increase_after_calls;
348
            handler_options.max= max;
349
            handler_options.id = "collection id";
350

    
351
            var last_ajax = undefined;
352
            var callback = _.bind(function() {
353
                // clone to avoid referenced objects
354
                var params = _.clone(fetch_params);
355
                updater._ajax = last_ajax;
356
                
357
                // wait for previous request to finish
358
                if (last_ajax && last_ajax.readyState < 4 && last_ajax.statusText != "timeout") {
359
                    // opera readystate for 304 responses is 0
360
                    if (!($.browser.opera && last_ajax.readyState == 0 && last_ajax.status == 304)) {
361
                        return;
362
                    }
363
                }
364
                last_ajax = this.fetch(params);
365
            }, this);
366
            handler_options.callback = callback;
367

    
368
            var updater = new snf.api.updateHandler(_.clone(_.extend(handler_options, fetch_params)));
369
            snf.api.bind("call", _.throttle(_.bind(function(){ updater.faster(true)}, this)), 1000);
370
            return updater;
371
        }
372
    });
373
    
374
    // Image model
375
    models.Image = models.Model.extend({
376
        path: 'images',
377
        
378
        get_size: function() {
379
            return parseInt(this.get('metadata') ? this.get('metadata').size : -1)
380
        },
381

    
382
        get_description: function(escape) {
383
            if (escape == undefined) { escape = true };
384
            if (escape) { return this.escape('description') || "No description available"}
385
            return this.get('description') || "No description available."
386
        },
387

    
388
        get_meta: function(key) {
389
            if (this.get('metadata') && this.get('metadata')) {
390
                if (!this.get('metadata')[key]) { return null }
391
                return _.escape(this.get('metadata')[key]);
392
            } else {
393
                return null;
394
            }
395
        },
396

    
397
        get_meta_keys: function() {
398
            if (this.get('metadata') && this.get('metadata')) {
399
                return _.keys(this.get('metadata'));
400
            } else {
401
                return [];
402
            }
403
        },
404

    
405
        get_owner: function() {
406
            return this.get('owner') || _.keys(synnefo.config.system_images_owners)[0];
407
        },
408

    
409
        get_owner_uuid: function() {
410
            return this.get('owner_uuid');
411
        },
412

    
413
        is_system_image: function() {
414
          var owner = this.get_owner();
415
          return _.include(_.keys(synnefo.config.system_images_owners), owner)
416
        },
417

    
418
        owned_by: function(user) {
419
          if (!user) { user = synnefo.user }
420
          return user.get_username() == this.get('owner_uuid');
421
        },
422

    
423
        display_owner: function() {
424
            var owner = this.get_owner();
425
            if (_.include(_.keys(synnefo.config.system_images_owners), owner)) {
426
                return synnefo.config.system_images_owners[owner];
427
            } else {
428
                return owner;
429
            }
430
        },
431
    
432
        get_readable_size: function() {
433
            if (this.is_deleted()) {
434
                return synnefo.config.image_deleted_size_title || '(none)';
435
            }
436
            return this.get_size() > 0 ? util.readablizeBytes(this.get_size() * 1024 * 1024) : '(none)';
437
        },
438

    
439
        get_os: function() {
440
            return this.get_meta('OS');
441
        },
442

    
443
        get_gui: function() {
444
            return this.get_meta('GUI');
445
        },
446

    
447
        get_created_users: function() {
448
            try {
449
              var users = this.get_meta('users').split(" ");
450
            } catch (err) { users = null }
451
            if (!users) {
452
                var osfamily = this.get_meta('osfamily');
453
                if (osfamily == 'windows') { 
454
                  users = ['Administrator'];
455
                } else {
456
                  users = ['root'];
457
                }
458
            }
459
            return users;
460
        },
461

    
462
        get_sort_order: function() {
463
            return parseInt(this.get('metadata') ? this.get('metadata').sortorder : -1)
464
        },
465

    
466
        get_vm: function() {
467
            var vm_id = this.get("serverRef");
468
            var vm = undefined;
469
            vm = storage.vms.get(vm_id);
470
            return vm;
471
        },
472

    
473
        is_public: function() {
474
            return this.get('is_public') == undefined ? true : this.get('is_public');
475
        },
476

    
477
        is_deleted: function() {
478
            return this.get('status') == "DELETED"
479
        },
480
        
481
        ssh_keys_paths: function() {
482
            return _.map(this.get_created_users(), function(username) {
483
                prepend = '';
484
                if (username != 'root') {
485
                    prepend = '/home'
486
                }
487
                return {'user': username, 'path': '{1}/{0}/.ssh/authorized_keys'.format(username, 
488
                                                             prepend)};
489
            });
490
        },
491

    
492
        _supports_ssh: function() {
493
            if (synnefo.config.support_ssh_os_list.indexOf(this.get_os()) > -1) {
494
                return true;
495
            }
496
            if (this.get_meta('osfamily') == 'linux') {
497
              return true;
498
            }
499
            return false;
500
        },
501

    
502
        supports: function(feature) {
503
            if (feature == "ssh") {
504
                return this._supports_ssh()
505
            }
506
            return false;
507
        },
508

    
509
        personality_data_for_keys: function(keys) {
510
            return _.map(this.ssh_keys_paths(), function(pathinfo) {
511
                var contents = '';
512
                _.each(keys, function(key){
513
                    contents = contents + key.get("content") + "\n"
514
                });
515
                contents = $.base64.encode(contents);
516

    
517
                return {
518
                    path: pathinfo.path,
519
                    contents: contents,
520
                    mode: 0600,
521
                    owner: pathinfo.user
522
                }
523
            });
524
        }
525
    });
526

    
527
    // Flavor model
528
    models.Flavor = models.Model.extend({
529
        path: 'flavors',
530

    
531
        details_string: function() {
532
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
533
        },
534

    
535
        get_disk_size: function() {
536
            return parseInt(this.get("disk") * 1000)
537
        },
538

    
539
        get_ram_size: function() {
540
            return parseInt(this.get("ram"))
541
        },
542

    
543
        get_disk_template_info: function() {
544
            var info = snf.config.flavors_disk_templates_info[this.get("disk_template")];
545
            if (!info) {
546
                info = { name: this.get("disk_template"), description:'' };
547
            }
548
            return info
549
        },
550

    
551
        disk_to_bytes: function() {
552
            return parseInt(this.get("disk")) * 1024 * 1024 * 1024;
553
        },
554

    
555
        ram_to_bytes: function() {
556
            return parseInt(this.get("ram")) * 1024 * 1024;
557
        },
558

    
559
    });
560
    
561
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
562
    _.extend(models.ParamsList.prototype, bb.Events, {
563

    
564
        initialize: function(parent, param_name) {
565
            this.parent = parent;
566
            this.actions = {};
567
            this.param_name = param_name;
568
            this.length = 0;
569
        },
570
        
571
        has_action: function(action) {
572
            return this.actions[action] ? true : false;
573
        },
574
            
575
        _parse_params: function(arguments) {
576
            if (arguments.length <= 1) {
577
                return [];
578
            }
579

    
580
            var args = _.toArray(arguments);
581
            return args.splice(1);
582
        },
583

    
584
        contains: function(action, params) {
585
            params = this._parse_params(arguments);
586
            var has_action = this.has_action(action);
587
            if (!has_action) { return false };
588

    
589
            var paramsEqual = false;
590
            _.each(this.actions[action], function(action_params) {
591
                if (_.isEqual(action_params, params)) {
592
                    paramsEqual = true;
593
                }
594
            });
595
                
596
            return paramsEqual;
597
        },
598
        
599
        is_empty: function() {
600
            return _.isEmpty(this.actions);
601
        },
602

    
603
        add: function(action, params) {
604
            params = this._parse_params(arguments);
605
            if (this.contains.apply(this, arguments)) { return this };
606
            var isnew = false
607
            if (!this.has_action(action)) {
608
                this.actions[action] = [];
609
                isnew = true;
610
            };
611

    
612
            this.actions[action].push(params);
613
            this.parent.trigger("change:" + this.param_name, this.parent, this);
614
            if (isnew) {
615
                this.trigger("add", action, params);
616
            } else {
617
                this.trigger("change", action, params);
618
            }
619
            return this;
620
        },
621
        
622
        remove_all: function(action) {
623
            if (this.has_action(action)) {
624
                delete this.actions[action];
625
                this.parent.trigger("change:" + this.param_name, this.parent, this);
626
                this.trigger("remove", action);
627
            }
628
            return this;
629
        },
630

    
631
        reset: function() {
632
            this.actions = {};
633
            this.parent.trigger("change:" + this.param_name, this.parent, this);
634
            this.trigger("reset");
635
            this.trigger("remove");
636
        },
637

    
638
        remove: function(action, params) {
639
            params = this._parse_params(arguments);
640
            if (!this.has_action(action)) { return this };
641
            var index = -1;
642
            _.each(this.actions[action], _.bind(function(action_params) {
643
                if (_.isEqual(action_params, params)) {
644
                    index = this.actions[action].indexOf(action_params);
645
                }
646
            }, this));
647
            
648
            if (index > -1) {
649
                this.actions[action].splice(index, 1);
650
                if (_.isEmpty(this.actions[action])) {
651
                    delete this.actions[action];
652
                }
653
                this.parent.trigger("change:" + this.param_name, this.parent, this);
654
                this.trigger("remove", action, params);
655
            }
656
        }
657

    
658
    });
659

    
660
    // Image model
661
    models.Network = models.Model.extend({
662
        path: 'networks',
663
        has_status: true,
664
        defaults: {'connecting':0},
665
        
666
        initialize: function() {
667
            var ret = models.Network.__super__.initialize.apply(this, arguments);
668
            this.set({"actions": new models.ParamsList(this, "actions")});
669
            this.update_state();
670
            this.bind("change:nics", _.bind(synnefo.storage.nics.update_net_nics, synnefo.storage.nics));
671
            this.bind("change:status", _.bind(this.update_state, this));
672
            return ret;
673
        },
674
        
675
        is_deleted: function() {
676
          return this.get('status') == 'DELETED';
677
        },
678

    
679
        toJSON: function() {
680
            var attrs = _.clone(this.attributes);
681
            attrs.actions = _.clone(this.get("actions").actions);
682
            return attrs;
683
        },
684
        
685
        set_state: function(val) {
686
            if (val == "PENDING" && this.get("state") == "DESTORY") {
687
                return "DESTROY";
688
            }
689
            return val;
690
        },
691

    
692
        update_state: function() {
693
            if (this.get("connecting") > 0) {
694
                this.set({state: "CONNECTING"});
695
                return
696
            }
697
            
698
            if (this.get_nics(function(nic){ return nic.get("removing") == 1}).length > 0) {
699
                this.set({state: "DISCONNECTING"});
700
                return
701
            }   
702
            
703
            if (this.contains_firewalling_nics() > 0) {
704
                this.set({state: "FIREWALLING"});
705
                return
706
            }   
707
            
708
            if (this.get("state") == "DESTROY") { 
709
                this.set({"destroyed":1});
710
            }
711
            
712
            this.set({state:this.get('status')});
713
        },
714

    
715
        is_public: function() {
716
            return this.get("public");
717
        },
718

    
719
        decrease_connecting: function() {
720
            var conn = this.get("connecting");
721
            if (!conn) { conn = 0 };
722
            if (conn > 0) {
723
                conn--;
724
            }
725
            this.set({"connecting": conn});
726
            this.update_state();
727
        },
728

    
729
        increase_connecting: function() {
730
            var conn = this.get("connecting");
731
            if (!conn) { conn = 0 };
732
            conn++;
733
            this.set({"connecting": conn});
734
            this.update_state();
735
        },
736

    
737
        connected_to: function(vm) {
738
            return this.get('linked_to').indexOf(""+vm.id) > -1;
739
        },
740

    
741
        connected_with_nic_id: function(nic_id) {
742
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
743
        },
744

    
745
        get_nics: function(filter) {
746
            var nics = synnefo.storage.nics.filter(function(nic) {
747
                return nic.get('network_id') == this.id;
748
            }, this);
749

    
750
            if (filter) {
751
                return _.filter(nics, filter);
752
            }
753
            return nics;
754
        },
755

    
756
        contains_firewalling_nics: function() {
757
            return this.get_nics(function(n){return n.get('pending_firewall')}).length
758
        },
759

    
760
        call: function(action, params, success, error) {
761
            if (action == "destroy") {
762
                var previous_state = this.get('state');
763
                var previous_status = this.get('status');
764

    
765
                this.set({state:"DESTROY"});
766

    
767
                var _success = _.bind(function() {
768
                    if (success) { success() };
769
                    synnefo.storage.quotas.get('cyclades.network.private').decrease();
770
                }, this);
771
                var _error = _.bind(function() {
772
                    this.set({state: previous_state, status: previous_status})
773
                    if (error) { error() };
774
                }, this);
775

    
776
                this.remove(undefined, _error, _success);
777
            }
778
            
779
            if (action == "disconnect") {
780
                if (this.get("state") == "DESTROY") {
781
                    return;
782
                }
783
                
784
                _.each(params, _.bind(function(nic_id) {
785
                    var nic = snf.storage.nics.get(nic_id);
786
                    this.get("actions").remove("disconnect", nic_id);
787
                    if (nic) {
788
                        this.remove_nic(nic, success, error);
789
                    }
790
                }, this));
791
            }
792
        },
793

    
794
        add_vm: function (vm, callback, error, options) {
795
            var payload = {add:{serverRef:"" + vm.id}};
796
            payload._options = options || {};
797
            return this.api_call(this.api_path() + "/action", "create", 
798
                                 payload,
799
                                 undefined,
800
                                 error,
801
                                 _.bind(function(){
802
                                     //this.vms.add_pending(vm.id);
803
                                     this.increase_connecting();
804
                                     if (callback) {callback()}
805
                                 },this), error);
806
        },
807

    
808
        remove_nic: function (nic, callback, error, options) {
809
            var payload = {remove:{attachment:"" + nic.get("attachment_id")}};
810
            payload._options = options || {};
811
            return this.api_call(this.api_path() + "/action", "create", 
812
                                 payload,
813
                                 undefined,
814
                                 error,
815
                                 _.bind(function(){
816
                                     nic.set({"removing": 1});
817
                                     nic.get_network().update_state();
818
                                     //this.vms.add_pending_for_remove(vm.id);
819
                                     if (callback) {callback()}
820
                                 },this), error);
821
        },
822

    
823
        rename: function(name, callback) {
824
            return this.api_call(this.api_path(), "update", {
825
                network:{name:name}, 
826
                _options:{
827
                    critical: false, 
828
                    error_params:{
829
                        title: "Network action failed",
830
                        ns: "Networks",
831
                        extra_details: {"Network id": this.id}
832
                    }
833
                }}, callback);
834
        },
835

    
836
        get_connectable_vms: function() {
837
            return storage.vms.filter(function(vm){
838
                return !vm.in_error_state() && !vm.is_building() && !vm.is_rebooting();
839
            });
840
        },
841

    
842
        state_message: function() {
843
            if (this.get("state") == "ACTIVE" && !this.is_public()) {
844
                if (this.get("cidr") && this.get("dhcp") == true) {
845
                    return this.get("cidr");
846
                } else {
847
                    return "Private network";
848
                }
849
            }
850
            if (this.get("state") == "ACTIVE" && this.is_public()) {
851
                  return "Public network";
852
            }
853

    
854
            return models.Network.STATES[this.get("state")];
855
        },
856

    
857
        in_progress: function() {
858
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
859
        },
860

    
861
        do_all_pending_actions: function(success, error) {
862
          var params, actions, action_params;
863
          actions = _.clone(this.get("actions").actions);
864
            _.each(actions, _.bind(function(params, action) {
865
                action_params = _.map(actions[action], function(a){ return _.clone(a)});
866
                _.each(action_params, _.bind(function(params) {
867
                    this.call(action, params, success, error);
868
                }, this));
869
            }, this));
870
            this.get("actions").reset();
871
        }
872
    });
873
    
874
    models.Network.STATES = {
875
        'ACTIVE': 'Private network',
876
        'CONNECTING': 'Connecting...',
877
        'DISCONNECTING': 'Disconnecting...',
878
        'FIREWALLING': 'Firewall update...',
879
        'DESTROY': 'Destroying...',
880
        'PENDING': 'Pending...',
881
        'ERROR': 'Error'
882
    }
883

    
884
    models.Network.STATES_TRANSITIONS = {
885
        'CONNECTING': ['ACTIVE'],
886
        'DISCONNECTING': ['ACTIVE'],
887
        'PENDING': ['ACTIVE'],
888
        'FIREWALLING': ['ACTIVE']
889
    }
890

    
891
    // Virtualmachine model
892
    models.VM = models.Model.extend({
893

    
894
        path: 'servers',
895
        has_status: true,
896
        initialize: function(params) {
897
            
898
            this.pending_firewalls = {};
899
            
900
            models.VM.__super__.initialize.apply(this, arguments);
901

    
902
            this.set({state: params.status || "ERROR"});
903
            this.log = new snf.logging.logger("VM " + this.id);
904
            this.pending_action = undefined;
905
            
906
            // init stats parameter
907
            this.set({'stats': undefined}, {silent: true});
908
            // defaults to not update the stats
909
            // each view should handle this vm attribute 
910
            // depending on if it displays stat images or not
911
            this.do_update_stats = false;
912
            
913
            // interval time
914
            // this will dynamicaly change if the server responds that
915
            // images get refreshed on different intervals
916
            this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000;
917
            this.stats_available = false;
918

    
919
            // initialize interval
920
            this.init_stats_intervals(this.stats_update_interval);
921
            
922
            // handle progress message on instance change
923
            this.bind("change", _.bind(this.update_status_message, this));
924
            this.bind("change:task_state", _.bind(this.update_status, this));
925
            // force update of progress message
926
            this.update_status_message(true);
927
            
928
            // default values
929
            this.bind("change:state", _.bind(function(){
930
                if (this.state() == "DESTROY") { 
931
                    this.handle_destroy() 
932
                }
933
            }, this));
934

    
935
            this.bind("change:nics", _.bind(synnefo.storage.nics.update_vm_nics, synnefo.storage.nics));
936
        },
937

    
938
        status: function(st) {
939
            if (!st) { return this.get("status")}
940
            return this.set({status:st});
941
        },
942
        
943
        update_status: function() {
944
            this.set_status(this.get('status'));
945
        },
946

    
947
        set_status: function(st) {
948
            var new_state = this.state_for_api_status(st);
949
            var transition = false;
950

    
951
            if (this.state() != new_state) {
952
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
953
                    transition = this.state();
954
                }
955
            }
956
            
957
            // call it silently to avoid double change trigger
958
            var state = this.state_for_api_status(st);
959
            this.set({'state': state}, {silent: true});
960
            
961
            // trigger transition
962
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
963
                this.trigger("transition", {from:transition, to:new_state}) 
964
            };
965
            return st;
966
        },
967
            
968
        get_diagnostics: function(success) {
969
            this.__make_api_call(this.get_diagnostics_url(),
970
                                 "read", // create so that sync later uses POST to make the call
971
                                 null, // payload
972
                                 function(data) {
973
                                     success(data);
974
                                 },  
975
                                 null, 'diagnostics');
976
        },
977

    
978
        has_diagnostics: function() {
979
            return this.get("diagnostics") && this.get("diagnostics").length;
980
        },
981

    
982
        get_progress_info: function() {
983
            // details about progress message
984
            // contains a list of diagnostic messages
985
            return this.get("status_messages");
986
        },
987

    
988
        get_status_message: function() {
989
            return this.get('status_message');
990
        },
991
        
992
        // extract status message from diagnostics
993
        status_message_from_diagnostics: function(diagnostics) {
994
            var valid_sources_map = synnefo.config.diagnostics_status_messages_map;
995
            var valid_sources = valid_sources_map[this.get('status')];
996
            if (!valid_sources) { return null };
997
            
998
            // filter messsages based on diagnostic source
999
            var messages = _.filter(diagnostics, function(diag) {
1000
                return valid_sources.indexOf(diag.source) > -1;
1001
            });
1002

    
1003
            var msg = messages[0];
1004
            if (msg) {
1005
              var message = msg.message;
1006
              var message_tpl = snf.config.diagnostic_messages_tpls[msg.source];
1007

    
1008
              if (message_tpl) {
1009
                  message = message_tpl.replace('MESSAGE', msg.message);
1010
              }
1011
              return message;
1012
            }
1013
            
1014
            // no message to display, but vm in build state, display
1015
            // finalizing message.
1016
            if (this.is_building() == 'BUILD') {
1017
                return synnefo.config.BUILDING_MESSAGES['FINAL'];
1018
            }
1019
            return null;
1020
        },
1021

    
1022
        update_status_message: function(force) {
1023
            // update only if one of the specified attributes has changed
1024
            if (
1025
              !this.keysChanged(['diagnostics', 'progress', 'status', 'state'])
1026
                && !force
1027
            ) { return };
1028
            
1029
            // if user requested to destroy the vm set the appropriate 
1030
            // message.
1031
            if (this.get('state') == "DESTROY") { 
1032
                message = "Terminating..."
1033
                this.set({status_message: message})
1034
                return;
1035
            }
1036
            
1037
            // set error message, if vm has diagnostic message display it as
1038
            // progress message
1039
            if (this.in_error_state()) {
1040
                var d = this.get('diagnostics');
1041
                if (d && d.length) {
1042
                    var message = this.status_message_from_diagnostics(d);
1043
                    this.set({status_message: message});
1044
                } else {
1045
                    this.set({status_message: null});
1046
                }
1047
                return;
1048
            }
1049
            
1050
            // identify building status message
1051
            if (this.is_building()) {
1052
                var self = this;
1053
                var success = function(msg) {
1054
                    self.set({status_message: msg});
1055
                }
1056
                this.get_building_status_message(success);
1057
                return;
1058
            }
1059

    
1060
            this.set({status_message:null});
1061
        },
1062
            
1063
        // get building status message. Asynchronous function since it requires
1064
        // access to vm image.
1065
        get_building_status_message: function(callback) {
1066
            // no progress is set, vm is in initial build status
1067
            var progress = this.get("progress");
1068
            if (progress == 0 || !progress) {
1069
                return callback(BUILDING_MESSAGES['INIT']);
1070
            }
1071
            
1072
            // vm has copy progress, display copy percentage
1073
            if (progress > 0 && progress <= 99) {
1074
                this.get_copy_details(true, undefined, _.bind(
1075
                    function(details){
1076
                        callback(BUILDING_MESSAGES['COPY'].format(details.copy, 
1077
                                                           details.size, 
1078
                                                           details.progress));
1079
                }, this));
1080
                return;
1081
            }
1082

    
1083
            // copy finished display FINAL message or identify status message
1084
            // from diagnostics.
1085
            if (progress >= 100) {
1086
                if (!this.has_diagnostics()) {
1087
                        callback(BUILDING_MESSAGES['FINAL']);
1088
                } else {
1089
                        var d = this.get("diagnostics");
1090
                        var msg = this.status_message_from_diagnostics(d);
1091
                        if (msg) {
1092
                              callback(msg);
1093
                        }
1094
                }
1095
            }
1096
        },
1097

    
1098
        get_copy_details: function(human, image, callback) {
1099
            var human = human || false;
1100
            var image = image || this.get_image(_.bind(function(image){
1101
                var progress = this.get('progress');
1102
                var size = image.get_size();
1103
                var size_copied = (size * progress / 100).toFixed(2);
1104
                
1105
                if (human) {
1106
                    size = util.readablizeBytes(size*1024*1024);
1107
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
1108
                }
1109

    
1110
                callback({'progress': progress, 'size': size, 'copy': size_copied})
1111
            }, this));
1112
        },
1113

    
1114
        start_stats_update: function(force_if_empty) {
1115
            var prev_state = this.do_update_stats;
1116

    
1117
            this.do_update_stats = true;
1118
            
1119
            // fetcher initialized ??
1120
            if (!this.stats_fetcher) {
1121
                this.init_stats_intervals();
1122
            }
1123

    
1124

    
1125
            // fetcher running ???
1126
            if (!this.stats_fetcher.running || !prev_state) {
1127
                this.stats_fetcher.start();
1128
            }
1129

    
1130
            if (force_if_empty && this.get("stats") == undefined) {
1131
                this.update_stats(true);
1132
            }
1133
        },
1134

    
1135
        stop_stats_update: function(stop_calls) {
1136
            this.do_update_stats = false;
1137

    
1138
            if (stop_calls) {
1139
                this.stats_fetcher.stop();
1140
            }
1141
        },
1142

    
1143
        // clear and reinitialize update interval
1144
        init_stats_intervals: function (interval) {
1145
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
1146
            this.stats_fetcher.start();
1147
        },
1148
        
1149
        get_stats_fetcher: function(timeout) {
1150
            var cb = _.bind(function(data){
1151
                this.update_stats();
1152
            }, this);
1153
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
1154
            return fetcher;
1155
        },
1156

    
1157
        // do the api call
1158
        update_stats: function(force) {
1159
            // do not update stats if flag not set
1160
            if ((!this.do_update_stats && !force) || this.updating_stats) {
1161
                return;
1162
            }
1163

    
1164
            // make the api call, execute handle_stats_update on sucess
1165
            // TODO: onError handler ???
1166
            stats_url = this.url() + "/stats";
1167
            this.updating_stats = true;
1168
            this.sync("read", this, {
1169
                handles_error:true, 
1170
                url: stats_url, 
1171
                refresh:true, 
1172
                success: _.bind(this.handle_stats_update, this),
1173
                error: _.bind(this.handle_stats_error, this),
1174
                complete: _.bind(function(){this.updating_stats = false;}, this),
1175
                critical: false,
1176
                log_error: false,
1177
                skips_timeouts: true
1178
            });
1179
        },
1180

    
1181
        get_stats_image: function(stat, type) {
1182
        },
1183
        
1184
        _set_stats: function(stats) {
1185
            var silent = silent === undefined ? false : silent;
1186
            // unavailable stats while building
1187
            if (this.get("status") == "BUILD") { 
1188
                this.stats_available = false;
1189
            } else { this.stats_available = true; }
1190

    
1191
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
1192
            
1193
            this.set({stats: stats}, {silent:true});
1194
            this.trigger("stats:update", stats);
1195
        },
1196

    
1197
        unbind: function() {
1198
            models.VM.__super__.unbind.apply(this, arguments);
1199
        },
1200

    
1201
        can_resize: function() {
1202
          return this.get('status') == 'STOPPED';
1203
        },
1204

    
1205
        handle_stats_error: function() {
1206
            stats = {};
1207
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1208
                stats[k] = false;
1209
            });
1210

    
1211
            this.set({'stats': stats});
1212
        },
1213

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

    
1225
                function check_images_loaded() {
1226
                    images_loaded++;
1227

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

    
1248
                    img.error(function() {
1249
                        images[stat + type] = false;
1250
                        check_images_loaded();
1251
                    });
1252

    
1253
                    img.attr({'src': stats[k]});
1254
                })
1255
                data.stats = stats;
1256
            }
1257

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

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

    
1279
        require_reboot: function() {
1280
            if (this.is_active()) {
1281
                this.set({'reboot_required': true});
1282
            }
1283
        },
1284
        
1285
        set_pending_action: function(data) {
1286
            this.pending_action = data;
1287
            return data;
1288
        },
1289

    
1290
        // machine has pending action
1291
        update_pending_action: function(action, force) {
1292
            this.set({pending_action: action});
1293
        },
1294

    
1295
        clear_pending_action: function() {
1296
            this.set({pending_action: undefined});
1297
        },
1298

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

    
1317
        in_error_state: function() {
1318
            return this.state() === "ERROR"
1319
        },
1320

    
1321
        // user can connect to machine
1322
        is_connectable: function() {
1323
            // check if ips exist
1324
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
1325
                return false;
1326
            }
1327
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1328
        },
1329
        
1330
        remove_meta: function(key, complete, error) {
1331
            var url = this.api_path() + "/metadata/" + key;
1332
            this.api_call(url, "delete", undefined, complete, error);
1333
        },
1334

    
1335
        save_meta: function(meta, complete, error) {
1336
            var url = this.api_path() + "/metadata/" + meta.key;
1337
            var payload = {meta:{}};
1338
            payload.meta[meta.key] = meta.value;
1339
            payload._options = {
1340
                critical:false, 
1341
                error_params: {
1342
                    title: "Machine metadata error",
1343
                    extra_details: {"Machine id": this.id}
1344
            }};
1345

    
1346
            this.api_call(url, "update", payload, complete, error);
1347
        },
1348

    
1349

    
1350
        // update/get the state of the machine
1351
        state: function() {
1352
            var args = slice.call(arguments);
1353
                
1354
            // TODO: it might not be a good idea to set the state in set_state method
1355
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1356
                this.set({'state': args[0]});
1357
            }
1358

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

    
1409
        get_resize_flavors: function() {
1410
          var vm_flavor = this.get_flavor();
1411
          var flavors = synnefo.storage.flavors.filter(function(f){
1412
              return f.get('disk_template') ==
1413
              vm_flavor.get('disk_template') && f.get('disk') ==
1414
              vm_flavor.get('disk');
1415
          });
1416
          return flavors;
1417
        },
1418

    
1419
        get_flavor_quotas: function() {
1420
          var flavor = this.get_flavor();
1421
          return {
1422
            cpu: flavor.get('cpu') + 1, 
1423
            ram: flavor.get_ram_size() + 1, 
1424
            disk:flavor.get_disk_size() + 1
1425
          }
1426
        },
1427

    
1428
        get_meta: function(key, deflt) {
1429
            if (this.get('metadata') && this.get('metadata')) {
1430
                if (!this.get('metadata')[key]) { return deflt }
1431
                return _.escape(this.get('metadata')[key]);
1432
            } else {
1433
                return deflt;
1434
            }
1435
        },
1436

    
1437
        get_meta_keys: function() {
1438
            if (this.get('metadata') && this.get('metadata')) {
1439
                return _.keys(this.get('metadata'));
1440
            } else {
1441
                return [];
1442
            }
1443
        },
1444
        
1445
        // get metadata OS value
1446
        get_os: function() {
1447
            var image = this.get_image();
1448
            return this.get_meta('OS') || (image ? 
1449
                                            image.get_os() || "okeanos" : "okeanos");
1450
        },
1451

    
1452
        get_gui: function() {
1453
            return this.get_meta('GUI');
1454
        },
1455
        
1456
        connected_to: function(net) {
1457
            return this.get('linked_to').indexOf(net.id) > -1;
1458
        },
1459

    
1460
        connected_with_nic_id: function(nic_id) {
1461
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
1462
        },
1463

    
1464
        get_nics: function(filter) {
1465
            ret = synnefo.storage.nics.filter(function(nic) {
1466
                return parseInt(nic.get('vm_id')) == this.id;
1467
            }, this);
1468

    
1469
            if (filter) {
1470
                return _.filter(ret, filter);
1471
            }
1472

    
1473
            return ret;
1474
        },
1475

    
1476
        get_net_nics: function(net_id) {
1477
            return this.get_nics(function(n){return n.get('network_id') == net_id});
1478
        },
1479

    
1480
        get_public_nic: function() {
1481
            return this.get_nics(function(n){ return n.get_network().is_public() === true })[0];
1482
        },
1483

    
1484
        get_hostname: function() {
1485
          var hostname = this.get_meta('hostname');
1486
          if (!hostname) {
1487
            if (synnefo.config.vm_hostname_format) {
1488
              hostname = synnefo.config.vm_hostname_format.format(this.id);
1489
            } else {
1490
              hostname = this.get_public_nic().get('ipv4');
1491
            }
1492
          }
1493
          return hostname;
1494
        },
1495

    
1496
        get_nic: function(net_id) {
1497
        },
1498

    
1499
        has_firewall: function() {
1500
            var nic = this.get_public_nic();
1501
            if (nic) {
1502
                var profile = nic.get('firewallProfile'); 
1503
                return ['ENABLED', 'PROTECTED'].indexOf(profile) > -1;
1504
            }
1505
            return false;
1506
        },
1507

    
1508
        get_firewall_profile: function() {
1509
            var nic = this.get_public_nic();
1510
            if (nic) {
1511
                return nic.get('firewallProfile');
1512
            }
1513
            return null;
1514
        },
1515

    
1516
        get_addresses: function() {
1517
            var pnic = this.get_public_nic();
1518
            if (!pnic) { return {'ip4': undefined, 'ip6': undefined }};
1519
            return {'ip4': pnic.get('ipv4'), 'ip6': pnic.get('ipv6')};
1520
        },
1521
    
1522
        // get actions that the user can execute
1523
        // depending on the vm state/status
1524
        get_available_actions: function() {
1525
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1526
        },
1527

    
1528
        set_profile: function(profile, net_id) {
1529
        },
1530
        
1531
        // call rename api
1532
        rename: function(new_name) {
1533
            //this.set({'name': new_name});
1534
            this.sync("update", this, {
1535
                critical: true,
1536
                data: {
1537
                    'server': {
1538
                        'name': new_name
1539
                    }
1540
                }, 
1541
                // do the rename after the method succeeds
1542
                success: _.bind(function(){
1543
                    //this.set({name: new_name});
1544
                    snf.api.trigger("call");
1545
                }, this)
1546
            });
1547
        },
1548
        
1549
        get_console_url: function(data) {
1550
            var url_params = {
1551
                machine: this.get("name"),
1552
                host_ip: this.get_addresses().ip4,
1553
                host_ip_v6: this.get_addresses().ip6,
1554
                host: data.host,
1555
                port: data.port,
1556
                password: data.password
1557
            }
1558
            return synnefo.config.ui_console_url + '?' + $.param(url_params);
1559
        },
1560

    
1561
        // action helper
1562
        call: function(action_name, success, error, params) {
1563
            var id_param = [this.id];
1564
            
1565
            params = params || {};
1566
            success = success || function() {};
1567
            error = error || function() {};
1568

    
1569
            var self = this;
1570

    
1571
            switch(action_name) {
1572
                case 'start':
1573
                    this.__make_api_call(this.get_action_url(), // vm actions url
1574
                                         "create", // create so that sync later uses POST to make the call
1575
                                         {start:{}}, // payload
1576
                                         function() {
1577
                                             // set state after successful call
1578
                                             self.state("START"); 
1579
                                             success.apply(this, arguments);
1580
                                             snf.api.trigger("call");
1581
                                         },  
1582
                                         error, 'start', params);
1583
                    break;
1584
                case 'reboot':
1585
                    this.__make_api_call(this.get_action_url(), // vm actions url
1586
                                         "create", // create so that sync later uses POST to make the call
1587
                                         {reboot:{}}, // payload
1588
                                         function() {
1589
                                             // set state after successful call
1590
                                             self.state("REBOOT"); 
1591
                                             success.apply(this, arguments)
1592
                                             snf.api.trigger("call");
1593
                                             self.set({'reboot_required': false});
1594
                                         },
1595
                                         error, 'reboot', params);
1596
                    break;
1597
                case 'shutdown':
1598
                    this.__make_api_call(this.get_action_url(), // vm actions url
1599
                                         "create", // create so that sync later uses POST to make the call
1600
                                         {shutdown:{}}, // payload
1601
                                         function() {
1602
                                             // set state after successful call
1603
                                             self.state("SHUTDOWN"); 
1604
                                             success.apply(this, arguments)
1605
                                             snf.api.trigger("call");
1606
                                         },  
1607
                                         error, 'shutdown', params);
1608
                    break;
1609
                case 'console':
1610
                    this.__make_api_call(this.url() + "/action", "create", 
1611
                                         {'console': {'type':'vnc'}}, 
1612
                                         function(data) {
1613
                        var cons_data = data.console;
1614
                        success.apply(this, [cons_data]);
1615
                    }, undefined, 'console', params)
1616
                    break;
1617
                case 'destroy':
1618
                    this.__make_api_call(this.url(), // vm actions url
1619
                                         "delete", // create so that sync later uses POST to make the call
1620
                                         undefined, // payload
1621
                                         function() {
1622
                                             // set state after successful call
1623
                                             self.state('DESTROY');
1624
                                             success.apply(this, arguments);
1625
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1626

    
1627
                                         },  
1628
                                         error, 'destroy', params);
1629
                    break;
1630
                case 'resize':
1631
                    this.__make_api_call(this.get_action_url(), // vm actions url
1632
                                         "create", // create so that sync later uses POST to make the call
1633
                                         {resize: {flavorRef:params.flavor}}, // payload
1634
                                         function() {
1635
                                             self.state('RESIZE');
1636
                                             success.apply(this, arguments);
1637
                                             snf.api.trigger("call");
1638
                                         },  
1639
                                         error, 'resize', params);
1640
                    break;
1641
                case 'addFloatingIp':
1642
                    this.__make_api_call(this.get_action_url(), // vm actions url
1643
                                         "create", // create so that sync later uses POST to make the call
1644
                                         {addFloatingIp: {address:params.address}}, // payload
1645
                                         function() {
1646
                                             self.state('CONNECT');
1647
                                             success.apply(this, arguments);
1648
                                             snf.api.trigger("call");
1649
                                         },  
1650
                                         error, 'addFloatingIp', params);
1651
                    break;
1652
                case 'removeFloatingIp':
1653
                    this.__make_api_call(this.get_action_url(), // vm actions url
1654
                                         "create", // create so that sync later uses POST to make the call
1655
                                         {removeFloatingIp: {address:params.address}}, // payload
1656
                                         function() {
1657
                                             self.state('DISCONNECT');
1658
                                             success.apply(this, arguments);
1659
                                             snf.api.trigger("call");
1660
                                         },  
1661
                                         error, 'addFloatingIp', params);
1662
                    break;
1663
                case 'destroy':
1664
                    this.__make_api_call(this.url(), // vm actions url
1665
                                         "delete", // create so that sync later uses POST to make the call
1666
                                         undefined, // payload
1667
                                         function() {
1668
                                             // set state after successful call
1669
                                             self.state('DESTROY');
1670
                                             success.apply(this, arguments);
1671
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1672

    
1673
                                         },  
1674
                                         error, 'destroy', params);
1675
                    break;
1676
                default:
1677
                    throw "Invalid VM action ("+action_name+")";
1678
            }
1679
        },
1680
        
1681
        __make_api_call: function(url, method, data, success, error, action, 
1682
                                  extra_params) {
1683
            var self = this;
1684
            error = error || function(){};
1685
            success = success || function(){};
1686

    
1687
            var params = {
1688
                url: url,
1689
                data: data,
1690
                success: function() { 
1691
                  self.handle_action_succeed.apply(self, arguments); 
1692
                  success.apply(this, arguments)
1693
                },
1694
                error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)},
1695
                error_params: { ns: "Machines actions", 
1696
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1697
                                extra_details: {'Machine ID': this.id, 
1698
                                                'URL': url, 
1699
                                                'Action': action || "undefined" },
1700
                                allow_reload: false
1701
                              },
1702
                display: false,
1703
                critical: false
1704
            }
1705
            _.extend(params, extra_params);
1706
            this.sync(method, this, params);
1707
        },
1708

    
1709
        handle_action_succeed: function() {
1710
            this.trigger("action:success", arguments);
1711
        },
1712
        
1713
        reset_action_error: function() {
1714
            this.action_error = false;
1715
            this.trigger("action:fail:reset", this.action_error);
1716
        },
1717

    
1718
        handle_action_fail: function() {
1719
            this.action_error = arguments;
1720
            this.trigger("action:fail", arguments);
1721
        },
1722

    
1723
        get_action_url: function(name) {
1724
            return this.url() + "/action";
1725
        },
1726

    
1727
        get_diagnostics_url: function() {
1728
            return this.url() + "/diagnostics";
1729
        },
1730

    
1731
        get_users: function() {
1732
            var image;
1733
            var users = [];
1734
            try {
1735
              var users = this.get_meta('users').split(" ");
1736
            } catch (err) { users = null }
1737
            if (!users) {
1738
              image = this.get_image();
1739
              if (image) {
1740
                  users = image.get_created_users();
1741
              }
1742
            }
1743
            return users;
1744
        },
1745

    
1746
        get_connection_info: function(host_os, success, error) {
1747
            var url = synnefo.config.ui_connect_url;
1748
            var users = this.get_users();
1749

    
1750
            params = {
1751
                ip_address: this.get_public_nic().get('ipv4'),
1752
                hostname: this.get_hostname(),
1753
                os: this.get_os(),
1754
                host_os: host_os,
1755
                srv: this.id
1756
            }
1757
            
1758
            if (users.length) { 
1759
                params['username'] = _.last(users)
1760
            }
1761

    
1762
            url = url + "?" + $.param(params);
1763

    
1764
            var ajax = snf.api.sync("read", undefined, { url: url, 
1765
                                                         error:error, 
1766
                                                         success:success, 
1767
                                                         handles_error:1});
1768
        }
1769
    })
1770
    
1771
    models.VM.ACTIONS = [
1772
        'start',
1773
        'shutdown',
1774
        'reboot',
1775
        'console',
1776
        'destroy',
1777
        'resize'
1778
    ]
1779

    
1780
    models.VM.TASK_STATE_STATUS_MAP = {
1781
      'BULDING': 'BUILD',
1782
      'REBOOTING': 'REBOOT',
1783
      'STOPPING': 'SHUTDOWN',
1784
      'STARTING': 'START',
1785
      'RESIZING': 'RESIZE',
1786
      'CONNECTING': 'CONNECT',
1787
      'DISCONNECTING': 'DISCONNECT',
1788
      'DESTROYING': 'DESTROY'
1789
    }
1790

    
1791
    models.VM.AVAILABLE_ACTIONS = {
1792
        'UNKNWON'       : ['destroy'],
1793
        'BUILD'         : ['destroy'],
1794
        'REBOOT'        : ['destroy'],
1795
        'STOPPED'       : ['start', 'destroy'],
1796
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1797
        'ERROR'         : ['destroy'],
1798
        'DELETED'       : ['destroy'],
1799
        'DESTROY'       : ['destroy'],
1800
        'SHUTDOWN'      : ['destroy'],
1801
        'START'         : ['destroy'],
1802
        'CONNECT'       : ['destroy'],
1803
        'DISCONNECT'    : ['destroy'],
1804
        'RESIZE'        : ['destroy']
1805
    }
1806

    
1807
    // api status values
1808
    models.VM.STATUSES = [
1809
        'UNKNWON',
1810
        'BUILD',
1811
        'REBOOT',
1812
        'STOPPED',
1813
        'ACTIVE',
1814
        'ERROR',
1815
        'DELETED',
1816
        'RESIZE'
1817
    ]
1818

    
1819
    // api status values
1820
    models.VM.CONNECT_STATES = [
1821
        'ACTIVE',
1822
        'REBOOT',
1823
        'SHUTDOWN'
1824
    ]
1825

    
1826
    // vm states
1827
    models.VM.STATES = models.VM.STATUSES.concat([
1828
        'DESTROY',
1829
        'SHUTDOWN',
1830
        'START',
1831
        'CONNECT',
1832
        'DISCONNECT',
1833
        'FIREWALL',
1834
        'RESIZE'
1835
    ]);
1836
    
1837
    models.VM.STATES_TRANSITIONS = {
1838
        'DESTROY' : ['DELETED'],
1839
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1840
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1841
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1842
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1843
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1844
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1845
        'RESIZE': ['ERROR', 'STOPPED']
1846
    }
1847

    
1848
    models.VM.TRANSITION_STATES = [
1849
        'DESTROY',
1850
        'SHUTDOWN',
1851
        'START',
1852
        'REBOOT',
1853
        'BUILD',
1854
        'RESIZE'
1855
    ]
1856

    
1857
    models.VM.ACTIVE_STATES = [
1858
        'BUILD', 'REBOOT', 'ACTIVE',
1859
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1860
    ]
1861

    
1862
    models.VM.BUILDING_STATES = [
1863
        'BUILD'
1864
    ]
1865

    
1866
    models.Networks = models.Collection.extend({
1867
        model: models.Network,
1868
        path: 'networks',
1869
        details: true,
1870
        //noUpdate: true,
1871
        defaults: {'nics':[],'linked_to':[]},
1872
        
1873
        parse: function (resp, xhr) {
1874
            // FIXME: depricated global var
1875
            if (!resp) { return []};
1876
            var data = _.filter(_.map(resp.networks, _.bind(this.parse_net_api_data, this)),
1877
                               function(e){ return e });
1878
            return data;
1879
        },
1880

    
1881
        add: function() {
1882
            ret = models.Networks.__super__.add.apply(this, arguments);
1883
            // update nics after each network addition
1884
            ret.each(function(r){
1885
                synnefo.storage.nics.update_net_nics(r);
1886
            });
1887
        },
1888

    
1889
        reset_pending_actions: function() {
1890
            this.each(function(net) {
1891
                net.get("actions").reset();
1892
            });
1893
        },
1894

    
1895
        do_all_pending_actions: function() {
1896
            this.each(function(net) {
1897
                net.do_all_pending_actions();
1898
            })
1899
        },
1900

    
1901
        parse_net_api_data: function(data) {
1902
            // append nic metadata
1903
            // net.get('nics') contains a list of vm/index objects 
1904
            // e.g. {'vm_id':12231, 'index':1}
1905
            // net.get('linked_to') contains a list of vms the network is 
1906
            // connected to e.g. [1001, 1002]
1907
            if (data.attachments && data.attachments) {
1908
                data['nics'] = {};
1909
                data['linked_to'] = [];
1910
                _.each(data.attachments, function(nic_id){
1911
                  
1912
                  var vm_id = NIC_REGEX.exec(nic_id)[1];
1913
                  var nic_index = parseInt(NIC_REGEX.exec(nic_id)[2]);
1914

    
1915
                  if (vm_id !== undefined && nic_index !== undefined) {
1916
                      data['nics'][nic_id] = {
1917
                          'vm_id': vm_id, 
1918
                          'index': nic_index, 
1919
                          'id': nic_id
1920
                      };
1921
                      if (data['linked_to'].indexOf(vm_id) == -1) {
1922
                        data['linked_to'].push(vm_id);
1923
                      }
1924
                  }
1925
                });
1926
            }
1927

    
1928
            if (data.status == "DELETED" && !this.get(parseInt(data.id))) {
1929
              return false;
1930
            }
1931
            return data;
1932
        },
1933

    
1934
        create: function (name, type, cidr, dhcp, callback) {
1935
            var params = {
1936
                network:{
1937
                    name:name
1938
                }
1939
            };
1940

    
1941
            if (!type) {
1942
                throw "Network type cannot be empty";
1943
            }
1944
            params.network.type = type;
1945
            if (cidr) {
1946
                params.network.cidr = cidr;
1947
            }
1948
            if (dhcp) {
1949
                params.network.dhcp = dhcp;
1950
            }
1951

    
1952
            if (dhcp === false) {
1953
                params.network.dhcp = false;
1954
            }
1955
            
1956
            var cb = function() {
1957
              callback();
1958
              synnefo.storage.quotas.get('cyclades.network.private').increase();
1959
            }
1960
            return this.api_call(this.path, "create", params, cb);
1961
        },
1962

    
1963
        get_public: function(){
1964
          return this.filter(function(n){return n.get('public')});
1965
        }
1966
    })
1967

    
1968
    models.Images = models.Collection.extend({
1969
        model: models.Image,
1970
        path: 'images',
1971
        details: true,
1972
        noUpdate: true,
1973
        supportIncUpdates: false,
1974
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1975
        meta_labels: {},
1976
        read_method: 'read',
1977

    
1978
        // update collection model with id passed
1979
        // making a direct call to the image
1980
        // api url
1981
        update_unknown_id: function(id, callback) {
1982
            var url = getUrl.call(this) + "/" + id;
1983
            this.api_call(this.path + "/" + id, this.read_method, {
1984
              _options:{
1985
                async:true, 
1986
                skip_api_error:true}
1987
              }, undefined, 
1988
            _.bind(function() {
1989
                if (!this.get(id)) {
1990
                            if (this.fallback_service) {
1991
                        // if current service has fallback_service attribute set
1992
                        // use this service to retrieve the missing image model
1993
                        var tmpservice = new this.fallback_service();
1994
                        tmpservice.update_unknown_id(id, _.bind(function(img){
1995
                            img.attributes.status = "DELETED";
1996
                            this.add(img.attributes);
1997
                            callback(this.get(id));
1998
                        }, this));
1999
                    } else {
2000
                        var title = synnefo.config.image_deleted_title || 'Deleted';
2001
                        // else add a dummy DELETED state image entry
2002
                        this.add({id:id, name:title, size:-1, 
2003
                                  progress:100, status:"DELETED"});
2004
                        callback(this.get(id));
2005
                    }   
2006
                } else {
2007
                    callback(this.get(id));
2008
                }
2009
            }, this), _.bind(function(image, msg, xhr) {
2010
                if (!image) {
2011
                    var title = synnefo.config.image_deleted_title || 'Deleted';
2012
                    this.add({id:id, name:title, size:-1, 
2013
                              progress:100, status:"DELETED"});
2014
                    callback(this.get(id));
2015
                    return;
2016
                }
2017
                var img_data = this._read_image_from_request(image, msg, xhr);
2018
                this.add(img_data);
2019
                callback(this.get(id));
2020
            }, this));
2021
        },
2022

    
2023
        _read_image_from_request: function(image, msg, xhr) {
2024
            return image.image;
2025
        },
2026

    
2027
        parse: function (resp, xhr) {
2028
            var parsed = _.map(resp.images, _.bind(this.parse_meta, this));
2029
            parsed = this.fill_owners(parsed);
2030
            return parsed;
2031
        },
2032

    
2033
        fill_owners: function(images) {
2034
            // do translate uuid->displayname if needed
2035
            // store display name in owner attribute for compatibility
2036
            var uuids = [];
2037

    
2038
            var images = _.map(images, function(img, index) {
2039
                if (synnefo.config.translate_uuids) {
2040
                    uuids.push(img['owner']);
2041
                }
2042
                img['owner_uuid'] = img['owner'];
2043
                return img;
2044
            });
2045
            
2046
            if (uuids.length > 0) {
2047
                var handle_results = function(data) {
2048
                    _.each(images, function (img) {
2049
                        img['owner'] = data.uuid_catalog[img['owner_uuid']];
2050
                    });
2051
                }
2052
                // notice the async false
2053
                var uuid_map = this.translate_uuids(uuids, false, 
2054
                                                    handle_results)
2055
            }
2056
            return images;
2057
        },
2058

    
2059
        translate_uuids: function(uuids, async, cb) {
2060
            var url = synnefo.config.user_catalog_url;
2061
            var data = JSON.stringify({'uuids': uuids});
2062
          
2063
            // post to user_catalogs api
2064
            snf.api.sync('create', undefined, {
2065
                url: url,
2066
                data: data,
2067
                async: async,
2068
                success:  cb
2069
            });
2070
        },
2071

    
2072
        get_meta_key: function(img, key) {
2073
            if (img.metadata && img.metadata && img.metadata[key]) {
2074
                return _.escape(img.metadata[key]);
2075
            }
2076
            return undefined;
2077
        },
2078

    
2079
        comparator: function(img) {
2080
            return -img.get_sort_order("sortorder") || 1000 * img.id;
2081
        },
2082

    
2083
        parse_meta: function(img) {
2084
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
2085
                if (img[key]) { return };
2086
                img[key] = this.get_meta_key(img, key) || "";
2087
            }, this));
2088
            return img;
2089
        },
2090

    
2091
        active: function() {
2092
            return this.filter(function(img){return img.get('status') != "DELETED"});
2093
        },
2094

    
2095
        predefined: function() {
2096
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
2097
        },
2098
        
2099
        fetch_for_type: function(type, complete, error) {
2100
            this.fetch({update:true, 
2101
                        success: complete, 
2102
                        error: error, 
2103
                        skip_api_error: true });
2104
        },
2105
        
2106
        get_images_for_type: function(type) {
2107
            if (this['get_{0}_images'.format(type)]) {
2108
                return this['get_{0}_images'.format(type)]();
2109
            }
2110

    
2111
            return this.active();
2112
        },
2113

    
2114
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
2115
            var load = false;
2116
            error = onError || function() {};
2117
            function complete(collection) { 
2118
                onComplete(collection.get_images_for_type(type)); 
2119
            }
2120
            
2121
            // do we need to fetch/update current collection entries
2122
            if (load) {
2123
                onStart();
2124
                this.fetch_for_type(type, complete, error);
2125
            } else {
2126
                // fallback to complete
2127
                complete(this);
2128
            }
2129
        }
2130
    })
2131

    
2132
    models.Flavors = models.Collection.extend({
2133
        model: models.Flavor,
2134
        path: 'flavors',
2135
        details: true,
2136
        noUpdate: true,
2137
        supportIncUpdates: false,
2138
        // update collection model with id passed
2139
        // making a direct call to the flavor
2140
        // api url
2141
        update_unknown_id: function(id, callback) {
2142
            var url = getUrl.call(this) + "/" + id;
2143
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
2144
            _.bind(function() {
2145
                this.add({id:id, cpu:"Unknown", ram:"Unknown", disk:"Unknown", name: "Unknown", status:"DELETED"})
2146
            }, this), _.bind(function(flv) {
2147
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
2148
                this.add(flv.flavor);
2149
            }, this));
2150
        },
2151

    
2152
        parse: function (resp, xhr) {
2153
            return _.map(resp.flavors, function(o) {
2154
              o.cpu = o['vcpus'];
2155
              o.disk_template = o['SNF:disk_template'];
2156
              return o
2157
            });
2158
        },
2159

    
2160
        comparator: function(flv) {
2161
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
2162
        },
2163
          
2164
        unavailable_values_for_quotas: function(quotas, flavors, extra) {
2165
            var flavors = flavors || this.active();
2166
            var index = {cpu:[], disk:[], ram:[]};
2167
            var extra = extra == undefined ? {cpu:0, disk:0, ram:0} : extra;
2168
            
2169
            _.each(flavors, function(el) {
2170

    
2171
                var disk_available = quotas['disk'] + extra.disk;
2172
                var disk_size = el.get_disk_size();
2173
                if (index.disk.indexOf(disk_size) == -1) {
2174
                  var disk = el.disk_to_bytes();
2175
                  if (disk > disk_available) {
2176
                    index.disk.push(disk_size);
2177
                  }
2178
                }
2179
                
2180
                var ram_available = quotas['ram'] + extra.ram * 1024 * 1024;
2181
                var ram_size = el.get_ram_size();
2182
                if (index.ram.indexOf(ram_size) == -1) {
2183
                  var ram = el.ram_to_bytes();
2184
                  if (ram > ram_available) {
2185
                    index.ram.push(el.get('ram'))
2186
                  }
2187
                }
2188

    
2189
                var cpu = el.get('cpu');
2190
                var cpu_available = quotas['cpu'] + extra.cpu;
2191
                if (index.cpu.indexOf(cpu) == -1) {
2192
                  if (cpu > cpu_available) {
2193
                    index.cpu.push(el.get('cpu'))
2194
                  }
2195
                }
2196
            });
2197
            return index;
2198
        },
2199

    
2200
        unavailable_values_for_image: function(img, flavors) {
2201
            var flavors = flavors || this.active();
2202
            var size = img.get_size();
2203
            
2204
            var index = {cpu:[], disk:[], ram:[]};
2205

    
2206
            _.each(this.active(), function(el) {
2207
                var img_size = size;
2208
                var flv_size = el.get_disk_size();
2209
                if (flv_size < img_size) {
2210
                    if (index.disk.indexOf(flv_size) == -1) {
2211
                        index.disk.push(flv_size);
2212
                    }
2213
                };
2214
            });
2215
            
2216
            return index;
2217
        },
2218

    
2219
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
2220
            if (!filter_list) { filter_list = this.models };
2221
            
2222
            return this.select(function(flv){
2223
                if (flv.get("cpu") == cpu + "" &&
2224
                   flv.get("ram") == mem + "" &&
2225
                   flv.get("disk") == disk + "" &&
2226
                   flv.get("disk_template") == disk_template &&
2227
                   filter_list.indexOf(flv) > -1) { return true; }
2228
            })[0];
2229
        },
2230
        
2231
        get_data: function(lst) {
2232
            var data = {'cpu': [], 'mem':[], 'disk':[], 'disk_template':[]};
2233

    
2234
            _.each(lst, function(flv) {
2235
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
2236
                    data.cpu.push(flv.get("cpu"));
2237
                }
2238
                if (data.mem.indexOf(flv.get("ram")) == -1) {
2239
                    data.mem.push(flv.get("ram"));
2240
                }
2241
                if (data.disk.indexOf(flv.get("disk")) == -1) {
2242
                    data.disk.push(flv.get("disk"));
2243
                }
2244
                if (data.disk_template.indexOf(flv.get("disk_template")) == -1) {
2245
                    data.disk_template.push(flv.get("disk_template"));
2246
                }
2247
            })
2248
            
2249
            return data;
2250
        },
2251

    
2252
        active: function() {
2253
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
2254
        }
2255
            
2256
    })
2257

    
2258
    models.VMS = models.Collection.extend({
2259
        model: models.VM,
2260
        path: 'servers',
2261
        details: true,
2262
        copy_image_meta: true,
2263

    
2264
        parse: function (resp, xhr) {
2265
            var data = resp;
2266
            if (!resp) { return [] };
2267
            data = _.filter(_.map(resp.servers, _.bind(this.parse_vm_api_data, this)), function(v){return v});
2268
            return data;
2269
        },
2270

    
2271
        parse_vm_api_data: function(data) {
2272
            // do not add non existing DELETED entries
2273
            if (data.status && data.status == "DELETED") {
2274
                if (!this.get(data.id)) {
2275
                    return false;
2276
                }
2277
            }
2278
            
2279
            if ('SNF:task_state' in data) { 
2280
                data['task_state'] = data['SNF:task_state'];
2281
                if (data['task_state']) {
2282
                    var status = models.VM.TASK_STATE_STATUS_MAP[data['task_state']];
2283
                    if (status) { data['status'] = status }
2284
                }
2285
            }
2286

    
2287
            // OS attribute
2288
            if (this.has_meta(data)) {
2289
                data['OS'] = data.metadata.OS || snf.config.unknown_os;
2290
            }
2291
            
2292
            if (!data.diagnostics) {
2293
                data.diagnostics = [];
2294
            }
2295

    
2296
            // network metadata
2297
            data['firewalls'] = {};
2298
            data['nics'] = {};
2299
            data['linked_to'] = [];
2300

    
2301
            if (data['attachments'] && data['attachments']) {
2302
                var nics = data['attachments'];
2303
                _.each(nics, function(nic) {
2304
                    var net_id = nic.network_id;
2305
                    var index = parseInt(NIC_REGEX.exec(nic.id)[2]);
2306
                    if (data['linked_to'].indexOf(net_id) == -1) {
2307
                        data['linked_to'].push(net_id);
2308
                    }
2309

    
2310
                    data['nics'][nic.id] = nic;
2311
                })
2312
            }
2313
            
2314
            // if vm has no metadata, no metadata object
2315
            // is in json response, reset it to force
2316
            // value update
2317
            if (!data['metadata']) {
2318
                data['metadata'] = {};
2319
            }
2320
            
2321
            // v2.0 API returns objects
2322
            data.image_obj = data.image;
2323
            data.image = data.image_obj.id;
2324
            data.flavor_obj = data.flavor;
2325
            data.flavor = data.flavor_obj.id;
2326

    
2327
            return data;
2328
        },
2329

    
2330
        add: function() {
2331
            ret = models.VMS.__super__.add.apply(this, arguments);
2332
            ret.each(function(r){
2333
                synnefo.storage.nics.update_vm_nics(r);
2334
            });
2335
        },
2336
        
2337
        get_reboot_required: function() {
2338
            return this.filter(function(vm){return vm.get("reboot_required") == true})
2339
        },
2340

    
2341
        has_pending_actions: function() {
2342
            return this.filter(function(vm){return vm.pending_action}).length > 0;
2343
        },
2344

    
2345
        reset_pending_actions: function() {
2346
            this.each(function(vm) {
2347
                vm.clear_pending_action();
2348
            })
2349
        },
2350

    
2351
        do_all_pending_actions: function(success, error) {
2352
            this.each(function(vm) {
2353
                if (vm.has_pending_action()) {
2354
                    vm.call(vm.pending_action, success, error);
2355
                    vm.clear_pending_action();
2356
                }
2357
            })
2358
        },
2359
        
2360
        do_all_reboots: function(success, error) {
2361
            this.each(function(vm) {
2362
                if (vm.get("reboot_required")) {
2363
                    vm.call("reboot", success, error);
2364
                }
2365
            });
2366
        },
2367

    
2368
        reset_reboot_required: function() {
2369
            this.each(function(vm) {
2370
                vm.set({'reboot_required': undefined});
2371
            })
2372
        },
2373
        
2374
        stop_stats_update: function(exclude) {
2375
            var exclude = exclude || [];
2376
            this.each(function(vm) {
2377
                if (exclude.indexOf(vm) > -1) {
2378
                    return;
2379
                }
2380
                vm.stop_stats_update();
2381
            })
2382
        },
2383
        
2384
        has_meta: function(vm_data) {
2385
            return vm_data.metadata && vm_data.metadata
2386
        },
2387

    
2388
        has_addresses: function(vm_data) {
2389
            return vm_data.metadata && vm_data.metadata
2390
        },
2391

    
2392
        create: function (name, image, flavor, meta, extra, callback) {
2393

    
2394
            if (this.copy_image_meta) {
2395
                if (synnefo.config.vm_image_common_metadata) {
2396
                    _.each(synnefo.config.vm_image_common_metadata, 
2397
                        function(key){
2398
                            if (image.get_meta(key)) {
2399
                                meta[key] = image.get_meta(key);
2400
                            }
2401
                    });
2402
                }
2403

    
2404
                if (image.get("OS")) {
2405
                    meta['OS'] = image.get("OS");
2406
                }
2407
            }
2408
            
2409
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, 
2410
                    metadata:meta}
2411
            opts = _.extend(opts, extra);
2412
            
2413
            var cb = function(data) {
2414
              synnefo.storage.quotas.get('cyclades.vm').increase();
2415
              callback(data);
2416
            }
2417

    
2418
            this.api_call(this.path, "create", {'server': opts}, undefined, 
2419
                          undefined, cb, {critical: true});
2420
        },
2421

    
2422
        load_missing_images: function(callback) {
2423
          var missing_ids = [];
2424
          var resolved = 0;
2425

    
2426
          // fill missing_ids
2427
          this.each(function(el) {
2428
            var imgid = el.get("image");
2429
            var existing = synnefo.storage.images.get(imgid);
2430
            if (!existing && missing_ids.indexOf(imgid) == -1) {
2431
              missing_ids.push(imgid);
2432
            }
2433
          });
2434
          var check = function() {
2435
            // once all missing ids where resolved continue calling the 
2436
            // callback
2437
            resolved++;
2438
            if (resolved == missing_ids.length) {
2439
              callback(missing_ids)
2440
            }
2441
          }
2442
          if (missing_ids.length == 0) {
2443
            callback(missing_ids);
2444
            return;
2445
          }
2446
          // start resolving missing image ids
2447
          _(missing_ids).each(function(imgid){
2448
            synnefo.storage.images.update_unknown_id(imgid, check);
2449
          });
2450
        },
2451

    
2452
        get_connectable: function() {
2453
            return storage.vms.filter(function(vm){
2454
                return !vm.in_error_state() && !vm.is_building();
2455
            });
2456
        }
2457
    })
2458
    
2459
    models.NIC = models.Model.extend({
2460
        
2461
        initialize: function() {
2462
            models.NIC.__super__.initialize.apply(this, arguments);
2463
            this.pending_for_firewall = false;
2464
            this.bind("change:firewallProfile", _.bind(this.check_firewall, this));
2465
            this.bind("change:pending_firewall", function(nic) {
2466
                nic.get_network().update_state();
2467
            });
2468
            this.get_vm().bind("remove", function(){
2469
                try {
2470
                    this.collection.remove(this);
2471
                } catch (err) {};
2472
            }, this);
2473
            this.get_network().bind("remove", function(){
2474
                try {
2475
                    this.collection.remove(this);
2476
                } catch (err) {};
2477
            }, this);
2478

    
2479
        },
2480

    
2481
        get_vm: function() {
2482
            return synnefo.storage.vms.get(parseInt(this.get('vm_id')));
2483
        },
2484

    
2485
        get_network: function() {
2486
            return synnefo.storage.networks.get(this.get('network_id'));
2487
        },
2488

    
2489
        get_v6_address: function() {
2490
            return this.get("ipv6");
2491
        },
2492

    
2493
        get_v4_address: function() {
2494
            return this.get("ipv4");
2495
        },
2496

    
2497
        set_firewall: function(value, callback, error, options) {
2498
            var net_id = this.get('network_id');
2499
            var self = this;
2500

    
2501
            // api call data
2502
            var payload = {"firewallProfile":{"profile":value}};
2503
            payload._options = _.extend({critical: false}, options);
2504
            
2505
            this.set({'pending_firewall': value});
2506
            this.set({'pending_firewall_sending': true});
2507
            this.set({'pending_firewall_from': this.get('firewallProfile')});
2508

    
2509
            var success_cb = function() {
2510
                if (callback) {
2511
                    callback();
2512
                }
2513
                self.set({'pending_firewall_sending': false});
2514
            };
2515

    
2516
            var error_cb = function() {
2517
                self.reset_pending_firewall();
2518
            }
2519
            
2520
            this.get_vm().api_call(this.get_vm().api_path() + "/action", "create", payload, success_cb, error_cb);
2521
        },
2522

    
2523
        reset_pending_firewall: function() {
2524
            this.set({'pending_firewall': false});
2525
            this.set({'pending_firewall': false});
2526
        },
2527

    
2528
        check_firewall: function() {
2529
            var firewall = this.get('firewallProfile');
2530
            var pending = this.get('pending_firewall');
2531
            var previous = this.get('pending_firewall_from');
2532
            if (previous != firewall) { this.get_vm().require_reboot() };
2533
            this.reset_pending_firewall();
2534
        }
2535
        
2536
    });
2537

    
2538
    models.NICs = models.Collection.extend({
2539
        model: models.NIC,
2540
        
2541
        add_or_update: function(nic_id, data, vm) {
2542
            var params = _.clone(data);
2543
            var vm;
2544
            params.attachment_id = params.id;
2545
            params.id = params.id + '-' + params.network_id;
2546
            params.vm_id = parseInt(NIC_REGEX.exec(nic_id)[1]);
2547

    
2548
            if (!this.get(params.id)) {
2549
                this.add(params);
2550
                var nic = this.get(params.id);
2551
                vm = nic.get_vm();
2552
                nic.get_network().decrease_connecting();
2553
                nic.bind("remove", function() {
2554
                    nic.set({"removing": 0});
2555
                    if (this.get_network()) {
2556
                        // network might got removed before nic
2557
                        nic.get_network().update_state();
2558
                    }
2559
                });
2560

    
2561
            } else {
2562
                this.get(params.id).set(params);
2563
                vm = this.get(params.id).get_vm();
2564
            }
2565
            
2566
            // vm nics changed, trigger vm update
2567
            if (vm) { vm.trigger("change", vm)};
2568
        },
2569
        
2570
        reset_nics: function(nics, filter_attr, filter_val) {
2571
            var nics_to_check = this.filter(function(nic) {
2572
                return nic.get(filter_attr) == filter_val;
2573
            });
2574
            
2575
            _.each(nics_to_check, function(nic) {
2576
                if (nics.indexOf(nic.get('id')) == -1) {
2577
                    this.remove(nic);
2578
                } else {
2579
                }
2580
            }, this);
2581
        },
2582

    
2583
        update_vm_nics: function(vm) {
2584
            var nics = vm.get('nics');
2585
            this.reset_nics(_.map(nics, function(nic, key){
2586
                return key + "-" + nic.network_id;
2587
            }), 'vm_id', vm.id);
2588

    
2589
            _.each(nics, function(val, key) {
2590
                var net = synnefo.storage.networks.get(val.network_id);
2591
                if (net && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
2592
                    this.add_or_update(key, vm.get('nics')[key], vm);
2593
                }
2594
            }, this);
2595
        },
2596

    
2597
        update_net_nics: function(net) {
2598
            var nics = net.get('nics');
2599
            this.reset_nics(_.map(nics, function(nic, key){
2600
                return key + "-" + net.get('id');
2601
            }), 'network_id', net.id);
2602

    
2603
            _.each(nics, function(val, key) {
2604
                var vm = synnefo.storage.vms.get(val.vm_id);
2605
                if (vm && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
2606
                    this.add_or_update(key, vm.get('nics')[key], vm);
2607
                }
2608
            }, this);
2609
        }
2610
    });
2611

    
2612
    models.PublicKey = models.Model.extend({
2613
        path: 'keys',
2614
        api_type: 'userdata',
2615
        details: false,
2616
        noUpdate: true,
2617

    
2618

    
2619
        get_public_key: function() {
2620
            return cryptico.publicKeyFromString(this.get("content"));
2621
        },
2622

    
2623
        get_filename: function() {
2624
            return "{0}.pub".format(this.get("name"));
2625
        },
2626

    
2627
        identify_type: function() {
2628
            try {
2629
                var cont = snf.util.validatePublicKey(this.get("content"));
2630
                var type = cont.split(" ")[0];
2631
                return synnefo.util.publicKeyTypesMap[type];
2632
            } catch (err) { return false };
2633
        }
2634

    
2635
    })
2636
    
2637
    models._ActionsModel = models.Model.extend({
2638
      defaults: { pending: null },
2639
      actions: [],
2640
      status: {
2641
        INACTIVE: 0,
2642
        PENDING: 1,
2643
        CALLED: 2
2644
      },
2645

    
2646
      initialize: function(attrs, opts) {
2647
        models._ActionsModel.__super__.initialize.call(this, attrs);
2648
        this.actions = opts.actions;
2649
        this.model = opts.model;
2650
        this.bind("change", function() {
2651
          this.set({'pending': this.get_pending()});
2652
        }, this);
2653
        this.clear();
2654
      },
2655
      
2656
      _in_status: function(st) {
2657
        var actions = null;
2658
        _.each(this.attributes, function(status, action){
2659
          if (status == st) {
2660
            if (!actions) {
2661
              actions = []
2662
            }
2663
            actions.push(action);
2664
          }
2665
        });
2666
        return actions;
2667
      },
2668

    
2669
      get_pending: function() {
2670
        return this._in_status(this.status.PENDING);
2671
      },
2672

    
2673
      unset_pending_action: function(action) {
2674
        var data = {};
2675
        data[action] = this.status.INACTIVE;
2676
        this.set(data);
2677
      },
2678

    
2679
      set_pending_action: function(action, reset_pending) {
2680
        reset_pending = reset_pending === undefined ? true : reset_pending;
2681
        var data = {};
2682
        data[action] = this.status.PENDING;
2683
        if (reset_pending) {
2684
          this.reset_pending();
2685
        }
2686
        this.set(data);
2687
      },
2688
      
2689
      reset_pending: function() {
2690
        var data = {};
2691
        _.each(this.actions, function(action) {
2692
          data[action] = this.status.INACTIVE;
2693
        }, this);
2694
        this.set(data);
2695
      }
2696
    });
2697

    
2698
    models.PublicPool = models.Model.extend({});
2699
    models.PublicPools = models.Collection.extend({
2700
      model: models.PublicPool,
2701
      path: 'os-floating-ip-pools',
2702
      api_type: 'compute',
2703
      noUpdate: true,
2704

    
2705
      parse: function(data) {
2706
        return _.map(data.floating_ip_pools, function(pool) {
2707
          pool.id = pool.name;
2708
          return pool;
2709
        });
2710
      }
2711
    });
2712

    
2713
    models.PublicIP = models.Model.extend({
2714
        path: 'os-floating-ips',
2715
        has_status: false,
2716
        
2717
        initialize: function() {
2718
            models.PublicIP.__super__.initialize.apply(this, arguments);
2719
            this.bind('change:instance_id', _.bind(this.handle_status_change, this));
2720
        },
2721

    
2722
        handle_status_change: function() {
2723
            this.set({state: null});
2724
        },
2725

    
2726
        get_vm: function() {
2727
            if (this.get('instance_id')) {
2728
                return synnefo.storage.vms.get(parseInt(this.get('instance_id')));
2729
            }
2730
            return null;
2731
        },
2732

    
2733
        connect_to: function(vm) {
2734
        }
2735
    });
2736

    
2737
    models.PublicIPs = models.Collection.extend({
2738
        model: models.PublicIP,
2739
        path: 'os-floating-ips',
2740
        api_type: 'compute',
2741
        noUpdate: true,
2742

    
2743
        parse: function(resp) {
2744
            resp = _.map(resp.floating_ips, function(ip) {
2745
              return ip;
2746
            });
2747

    
2748
            return resp;
2749
        }
2750
    });
2751

    
2752
    models.PublicKeys = models.Collection.extend({
2753
        model: models.PublicKey,
2754
        details: false,
2755
        path: 'keys',
2756
        api_type: 'userdata',
2757
        noUpdate: true,
2758

    
2759
        generate_new: function(success, error) {
2760
            snf.api.sync('create', undefined, {
2761
                url: getUrl.call(this, this.base_url) + "/generate", 
2762
                success: success, 
2763
                error: error,
2764
                skip_api_error: true
2765
            });
2766
        },
2767

    
2768
        add_crypto_key: function(key, success, error, options) {
2769
            var options = options || {};
2770
            var m = new models.PublicKey();
2771

    
2772
            // guess a name
2773
            var name_tpl = "my generated public key";
2774
            var name = name_tpl;
2775
            var name_count = 1;
2776
            
2777
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
2778
                name = name_tpl + " " + name_count;
2779
                name_count++;
2780
            }
2781
            
2782
            m.set({name: name});
2783
            m.set({content: key});
2784
            
2785
            options.success = function () { return success(m) };
2786
            options.errror = error;
2787
            options.skip_api_error = true;
2788
            
2789
            this.create(m.attributes, options);
2790
        }
2791
    });
2792

    
2793
  
2794
    models.Quota = models.Model.extend({
2795

    
2796
        initialize: function() {
2797
            models.Quota.__super__.initialize.apply(this, arguments);
2798
            this.bind("change", this.check, this);
2799
            this.check();
2800
        },
2801
        
2802
        check: function() {
2803
            var usage, limit;
2804
            usage = this.get('usage');
2805
            limit = this.get('limit');
2806
            if (usage >= limit) {
2807
                this.trigger("available");
2808
            } else {
2809
                this.trigger("unavailable");
2810
            }
2811
        },
2812

    
2813
        increase: function(val) {
2814
            if (val === undefined) { val = 1};
2815
            this.set({'usage': this.get('usage') + val})
2816
        },
2817

    
2818
        decrease: function(val) {
2819
            if (val === undefined) { val = 1};
2820
            this.set({'usage': this.get('usage') - val})
2821
        },
2822

    
2823
        can_consume: function() {
2824
            var usage, limit;
2825
            usage = this.get('usage');
2826
            limit = this.get('limit');
2827
            if (usage >= limit) {
2828
                return false
2829
            } else {
2830
                return true
2831
            }
2832
        },
2833
        
2834
        is_bytes: function() {
2835
            return this.get('resource').get('unit') == 'bytes';
2836
        },
2837
        
2838
        get_available: function(active) {
2839
            suffix = '';
2840
            if (active) { suffix = '_active'}
2841
            var value = this.get('limit'+suffix) - this.get('usage'+suffix);
2842
            if (active) {
2843
              if (this.get('available') <= value) {
2844
                value = this.get('available');
2845
              }
2846
            }
2847
            if (value < 0) { return value }
2848
            return value
2849
        },
2850

    
2851
        get_readable: function(key, active) {
2852
            var value;
2853
            if (key == 'available') {
2854
                value = this.get_available(active);
2855
            } else {
2856
                value = this.get(key)
2857
            }
2858
            if (value <= 0) { value = 0 }
2859
            if (!this.is_bytes()) {
2860
              return value + "";
2861
            }
2862
            return snf.util.readablizeBytes(value);
2863
        }
2864
    });
2865

    
2866
    models.Quotas = models.Collection.extend({
2867
        model: models.Quota,
2868
        api_type: 'accounts',
2869
        path: 'quotas',
2870
        parse: function(resp) {
2871
            filtered = _.map(resp.system, function(value, key) {
2872
                var available = (value.limit - value.usage) || 0;
2873
                var available_active = available;
2874
                var keysplit = key.split(".");
2875
                var limit_active = value.limit;
2876
                var usage_active = value.usage;
2877
                keysplit[keysplit.length-1] = "active_" + keysplit[keysplit.length-1];
2878
                var activekey = keysplit.join(".");
2879
                var exists = resp.system[activekey];
2880
                if (exists) {
2881
                    available_active = exists.limit - exists.usage;
2882
                    limit_active = exists.limit;
2883
                    usage_active = exists.usage;
2884
                }
2885
                return _.extend(value, {'name': key, 'id': key, 
2886
                          'available': available,
2887
                          'available_active': available_active,
2888
                          'limit_active': limit_active,
2889
                          'usage_active': usage_active,
2890
                          'resource': snf.storage.resources.get(key)});
2891
            });
2892
            return filtered;
2893
        },
2894
        
2895
        get_by_id: function(k) {
2896
          return this.filter(function(q) { return q.get('name') == k})[0]
2897
        },
2898

    
2899
        get_available_for_vm: function(options) {
2900
          var quotas = synnefo.storage.quotas;
2901
          var key = 'available';
2902
          var available_quota = {};
2903
          _.each(['cyclades.ram', 'cyclades.cpu', 'cyclades.disk'], 
2904
            function (key) {
2905
              var value = quotas.get(key).get_available(true);
2906
              available_quota[key.replace('cyclades.', '')] = value;
2907
          });
2908
          return available_quota;
2909
        }
2910
    })
2911

    
2912
    models.Resource = models.Model.extend({
2913
        api_type: 'accounts',
2914
        path: 'resources'
2915
    });
2916

    
2917
    models.Resources = models.Collection.extend({
2918
        api_type: 'accounts',
2919
        path: 'resources',
2920
        model: models.Network,
2921

    
2922
        parse: function(resp) {
2923
            return _.map(resp, function(value, key) {
2924
                return _.extend(value, {'name': key, 'id': key});
2925
            })
2926
        }
2927
    });
2928
    
2929
    // storage initialization
2930
    snf.storage.images = new models.Images();
2931
    snf.storage.flavors = new models.Flavors();
2932
    snf.storage.networks = new models.Networks();
2933
    snf.storage.vms = new models.VMS();
2934
    snf.storage.keys = new models.PublicKeys();
2935
    snf.storage.nics = new models.NICs();
2936
    snf.storage.resources = new models.Resources();
2937
    snf.storage.quotas = new models.Quotas();
2938
    snf.storage.public_ips = new models.PublicIPs();
2939
    snf.storage.public_pools = new models.PublicPools();
2940

    
2941
})(this);