Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (84.3 kB)

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

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

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

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

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

    
65
    // Base object for all our models
66
    models.Model = bb.Model.extend({
67
        sync: snf.api.sync,
68
        api: snf.api,
69
        api_type: 'compute',
70
        has_status: false,
71

    
72
        initialize: function() {
73
            if (this.has_status) {
74
                this.bind("change:status", this.handle_remove);
75
                this.handle_remove();
76
            }
77
            
78
            this.api_call = _.bind(this.api.call, this);
79
            models.Model.__super__.initialize.apply(this, arguments);
80
        },
81

    
82
        handle_remove: function() {
83
            if (this.get("status") == 'DELETED') {
84
                if (this.collection) {
85
                    try { this.clear_pending_action();} catch (err) {};
86
                    try { this.reset_pending_actions();} catch (err) {};
87
                    try { this.stop_stats_update();} catch (err) {};
88
                    this.collection.remove(this.id);
89
                }
90
            }
91
        },
92
        
93
        // custom set method to allow submodels to use
94
        // set_<attr> methods for handling the value of each
95
        // attribute and overriding the default set method
96
        // for specific parameters
97
        set: function(params, options) {
98
            _.each(params, _.bind(function(value, key){
99
                if (this["set_" + key]) {
100
                    params[key] = this["set_" + key](value);
101
                }
102
            }, this))
103
            var ret = bb.Model.prototype.set.call(this, params, options);
104
            return ret;
105
        },
106

    
107
        url: function(options) {
108
            return getUrl.call(this, this.base_url) + "/" + this.id;
109
        },
110

    
111
        api_path: function(options) {
112
            return this.path + "/" + this.id;
113
        },
114

    
115
        parse: function(resp, xhr) {
116
        },
117

    
118
        remove: function(complete, error, success) {
119
            this.api_call(this.api_path(), "delete", undefined, complete, error, success);
120
        },
121

    
122
        changedKeys: function() {
123
            return _.keys(this.changedAttributes() || {});
124
        },
125
            
126
        // return list of changed attributes that included in passed list
127
        // argument
128
        getKeysChanged: function(keys) {
129
            return _.intersection(keys, this.changedKeys());
130
        },
131
        
132
        // boolean check of keys changed
133
        keysChanged: function(keys) {
134
            return this.getKeysChanged(keys).length > 0;
135
        },
136

    
137
        // check if any of the passed attribues has changed
138
        hasOnlyChange: function(keys) {
139
            var ret = false;
140
            _.each(keys, _.bind(function(key) {
141
                if (this.changedKeys().length == 1 && this.changedKeys().indexOf(key) > -1) { ret = true};
142
            }, this));
143
            return ret;
144
        }
145

    
146
    })
147
    
148
    // Base object for all our model collections
149
    models.Collection = bb.Collection.extend({
150
        sync: snf.api.sync,
151
        api: snf.api,
152
        api_type: 'compute',
153
        supportIncUpdates: true,
154

    
155
        initialize: function() {
156
            models.Collection.__super__.initialize.apply(this, arguments);
157
            this.api_call = _.bind(this.api.call, this);
158
        },
159

    
160
        url: function(options, method) {
161
            return getUrl.call(this, this.base_url) + (
162
                    options.details || this.details && method != 'create' ? '/detail' : '');
163
        },
164

    
165
        fetch: function(options) {
166
            if (!options) { options = {} };
167
            // default to update
168
            if (!this.noUpdate) {
169
                if (options.update === undefined) { options.update = true };
170
                if (!options.removeMissing && options.refresh) { options.removeMissing = true };
171
            } else {
172
                if (options.refresh === undefined) {
173
                    options.refresh = true;
174
                }
175
            }
176
            // custom event foreach fetch
177
            return bb.Collection.prototype.fetch.call(this, options)
178
        },
179

    
180
        create: function(model, options) {
181
            var coll = this;
182
            options || (options = {});
183
            model = this._prepareModel(model, options);
184
            if (!model) return false;
185
            var success = options.success;
186
            options.success = function(nextModel, resp, xhr) {
187
                if (success) success(nextModel, resp, xhr);
188
            };
189
            model.save(null, options);
190
            return model;
191
        },
192

    
193
        get_fetcher: function(interval, increase, fast, increase_after_calls, max, initial_call, params) {
194
            var fetch_params = params || {};
195
            var handler_options = {};
196

    
197
            fetch_params.skips_timeouts = true;
198
            handler_options.interval = interval;
199
            handler_options.increase = increase;
200
            handler_options.fast = fast;
201
            handler_options.increase_after_calls = increase_after_calls;
202
            handler_options.max= max;
203
            handler_options.id = "collection id";
204

    
205
            var last_ajax = undefined;
206
            var callback = _.bind(function() {
207
                // clone to avoid referenced objects
208
                var params = _.clone(fetch_params);
209
                updater._ajax = last_ajax;
210
                
211
                // wait for previous request to finish
212
                if (last_ajax && last_ajax.readyState < 4 && last_ajax.statusText != "timeout") {
213
                    // opera readystate for 304 responses is 0
214
                    if (!($.browser.opera && last_ajax.readyState == 0 && last_ajax.status == 304)) {
215
                        return;
216
                    }
217
                }
218
                
219
                last_ajax = this.fetch(params);
220
            }, this);
221
            handler_options.callback = callback;
222

    
223
            var updater = new snf.api.updateHandler(_.clone(_.extend(handler_options, fetch_params)));
224
            snf.api.bind("call", _.throttle(_.bind(function(){ updater.faster(true)}, this)), 1000);
225
            return updater;
226
        }
227
    });
228
    
229
    // Image model
230
    models.Image = models.Model.extend({
231
        path: 'images',
232
        
233
        get_size: function() {
234
            return parseInt(this.get('metadata') ? this.get('metadata').values.size : -1)
235
        },
236

    
237
        get_description: function(escape) {
238
            if (escape == undefined) { escape = true };
239
            if (escape) { return this.escape('description') || "No description available"}
240
            return this.get('description') || "No description available."
241
        },
242

    
243
        get_meta: function(key) {
244
            if (this.get('metadata') && this.get('metadata').values) {
245
                if (!this.get('metadata').values[key]) { return null }
246
                return _.escape(this.get('metadata').values[key]);
247
            } else {
248
                return null;
249
            }
250
        },
251

    
252
        get_meta_keys: function() {
253
            if (this.get('metadata') && this.get('metadata').values) {
254
                return _.keys(this.get('metadata').values);
255
            } else {
256
                return [];
257
            }
258
        },
259

    
260
        get_owner: function() {
261
            return this.get('owner') || _.keys(synnefo.config.system_images_owners)[0];
262
        },
263

    
264
        get_owner_uuid: function() {
265
            return this.get('owner_uuid');
266
        },
267

    
268
        is_system_image: function() {
269
          var owner = this.get_owner();
270
          return _.include(_.keys(synnefo.config.system_images_owners), owner)
271
        },
272

    
273
        owned_by: function(user) {
274
          if (!user) { user = synnefo.user }
275
          return user.get_username() == this.get('owner_uuid');
276
        },
277

    
278
        display_owner: function() {
279
            var owner = this.get_owner();
280
            if (_.include(_.keys(synnefo.config.system_images_owners), owner)) {
281
                return synnefo.config.system_images_owners[owner];
282
            } else {
283
                return owner;
284
            }
285
        },
286
    
287
        get_readable_size: function() {
288
            if (this.is_deleted()) {
289
                return synnefo.config.image_deleted_size_title || '(none)';
290
            }
291
            return this.get_size() > 0 ? util.readablizeBytes(this.get_size() * 1024 * 1024) : '(none)';
292
        },
293

    
294
        get_os: function() {
295
            return this.get_meta('OS');
296
        },
297

    
298
        get_gui: function() {
299
            return this.get_meta('GUI');
300
        },
301

    
302
        get_created_users: function() {
303
            try {
304
              var users = this.get_meta('users').split(" ");
305
            } catch (err) { users = null }
306
            if (!users) { users = [synnefo.config.os_created_users[this.get_os()] || "root"]}
307
            return users;
308
        },
309

    
310
        get_sort_order: function() {
311
            return parseInt(this.get('metadata') ? this.get('metadata').values.sortorder : -1)
312
        },
313

    
314
        get_vm: function() {
315
            var vm_id = this.get("serverRef");
316
            var vm = undefined;
317
            vm = storage.vms.get(vm_id);
318
            return vm;
319
        },
320

    
321
        is_public: function() {
322
            return this.get('is_public') == undefined ? true : this.get('is_public');
323
        },
324

    
325
        is_deleted: function() {
326
            return this.get('status') == "DELETED"
327
        },
328
        
329
        ssh_keys_paths: function() {
330
            return _.map(this.get_created_users(), function(username) {
331
                prepend = '';
332
                if (username != 'root') {
333
                    prepend = '/home'
334
                }
335
                return {'user': username, 'path': '{1}/{0}/.ssh/authorized_keys'.format(username, 
336
                                                             prepend)};
337
            });
338
        },
339

    
340
        _supports_ssh: function() {
341
            if (synnefo.config.support_ssh_os_list.indexOf(this.get_os()) > -1) {
342
                return true;
343
            }
344
            if (this.get_meta('osfamily') == 'linux') {
345
              return true;
346
            }
347
            return false;
348
        },
349

    
350
        supports: function(feature) {
351
            if (feature == "ssh") {
352
                return this._supports_ssh()
353
            }
354
            return false;
355
        },
356

    
357
        personality_data_for_keys: function(keys) {
358
            return _.map(this.ssh_keys_paths(), function(pathinfo) {
359
                var contents = '';
360
                _.each(keys, function(key){
361
                    contents = contents + key.get("content") + "\n"
362
                });
363
                contents = $.base64.encode(contents);
364

    
365
                return {
366
                    path: pathinfo.path,
367
                    contents: contents,
368
                    mode: 0600,
369
                    owner: pathinfo.user
370
                }
371
            });
372
        }
373
    });
374

    
375
    // Flavor model
376
    models.Flavor = models.Model.extend({
377
        path: 'flavors',
378

    
379
        details_string: function() {
380
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
381
        },
382

    
383
        get_disk_size: function() {
384
            return parseInt(this.get("disk") * 1000)
385
        },
386

    
387
        get_ram_size: function() {
388
            return parseInt(this.get("ram"))
389
        },
390

    
391
        get_disk_template_info: function() {
392
            var info = snf.config.flavors_disk_templates_info[this.get("disk_template")];
393
            if (!info) {
394
                info = { name: this.get("disk_template"), description:'' };
395
            }
396
            return info
397
        },
398

    
399
        disk_to_bytes: function() {
400
            return parseInt(this.get("disk")) * 1024 * 1024 * 1024;
401
        },
402

    
403
        ram_to_bytes: function() {
404
            return parseInt(this.get("ram")) * 1024 * 1024;
405
        },
406

    
407
    });
408
    
409
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
410
    _.extend(models.ParamsList.prototype, bb.Events, {
411

    
412
        initialize: function(parent, param_name) {
413
            this.parent = parent;
414
            this.actions = {};
415
            this.param_name = param_name;
416
            this.length = 0;
417
        },
418
        
419
        has_action: function(action) {
420
            return this.actions[action] ? true : false;
421
        },
422
            
423
        _parse_params: function(arguments) {
424
            if (arguments.length <= 1) {
425
                return [];
426
            }
427

    
428
            var args = _.toArray(arguments);
429
            return args.splice(1);
430
        },
431

    
432
        contains: function(action, params) {
433
            params = this._parse_params(arguments);
434
            var has_action = this.has_action(action);
435
            if (!has_action) { return false };
436

    
437
            var paramsEqual = false;
438
            _.each(this.actions[action], function(action_params) {
439
                if (_.isEqual(action_params, params)) {
440
                    paramsEqual = true;
441
                }
442
            });
443
                
444
            return paramsEqual;
445
        },
446
        
447
        is_empty: function() {
448
            return _.isEmpty(this.actions);
449
        },
450

    
451
        add: function(action, params) {
452
            params = this._parse_params(arguments);
453
            if (this.contains.apply(this, arguments)) { return this };
454
            var isnew = false
455
            if (!this.has_action(action)) {
456
                this.actions[action] = [];
457
                isnew = true;
458
            };
459

    
460
            this.actions[action].push(params);
461
            this.parent.trigger("change:" + this.param_name, this.parent, this);
462
            if (isnew) {
463
                this.trigger("add", action, params);
464
            } else {
465
                this.trigger("change", action, params);
466
            }
467
            return this;
468
        },
469
        
470
        remove_all: function(action) {
471
            if (this.has_action(action)) {
472
                delete this.actions[action];
473
                this.parent.trigger("change:" + this.param_name, this.parent, this);
474
                this.trigger("remove", action);
475
            }
476
            return this;
477
        },
478

    
479
        reset: function() {
480
            this.actions = {};
481
            this.parent.trigger("change:" + this.param_name, this.parent, this);
482
            this.trigger("reset");
483
            this.trigger("remove");
484
        },
485

    
486
        remove: function(action, params) {
487
            params = this._parse_params(arguments);
488
            if (!this.has_action(action)) { return this };
489
            var index = -1;
490
            _.each(this.actions[action], _.bind(function(action_params) {
491
                if (_.isEqual(action_params, params)) {
492
                    index = this.actions[action].indexOf(action_params);
493
                }
494
            }, this));
495
            
496
            if (index > -1) {
497
                this.actions[action].splice(index, 1);
498
                if (_.isEmpty(this.actions[action])) {
499
                    delete this.actions[action];
500
                }
501
                this.parent.trigger("change:" + this.param_name, this.parent, this);
502
                this.trigger("remove", action, params);
503
            }
504
        }
505

    
506
    });
507

    
508
    // Image model
509
    models.Network = models.Model.extend({
510
        path: 'networks',
511
        has_status: true,
512
        defaults: {'connecting':0},
513
        
514
        initialize: function() {
515
            var ret = models.Network.__super__.initialize.apply(this, arguments);
516
            this.set({"actions": new models.ParamsList(this, "actions")});
517
            this.update_state();
518
            this.bind("change:nics", _.bind(synnefo.storage.nics.update_net_nics, synnefo.storage.nics));
519
            this.bind("change:status", _.bind(this.update_state, this));
520
            return ret;
521
        },
522
        
523
        is_deleted: function() {
524
          return this.get('status') == 'DELETED';
525
        },
526

    
527
        toJSON: function() {
528
            var attrs = _.clone(this.attributes);
529
            attrs.actions = _.clone(this.get("actions").actions);
530
            return attrs;
531
        },
532
        
533
        set_state: function(val) {
534
            if (val == "PENDING" && this.get("state") == "DESTORY") {
535
                return "DESTROY";
536
            }
537
            return val;
538
        },
539

    
540
        update_state: function() {
541
            if (this.get("connecting") > 0) {
542
                this.set({state: "CONNECTING"});
543
                return
544
            }
545
            
546
            if (this.get_nics(function(nic){ return nic.get("removing") == 1}).length > 0) {
547
                this.set({state: "DISCONNECTING"});
548
                return
549
            }   
550
            
551
            if (this.contains_firewalling_nics() > 0) {
552
                this.set({state: "FIREWALLING"});
553
                return
554
            }   
555
            
556
            if (this.get("state") == "DESTROY") { 
557
                this.set({"destroyed":1});
558
            }
559
            
560
            this.set({state:this.get('status')});
561
        },
562

    
563
        is_public: function() {
564
            return this.get("public");
565
        },
566

    
567
        decrease_connecting: function() {
568
            var conn = this.get("connecting");
569
            if (!conn) { conn = 0 };
570
            if (conn > 0) {
571
                conn--;
572
            }
573
            this.set({"connecting": conn});
574
            this.update_state();
575
        },
576

    
577
        increase_connecting: function() {
578
            var conn = this.get("connecting");
579
            if (!conn) { conn = 0 };
580
            conn++;
581
            this.set({"connecting": conn});
582
            this.update_state();
583
        },
584

    
585
        connected_to: function(vm) {
586
            return this.get('linked_to').indexOf(""+vm.id) > -1;
587
        },
588

    
589
        connected_with_nic_id: function(nic_id) {
590
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
591
        },
592

    
593
        get_nics: function(filter) {
594
            var nics = synnefo.storage.nics.filter(function(nic) {
595
                return nic.get('network_id') == this.id;
596
            }, this);
597

    
598
            if (filter) {
599
                return _.filter(nics, filter);
600
            }
601
            return nics;
602
        },
603

    
604
        contains_firewalling_nics: function() {
605
            return this.get_nics(function(n){return n.get('pending_firewall')}).length
606
        },
607

    
608
        call: function(action, params, success, error) {
609
            if (action == "destroy") {
610
                var previous_state = this.get('state');
611
                var previous_status = this.get('status');
612

    
613
                this.set({state:"DESTROY"});
614

    
615
                var _success = _.bind(function() {
616
                    if (success) { success() };
617
                    synnefo.storage.quotas.get('cyclades.network.private').decrease();
618
                }, this);
619
                var _error = _.bind(function() {
620
                    this.set({state: previous_state, status: previous_status})
621
                    if (error) { error() };
622
                }, this);
623

    
624
                this.remove(undefined, _error, _success);
625
            }
626
            
627
            if (action == "disconnect") {
628
                if (this.get("state") == "DESTROY") {
629
                    return;
630
                }
631
                
632
                _.each(params, _.bind(function(nic_id) {
633
                    var nic = snf.storage.nics.get(nic_id);
634
                    this.get("actions").remove("disconnect", nic_id);
635
                    if (nic) {
636
                        this.remove_nic(nic, success, error);
637
                    }
638
                }, this));
639
            }
640
        },
641

    
642
        add_vm: function (vm, callback, error, options) {
643
            var payload = {add:{serverRef:"" + vm.id}};
644
            payload._options = options || {};
645
            return this.api_call(this.api_path() + "/action", "create", 
646
                                 payload,
647
                                 undefined,
648
                                 error,
649
                                 _.bind(function(){
650
                                     //this.vms.add_pending(vm.id);
651
                                     this.increase_connecting();
652
                                     if (callback) {callback()}
653
                                 },this), error);
654
        },
655

    
656
        remove_nic: function (nic, callback, error, options) {
657
            var payload = {remove:{attachment:"" + nic.get("attachment_id")}};
658
            payload._options = options || {};
659
            return this.api_call(this.api_path() + "/action", "create", 
660
                                 payload,
661
                                 undefined,
662
                                 error,
663
                                 _.bind(function(){
664
                                     nic.set({"removing": 1});
665
                                     nic.get_network().update_state();
666
                                     //this.vms.add_pending_for_remove(vm.id);
667
                                     if (callback) {callback()}
668
                                 },this), error);
669
        },
670

    
671
        rename: function(name, callback) {
672
            return this.api_call(this.api_path(), "update", {
673
                network:{name:name}, 
674
                _options:{
675
                    critical: false, 
676
                    error_params:{
677
                        title: "Network action failed",
678
                        ns: "Networks",
679
                        extra_details: {"Network id": this.id}
680
                    }
681
                }}, callback);
682
        },
683

    
684
        get_connectable_vms: function() {
685
            return storage.vms.filter(function(vm){
686
                return !vm.in_error_state() && !vm.is_building();
687
            })
688
        },
689

    
690
        state_message: function() {
691
            if (this.get("state") == "ACTIVE" && !this.is_public()) {
692
                if (this.get("cidr") && this.get("dhcp") == true) {
693
                    return this.get("cidr");
694
                } else {
695
                    return "Private network";
696
                }
697
            }
698
            if (this.get("state") == "ACTIVE" && this.is_public()) {
699
                  return "Public network";
700
            }
701

    
702
            return models.Network.STATES[this.get("state")];
703
        },
704

    
705
        in_progress: function() {
706
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
707
        },
708

    
709
        do_all_pending_actions: function(success, error) {
710
          var params, actions, action_params;
711
          actions = _.clone(this.get("actions").actions);
712
            _.each(actions, _.bind(function(params, action) {
713
                action_params = _.map(actions[action], function(a){ return _.clone(a)});
714
                _.each(action_params, _.bind(function(params) {
715
                    this.call(action, params, success, error);
716
                }, this));
717
            }, this));
718
            this.get("actions").reset();
719
        }
720
    });
721
    
722
    models.Network.STATES = {
723
        'ACTIVE': 'Private network',
724
        'CONNECTING': 'Connecting...',
725
        'DISCONNECTING': 'Disconnecting...',
726
        'FIREWALLING': 'Firewall update...',
727
        'DESTROY': 'Destroying...',
728
        'PENDING': 'Pending...',
729
        'ERROR': 'Error'
730
    }
731

    
732
    models.Network.STATES_TRANSITIONS = {
733
        'CONNECTING': ['ACTIVE'],
734
        'DISCONNECTING': ['ACTIVE'],
735
        'PENDING': ['ACTIVE'],
736
        'FIREWALLING': ['ACTIVE']
737
    }
738

    
739
    // Virtualmachine model
740
    models.VM = models.Model.extend({
741

    
742
        path: 'servers',
743
        has_status: true,
744
        initialize: function(params) {
745
            
746
            this.pending_firewalls = {};
747
            
748
            models.VM.__super__.initialize.apply(this, arguments);
749

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

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

    
782
            this.bind("change:nics", _.bind(synnefo.storage.nics.update_vm_nics, synnefo.storage.nics));
783
        },
784

    
785
        status: function(st) {
786
            if (!st) { return this.get("status")}
787
            return this.set({status:st});
788
        },
789

    
790
        set_status: function(st) {
791
            var new_state = this.state_for_api_status(st);
792
            var transition = false;
793

    
794
            if (this.state() != new_state) {
795
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
796
                    transition = this.state();
797
                }
798
            }
799
            
800
            // call it silently to avoid double change trigger
801
            this.set({'state': this.state_for_api_status(st)}, {silent: true});
802
            
803
            // trigger transition
804
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
805
                this.trigger("transition", {from:transition, to:new_state}) 
806
            };
807
            return st;
808
        },
809
            
810
        get_diagnostics: function(success) {
811
            this.__make_api_call(this.get_diagnostics_url(),
812
                                 "read", // create so that sync later uses POST to make the call
813
                                 null, // payload
814
                                 function(data) {
815
                                     success(data);
816
                                 },  
817
                                 null, 'diagnostics');
818
        },
819

    
820
        has_diagnostics: function() {
821
            return this.get("diagnostics") && this.get("diagnostics").length;
822
        },
823

    
824
        get_progress_info: function() {
825
            // details about progress message
826
            // contains a list of diagnostic messages
827
            return this.get("status_messages");
828
        },
829

    
830
        get_status_message: function() {
831
            return this.get('status_message');
832
        },
833
        
834
        // extract status message from diagnostics
835
        status_message_from_diagnostics: function(diagnostics) {
836
            var valid_sources_map = synnefo.config.diagnostics_status_messages_map;
837
            var valid_sources = valid_sources_map[this.get('status')];
838
            if (!valid_sources) { return null };
839
            
840
            // filter messsages based on diagnostic source
841
            var messages = _.filter(diagnostics, function(diag) {
842
                return valid_sources.indexOf(diag.source) > -1;
843
            });
844

    
845
            var msg = messages[0];
846
            if (msg) {
847
              var message = msg.message;
848
              var message_tpl = snf.config.diagnostic_messages_tpls[msg.source];
849

    
850
              if (message_tpl) {
851
                  message = message_tpl.replace('MESSAGE', msg.message);
852
              }
853
              return message;
854
            }
855
            
856
            // no message to display, but vm in build state, display
857
            // finalizing message.
858
            if (this.is_building() == 'BUILD') {
859
                return synnefo.config.BUILDING_MESSAGES['FINAL'];
860
            }
861
            return null;
862
        },
863

    
864
        update_status_message: function(force) {
865
            // update only if one of the specified attributes has changed
866
            if (
867
              !this.keysChanged(['diagnostics', 'progress', 'status', 'state'])
868
                && !force
869
            ) { return };
870
            
871
            // if user requested to destroy the vm set the appropriate 
872
            // message.
873
            if (this.get('state') == "DESTROY") { 
874
                message = "Terminating..."
875
                this.set({status_message: message})
876
                return;
877
            }
878
            
879
            // set error message, if vm has diagnostic message display it as
880
            // progress message
881
            if (this.in_error_state()) {
882
                var d = this.get('diagnostics');
883
                if (d && d.length) {
884
                    var message = this.status_message_from_diagnostics(d);
885
                    this.set({status_message: message});
886
                } else {
887
                    this.set({status_message: null});
888
                }
889
                return;
890
            }
891
            
892
            // identify building status message
893
            if (this.is_building()) {
894
                var self = this;
895
                var success = function(msg) {
896
                    self.set({status_message: msg});
897
                }
898
                this.get_building_status_message(success);
899
                return;
900
            }
901

    
902
            this.set({status_message:null});
903
        },
904
            
905
        // get building status message. Asynchronous function since it requires
906
        // access to vm image.
907
        get_building_status_message: function(callback) {
908
            // no progress is set, vm is in initial build status
909
            var progress = this.get("progress");
910
            if (progress == 0 || !progress) {
911
                return callback(BUILDING_MESSAGES['INIT']);
912
            }
913
            
914
            // vm has copy progress, display copy percentage
915
            if (progress > 0 && progress <= 99) {
916
                this.get_copy_details(true, undefined, _.bind(
917
                    function(details){
918
                        callback(BUILDING_MESSAGES['COPY'].format(details.copy, 
919
                                                           details.size, 
920
                                                           details.progress));
921
                }, this));
922
                return;
923
            }
924

    
925
            // copy finished display FINAL message or identify status message
926
            // from diagnostics.
927
            if (progress >= 100) {
928
                if (!this.has_diagnostics()) {
929
                        callback(BUILDING_MESSAGES['FINAL']);
930
                } else {
931
                        var d = this.get("diagnostics");
932
                        var msg = this.status_message_from_diagnostics(d);
933
                        if (msg) {
934
                              callback(msg);
935
                        }
936
                }
937
            }
938
        },
939

    
940
        get_copy_details: function(human, image, callback) {
941
            var human = human || false;
942
            var image = image || this.get_image(_.bind(function(image){
943
                var progress = this.get('progress');
944
                var size = image.get_size();
945
                var size_copied = (size * progress / 100).toFixed(2);
946
                
947
                if (human) {
948
                    size = util.readablizeBytes(size*1024*1024);
949
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
950
                }
951

    
952
                callback({'progress': progress, 'size': size, 'copy': size_copied})
953
            }, this));
954
        },
955

    
956
        start_stats_update: function(force_if_empty) {
957
            var prev_state = this.do_update_stats;
958

    
959
            this.do_update_stats = true;
960
            
961
            // fetcher initialized ??
962
            if (!this.stats_fetcher) {
963
                this.init_stats_intervals();
964
            }
965

    
966

    
967
            // fetcher running ???
968
            if (!this.stats_fetcher.running || !prev_state) {
969
                this.stats_fetcher.start();
970
            }
971

    
972
            if (force_if_empty && this.get("stats") == undefined) {
973
                this.update_stats(true);
974
            }
975
        },
976

    
977
        stop_stats_update: function(stop_calls) {
978
            this.do_update_stats = false;
979

    
980
            if (stop_calls) {
981
                this.stats_fetcher.stop();
982
            }
983
        },
984

    
985
        // clear and reinitialize update interval
986
        init_stats_intervals: function (interval) {
987
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
988
            this.stats_fetcher.start();
989
        },
990
        
991
        get_stats_fetcher: function(timeout) {
992
            var cb = _.bind(function(data){
993
                this.update_stats();
994
            }, this);
995
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
996
            return fetcher;
997
        },
998

    
999
        // do the api call
1000
        update_stats: function(force) {
1001
            // do not update stats if flag not set
1002
            if ((!this.do_update_stats && !force) || this.updating_stats) {
1003
                return;
1004
            }
1005

    
1006
            // make the api call, execute handle_stats_update on sucess
1007
            // TODO: onError handler ???
1008
            stats_url = this.url() + "/stats";
1009
            this.updating_stats = true;
1010
            this.sync("read", this, {
1011
                handles_error:true, 
1012
                url: stats_url, 
1013
                refresh:true, 
1014
                success: _.bind(this.handle_stats_update, this),
1015
                error: _.bind(this.handle_stats_error, this),
1016
                complete: _.bind(function(){this.updating_stats = false;}, this),
1017
                critical: false,
1018
                log_error: false,
1019
                skips_timeouts: true
1020
            });
1021
        },
1022

    
1023
        get_stats_image: function(stat, type) {
1024
        },
1025
        
1026
        _set_stats: function(stats) {
1027
            var silent = silent === undefined ? false : silent;
1028
            // unavailable stats while building
1029
            if (this.get("status") == "BUILD") { 
1030
                this.stats_available = false;
1031
            } else { this.stats_available = true; }
1032

    
1033
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
1034
            
1035
            this.set({stats: stats}, {silent:true});
1036
            this.trigger("stats:update", stats);
1037
        },
1038

    
1039
        unbind: function() {
1040
            models.VM.__super__.unbind.apply(this, arguments);
1041
        },
1042

    
1043
        handle_stats_error: function() {
1044
            stats = {};
1045
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1046
                stats[k] = false;
1047
            });
1048

    
1049
            this.set({'stats': stats});
1050
        },
1051

    
1052
        // this method gets executed after a successful vm stats api call
1053
        handle_stats_update: function(data) {
1054
            var self = this;
1055
            // avoid browser caching
1056
            
1057
            if (data.stats && _.size(data.stats) > 0) {
1058
                var ts = $.now();
1059
                var stats = data.stats;
1060
                var images_loaded = 0;
1061
                var images = {};
1062

    
1063
                function check_images_loaded() {
1064
                    images_loaded++;
1065

    
1066
                    if (images_loaded == 4) {
1067
                        self._set_stats(images);
1068
                    }
1069
                }
1070
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1071
                    
1072
                    stats[k] = stats[k] + "?_=" + ts;
1073
                    
1074
                    var stat = k.slice(0,3);
1075
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
1076
                    var img = $("<img />");
1077
                    var val = stats[k];
1078
                    
1079
                    // load stat image to a temporary dom element
1080
                    // update model stats on image load/error events
1081
                    img.load(function() {
1082
                        images[k] = val;
1083
                        check_images_loaded();
1084
                    });
1085

    
1086
                    img.error(function() {
1087
                        images[stat + type] = false;
1088
                        check_images_loaded();
1089
                    });
1090

    
1091
                    img.attr({'src': stats[k]});
1092
                })
1093
                data.stats = stats;
1094
            }
1095

    
1096
            // do we need to change the interval ??
1097
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
1098
                this.stats_update_interval = data.stats.refresh * 1000;
1099
                this.stats_fetcher.interval = this.stats_update_interval;
1100
                this.stats_fetcher.maximum_interval = this.stats_update_interval;
1101
                this.stats_fetcher.stop();
1102
                this.stats_fetcher.start(false);
1103
            }
1104
        },
1105

    
1106
        // helper method that sets the do_update_stats
1107
        // in the future this method could also make an api call
1108
        // immediaetly if needed
1109
        enable_stats_update: function() {
1110
            this.do_update_stats = true;
1111
        },
1112
        
1113
        handle_destroy: function() {
1114
            this.stats_fetcher.stop();
1115
        },
1116

    
1117
        require_reboot: function() {
1118
            if (this.is_active()) {
1119
                this.set({'reboot_required': true});
1120
            }
1121
        },
1122
        
1123
        set_pending_action: function(data) {
1124
            this.pending_action = data;
1125
            return data;
1126
        },
1127

    
1128
        // machine has pending action
1129
        update_pending_action: function(action, force) {
1130
            this.set({pending_action: action});
1131
        },
1132

    
1133
        clear_pending_action: function() {
1134
            this.set({pending_action: undefined});
1135
        },
1136

    
1137
        has_pending_action: function() {
1138
            return this.get("pending_action") ? this.get("pending_action") : false;
1139
        },
1140
        
1141
        // machine is active
1142
        is_active: function() {
1143
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
1144
        },
1145
        
1146
        // machine is building 
1147
        is_building: function() {
1148
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
1149
        },
1150
        
1151
        in_error_state: function() {
1152
            return this.state() === "ERROR"
1153
        },
1154

    
1155
        // user can connect to machine
1156
        is_connectable: function() {
1157
            // check if ips exist
1158
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
1159
                return false;
1160
            }
1161
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1162
        },
1163
        
1164
        remove_meta: function(key, complete, error) {
1165
            var url = this.api_path() + "/meta/" + key;
1166
            this.api_call(url, "delete", undefined, complete, error);
1167
        },
1168

    
1169
        save_meta: function(meta, complete, error) {
1170
            var url = this.api_path() + "/meta/" + meta.key;
1171
            var payload = {meta:{}};
1172
            payload.meta[meta.key] = meta.value;
1173
            payload._options = {
1174
                critical:false, 
1175
                error_params: {
1176
                    title: "Machine metadata error",
1177
                    extra_details: {"Machine id": this.id}
1178
            }};
1179

    
1180
            this.api_call(url, "update", payload, complete, error);
1181
        },
1182

    
1183

    
1184
        // update/get the state of the machine
1185
        state: function() {
1186
            var args = slice.call(arguments);
1187
                
1188
            // TODO: it might not be a good idea to set the state in set_state method
1189
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1190
                this.set({'state': args[0]});
1191
            }
1192

    
1193
            return this.get('state');
1194
        },
1195
        
1196
        // get the state that the api status corresponds to
1197
        state_for_api_status: function(status) {
1198
            return this.state_transition(this.state(), status);
1199
        },
1200
        
1201
        // vm state equals vm api status
1202
        state_is_status: function(state) {
1203
            return models.VM.STATUSES.indexOf(state) != -1;
1204
        },
1205
        
1206
        // get transition state for the corresponging api status
1207
        state_transition: function(state, new_status) {
1208
            var statuses = models.VM.STATES_TRANSITIONS[state];
1209
            if (statuses) {
1210
                if (statuses.indexOf(new_status) > -1) {
1211
                    return new_status;
1212
                } else {
1213
                    return state;
1214
                }
1215
            } else {
1216
                return new_status;
1217
            }
1218
        },
1219
        
1220
        // the current vm state is a transition state
1221
        in_transition: function() {
1222
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
1223
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
1224
        },
1225
        
1226
        // get image object
1227
        get_image: function(callback) {
1228
            if (callback == undefined) { callback = function(){} }
1229
            var image = storage.images.get(this.get('imageRef'));
1230
            if (!image) {
1231
                storage.images.update_unknown_id(this.get('imageRef'), callback);
1232
                return;
1233
            }
1234
            callback(image);
1235
            return image;
1236
        },
1237
        
1238
        // get flavor object
1239
        get_flavor: function() {
1240
            var flv = storage.flavors.get(this.get('flavorRef'));
1241
            if (!flv) {
1242
                storage.flavors.update_unknown_id(this.get('flavorRef'));
1243
                flv = storage.flavors.get(this.get('flavorRef'));
1244
            }
1245
            return flv;
1246
        },
1247

    
1248
        get_meta: function(key, deflt) {
1249
            if (this.get('metadata') && this.get('metadata').values) {
1250
                if (!this.get('metadata').values[key]) { return deflt }
1251
                return _.escape(this.get('metadata').values[key]);
1252
            } else {
1253
                return deflt;
1254
            }
1255
        },
1256

    
1257
        get_meta_keys: function() {
1258
            if (this.get('metadata') && this.get('metadata').values) {
1259
                return _.keys(this.get('metadata').values);
1260
            } else {
1261
                return [];
1262
            }
1263
        },
1264
        
1265
        // get metadata OS value
1266
        get_os: function() {
1267
            var image = this.get_image();
1268
            return this.get_meta('OS') || (image ? 
1269
                                            image.get_os() || "okeanos" : "okeanos");
1270
        },
1271

    
1272
        get_gui: function() {
1273
            return this.get_meta('GUI');
1274
        },
1275
        
1276
        connected_to: function(net) {
1277
            return this.get('linked_to').indexOf(net.id) > -1;
1278
        },
1279

    
1280
        connected_with_nic_id: function(nic_id) {
1281
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
1282
        },
1283

    
1284
        get_nics: function(filter) {
1285
            ret = synnefo.storage.nics.filter(function(nic) {
1286
                return parseInt(nic.get('vm_id')) == this.id;
1287
            }, this);
1288

    
1289
            if (filter) {
1290
                return _.filter(ret, filter);
1291
            }
1292

    
1293
            return ret;
1294
        },
1295

    
1296
        get_net_nics: function(net_id) {
1297
            return this.get_nics(function(n){return n.get('network_id') == net_id});
1298
        },
1299

    
1300
        get_public_nic: function() {
1301
            return this.get_nics(function(n){ return n.get_network().is_public() === true })[0];
1302
        },
1303

    
1304
        get_hostname: function() {
1305
          var hostname = this.get_meta('hostname');
1306
          if (!hostname) {
1307
            if (synnefo.config.vm_hostname_format) {
1308
              hostname = synnefo.config.vm_hostname_format.format(this.id);
1309
            } else {
1310
              hostname = this.get_public_nic().get('ipv4');
1311
            }
1312
          }
1313
          return hostname;
1314
        },
1315

    
1316
        get_nic: function(net_id) {
1317
        },
1318

    
1319
        has_firewall: function() {
1320
            var nic = this.get_public_nic();
1321
            if (nic) {
1322
                var profile = nic.get('firewallProfile'); 
1323
                return ['ENABLED', 'PROTECTED'].indexOf(profile) > -1;
1324
            }
1325
            return false;
1326
        },
1327

    
1328
        get_firewall_profile: function() {
1329
            var nic = this.get_public_nic();
1330
            if (nic) {
1331
                return nic.get('firewallProfile');
1332
            }
1333
            return null;
1334
        },
1335

    
1336
        get_addresses: function() {
1337
            var pnic = this.get_public_nic();
1338
            if (!pnic) { return {'ip4': undefined, 'ip6': undefined }};
1339
            return {'ip4': pnic.get('ipv4'), 'ip6': pnic.get('ipv6')};
1340
        },
1341
    
1342
        // get actions that the user can execute
1343
        // depending on the vm state/status
1344
        get_available_actions: function() {
1345
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1346
        },
1347

    
1348
        set_profile: function(profile, net_id) {
1349
        },
1350
        
1351
        // call rename api
1352
        rename: function(new_name) {
1353
            //this.set({'name': new_name});
1354
            this.sync("update", this, {
1355
                critical: true,
1356
                data: {
1357
                    'server': {
1358
                        'name': new_name
1359
                    }
1360
                }, 
1361
                // do the rename after the method succeeds
1362
                success: _.bind(function(){
1363
                    //this.set({name: new_name});
1364
                    snf.api.trigger("call");
1365
                }, this)
1366
            });
1367
        },
1368
        
1369
        get_console_url: function(data) {
1370
            var url_params = {
1371
                machine: this.get("name"),
1372
                host_ip: this.get_addresses().ip4,
1373
                host_ip_v6: this.get_addresses().ip6,
1374
                host: data.host,
1375
                port: data.port,
1376
                password: data.password
1377
            }
1378
            return '/machines/console?' + $.param(url_params);
1379
        },
1380

    
1381
        // action helper
1382
        call: function(action_name, success, error, params) {
1383
            var id_param = [this.id];
1384
            
1385
            params = params || {};
1386
            success = success || function() {};
1387
            error = error || function() {};
1388

    
1389
            var self = this;
1390

    
1391
            switch(action_name) {
1392
                case 'start':
1393
                    this.__make_api_call(this.get_action_url(), // vm actions url
1394
                                         "create", // create so that sync later uses POST to make the call
1395
                                         {start:{}}, // payload
1396
                                         function() {
1397
                                             // set state after successful call
1398
                                             self.state("START"); 
1399
                                             success.apply(this, arguments);
1400
                                             snf.api.trigger("call");
1401
                                         },  
1402
                                         error, 'start', params);
1403
                    break;
1404
                case 'reboot':
1405
                    this.__make_api_call(this.get_action_url(), // vm actions url
1406
                                         "create", // create so that sync later uses POST to make the call
1407
                                         {reboot:{type:"HARD"}}, // payload
1408
                                         function() {
1409
                                             // set state after successful call
1410
                                             self.state("REBOOT"); 
1411
                                             success.apply(this, arguments)
1412
                                             snf.api.trigger("call");
1413
                                             self.set({'reboot_required': false});
1414
                                         },
1415
                                         error, 'reboot', params);
1416
                    break;
1417
                case 'shutdown':
1418
                    this.__make_api_call(this.get_action_url(), // vm actions url
1419
                                         "create", // create so that sync later uses POST to make the call
1420
                                         {shutdown:{}}, // payload
1421
                                         function() {
1422
                                             // set state after successful call
1423
                                             self.state("SHUTDOWN"); 
1424
                                             success.apply(this, arguments)
1425
                                             snf.api.trigger("call");
1426
                                         },  
1427
                                         error, 'shutdown', params);
1428
                    break;
1429
                case 'console':
1430
                    this.__make_api_call(this.url() + "/action", "create", {'console': {'type':'vnc'}}, function(data) {
1431
                        var cons_data = data.console;
1432
                        success.apply(this, [cons_data]);
1433
                    }, undefined, 'console', params)
1434
                    break;
1435
                case 'destroy':
1436
                    this.__make_api_call(this.url(), // vm actions url
1437
                                         "delete", // create so that sync later uses POST to make the call
1438
                                         undefined, // payload
1439
                                         function() {
1440
                                             // set state after successful call
1441
                                             self.state('DESTROY');
1442
                                             success.apply(this, arguments);
1443
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1444

    
1445
                                         },  
1446
                                         error, 'destroy', params);
1447
                    break;
1448
                default:
1449
                    throw "Invalid VM action ("+action_name+")";
1450
            }
1451
        },
1452
        
1453
        __make_api_call: function(url, method, data, success, error, action, extra_params) {
1454
            var self = this;
1455
            error = error || function(){};
1456
            success = success || function(){};
1457

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

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

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

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

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

    
1497
        get_connection_info: function(host_os, success, error) {
1498
            var url = "/machines/connect";
1499
            params = {
1500
                ip_address: this.get_public_nic().get('ipv4'),
1501
                hostname: this.get_hostname(),
1502
                os: this.get_os(),
1503
                host_os: host_os,
1504
                srv: this.id
1505
            }
1506

    
1507
            url = url + "?" + $.param(params);
1508

    
1509
            var ajax = snf.api.sync("read", undefined, { url: url, 
1510
                                                         error:error, 
1511
                                                         success:success, 
1512
                                                         handles_error:1});
1513
        }
1514
    })
1515
    
1516
    models.VM.ACTIONS = [
1517
        'start',
1518
        'shutdown',
1519
        'reboot',
1520
        'console',
1521
        'destroy'
1522
    ]
1523

    
1524
    models.VM.AVAILABLE_ACTIONS = {
1525
        'UNKNWON'       : ['destroy'],
1526
        'BUILD'         : ['destroy'],
1527
        'REBOOT'        : ['shutdown', 'destroy', 'console'],
1528
        'STOPPED'       : ['start', 'destroy'],
1529
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1530
        'ERROR'         : ['destroy'],
1531
        'DELETED'        : [],
1532
        'DESTROY'       : [],
1533
        'BUILD_INIT'    : ['destroy'],
1534
        'BUILD_COPY'    : ['destroy'],
1535
        'BUILD_FINAL'   : ['destroy'],
1536
        'SHUTDOWN'      : ['destroy'],
1537
        'START'         : [],
1538
        'CONNECT'       : [],
1539
        'DISCONNECT'    : []
1540
    }
1541

    
1542
    // api status values
1543
    models.VM.STATUSES = [
1544
        'UNKNWON',
1545
        'BUILD',
1546
        'REBOOT',
1547
        'STOPPED',
1548
        'ACTIVE',
1549
        'ERROR',
1550
        'DELETED'
1551
    ]
1552

    
1553
    // api status values
1554
    models.VM.CONNECT_STATES = [
1555
        'ACTIVE',
1556
        'REBOOT',
1557
        'SHUTDOWN'
1558
    ]
1559

    
1560
    // vm states
1561
    models.VM.STATES = models.VM.STATUSES.concat([
1562
        'DESTROY',
1563
        'BUILD_INIT',
1564
        'BUILD_COPY',
1565
        'BUILD_FINAL',
1566
        'SHUTDOWN',
1567
        'START',
1568
        'CONNECT',
1569
        'DISCONNECT',
1570
        'FIREWALL'
1571
    ]);
1572
    
1573
    models.VM.STATES_TRANSITIONS = {
1574
        'DESTROY' : ['DELETED'],
1575
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1576
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1577
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1578
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1579
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1580
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1581
        'BUILD_COPY': ['ERROR', 'ACTIVE', 'BUILD_FINAL', 'DESTROY'],
1582
        'BUILD_FINAL': ['ERROR', 'ACTIVE', 'DESTROY'],
1583
        'BUILD_INIT': ['ERROR', 'ACTIVE', 'BUILD_COPY', 'BUILD_FINAL', 'DESTROY']
1584
    }
1585

    
1586
    models.VM.TRANSITION_STATES = [
1587
        'DESTROY',
1588
        'SHUTDOWN',
1589
        'START',
1590
        'REBOOT',
1591
        'BUILD'
1592
    ]
1593

    
1594
    models.VM.ACTIVE_STATES = [
1595
        'BUILD', 'REBOOT', 'ACTIVE',
1596
        'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL',
1597
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1598
    ]
1599

    
1600
    models.VM.BUILDING_STATES = [
1601
        'BUILD', 'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL'
1602
    ]
1603

    
1604
    models.Networks = models.Collection.extend({
1605
        model: models.Network,
1606
        path: 'networks',
1607
        details: true,
1608
        //noUpdate: true,
1609
        defaults: {'nics':[],'linked_to':[]},
1610
        
1611
        parse: function (resp, xhr) {
1612
            // FIXME: depricated global var
1613
            if (!resp) { return []};
1614
            var data = _.filter(_.map(resp.networks.values, _.bind(this.parse_net_api_data, this)),
1615
                               function(e){ return e });
1616
            return data;
1617
        },
1618

    
1619
        add: function() {
1620
            ret = models.Networks.__super__.add.apply(this, arguments);
1621
            // update nics after each network addition
1622
            ret.each(function(r){
1623
                synnefo.storage.nics.update_net_nics(r);
1624
            });
1625
        },
1626

    
1627
        reset_pending_actions: function() {
1628
            this.each(function(net) {
1629
                net.get("actions").reset();
1630
            });
1631
        },
1632

    
1633
        do_all_pending_actions: function() {
1634
            this.each(function(net) {
1635
                net.do_all_pending_actions();
1636
            })
1637
        },
1638

    
1639
        parse_net_api_data: function(data) {
1640
            // append nic metadata
1641
            // net.get('nics') contains a list of vm/index objects 
1642
            // e.g. {'vm_id':12231, 'index':1}
1643
            // net.get('linked_to') contains a list of vms the network is 
1644
            // connected to e.g. [1001, 1002]
1645
            if (data.attachments && data.attachments.values) {
1646
                data['nics'] = {};
1647
                data['linked_to'] = [];
1648
                _.each(data.attachments.values, function(nic_id){
1649
                  
1650
                  var vm_id = NIC_REGEX.exec(nic_id)[1];
1651
                  var nic_index = parseInt(NIC_REGEX.exec(nic_id)[2]);
1652

    
1653
                  if (vm_id !== undefined && nic_index !== undefined) {
1654
                      data['nics'][nic_id] = {
1655
                          'vm_id': vm_id, 
1656
                          'index': nic_index, 
1657
                          'id': nic_id
1658
                      };
1659
                      if (data['linked_to'].indexOf(vm_id) == -1) {
1660
                        data['linked_to'].push(vm_id);
1661
                      }
1662
                  }
1663
                });
1664
            }
1665

    
1666
            if (data.status == "DELETED" && !this.get(parseInt(data.id))) {
1667
              return false;
1668
            }
1669
            return data;
1670
        },
1671

    
1672
        create: function (name, type, cidr, dhcp, callback) {
1673
            var params = {
1674
                network:{
1675
                    name:name
1676
                }
1677
            };
1678

    
1679
            if (type) {
1680
                params.network.type = type;
1681
            }
1682
            if (cidr) {
1683
                params.network.cidr = cidr;
1684
            }
1685
            if (dhcp) {
1686
                params.network.dhcp = dhcp;
1687
            }
1688

    
1689
            if (dhcp === false) {
1690
                params.network.dhcp = false;
1691
            }
1692
            
1693
            var cb = function() {
1694
              callback();
1695
              synnefo.storage.quotas.get('cyclades.network.private').increase();
1696
            }
1697
            return this.api_call(this.path, "create", params, cb);
1698
        },
1699

    
1700
        get_public: function(){
1701
          return this.filter(function(n){return n.get('public')});
1702
        }
1703
    })
1704

    
1705
    models.Images = models.Collection.extend({
1706
        model: models.Image,
1707
        path: 'images',
1708
        details: true,
1709
        noUpdate: true,
1710
        supportIncUpdates: false,
1711
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1712
        meta_labels: {},
1713
        read_method: 'read',
1714

    
1715
        // update collection model with id passed
1716
        // making a direct call to the image
1717
        // api url
1718
        update_unknown_id: function(id, callback) {
1719
            var url = getUrl.call(this) + "/" + id;
1720
            this.api_call(this.path + "/" + id, this.read_method, {
1721
              _options:{
1722
                async:true, 
1723
                skip_api_error:true}
1724
              }, undefined, 
1725
            _.bind(function() {
1726
                if (!this.get(id)) {
1727
                            if (this.fallback_service) {
1728
                        // if current service has fallback_service attribute set
1729
                        // use this service to retrieve the missing image model
1730
                        var tmpservice = new this.fallback_service();
1731
                        tmpservice.update_unknown_id(id, _.bind(function(img){
1732
                            img.attributes.status = "DELETED";
1733
                            this.add(img.attributes);
1734
                            callback(this.get(id));
1735
                        }, this));
1736
                    } else {
1737
                        var title = synnefo.config.image_deleted_title || 'Deleted';
1738
                        // else add a dummy DELETED state image entry
1739
                        this.add({id:id, name:title, size:-1, 
1740
                                  progress:100, status:"DELETED"});
1741
                        callback(this.get(id));
1742
                    }   
1743
                } else {
1744
                    callback(this.get(id));
1745
                }
1746
            }, this), _.bind(function(image, msg, xhr) {
1747
                if (!image) {
1748
                    var title = synnefo.config.image_deleted_title || 'Deleted';
1749
                    this.add({id:id, name:title, size:-1, 
1750
                              progress:100, status:"DELETED"});
1751
                    callback(this.get(id));
1752
                    return;
1753
                }
1754
                var img_data = this._read_image_from_request(image, msg, xhr);
1755
                this.add(img_data);
1756
                callback(this.get(id));
1757
            }, this));
1758
        },
1759

    
1760
        _read_image_from_request: function(image, msg, xhr) {
1761
            return image.image;
1762
        },
1763

    
1764
        parse: function (resp, xhr) {
1765
            var parsed = _.map(resp.images.values, _.bind(this.parse_meta, this));
1766
            parsed = this.fill_owners(parsed);
1767
            return parsed;
1768
        },
1769

    
1770
        fill_owners: function(images) {
1771
            // do translate uuid->displayname if needed
1772
            // store display name in owner attribute for compatibility
1773
            var uuids = [];
1774

    
1775
            var images = _.map(images, function(img, index) {
1776
                if (synnefo.config.translate_uuids) {
1777
                    uuids.push(img['owner']);
1778
                }
1779
                img['owner_uuid'] = img['owner'];
1780
                return img;
1781
            });
1782
            
1783
            if (uuids.length > 0) {
1784
                var handle_results = function(data) {
1785
                    _.each(images, function (img) {
1786
                        img['owner'] = data.uuid_catalog[img['owner_uuid']];
1787
                    });
1788
                }
1789
                // notice the async false
1790
                var uuid_map = this.translate_uuids(uuids, false, 
1791
                                                    handle_results)
1792
            }
1793
            return images;
1794
        },
1795

    
1796
        translate_uuids: function(uuids, async, cb) {
1797
            var url = synnefo.config.user_catalog_url;
1798
            var data = JSON.stringify({'uuids': uuids});
1799
          
1800
            // post to user_catalogs api
1801
            snf.api.sync('create', undefined, {
1802
                url: url,
1803
                data: data,
1804
                async: async,
1805
                success:  cb
1806
            });
1807
        },
1808

    
1809
        get_meta_key: function(img, key) {
1810
            if (img.metadata && img.metadata.values && img.metadata.values[key]) {
1811
                return _.escape(img.metadata.values[key]);
1812
            }
1813
            return undefined;
1814
        },
1815

    
1816
        comparator: function(img) {
1817
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1818
        },
1819

    
1820
        parse_meta: function(img) {
1821
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1822
                if (img[key]) { return };
1823
                img[key] = this.get_meta_key(img, key) || "";
1824
            }, this));
1825
            return img;
1826
        },
1827

    
1828
        active: function() {
1829
            return this.filter(function(img){return img.get('status') != "DELETED"});
1830
        },
1831

    
1832
        predefined: function() {
1833
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
1834
        },
1835
        
1836
        fetch_for_type: function(type, complete, error) {
1837
            this.fetch({update:true, 
1838
                        success: complete, 
1839
                        error: error, 
1840
                        skip_api_error: true });
1841
        },
1842
        
1843
        get_images_for_type: function(type) {
1844
            if (this['get_{0}_images'.format(type)]) {
1845
                return this['get_{0}_images'.format(type)]();
1846
            }
1847

    
1848
            return this.active();
1849
        },
1850

    
1851
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
1852
            var load = false;
1853
            error = onError || function() {};
1854
            function complete(collection) { 
1855
                onComplete(collection.get_images_for_type(type)); 
1856
            }
1857
            
1858
            // do we need to fetch/update current collection entries
1859
            if (load) {
1860
                onStart();
1861
                this.fetch_for_type(type, complete, error);
1862
            } else {
1863
                // fallback to complete
1864
                complete(this);
1865
            }
1866
        }
1867
    })
1868

    
1869
    models.Flavors = models.Collection.extend({
1870
        model: models.Flavor,
1871
        path: 'flavors',
1872
        details: true,
1873
        noUpdate: true,
1874
        supportIncUpdates: false,
1875
        // update collection model with id passed
1876
        // making a direct call to the flavor
1877
        // api url
1878
        update_unknown_id: function(id, callback) {
1879
            var url = getUrl.call(this) + "/" + id;
1880
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
1881
            _.bind(function() {
1882
                this.add({id:id, cpu:"Unknown", ram:"Unknown", disk:"Unknown", name: "Unknown", status:"DELETED"})
1883
            }, this), _.bind(function(flv) {
1884
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
1885
                this.add(flv.flavor);
1886
            }, this));
1887
        },
1888

    
1889
        parse: function (resp, xhr) {
1890
            return _.map(resp.flavors.values, function(o) { o.disk_template = o['SNF:disk_template']; return o});
1891
        },
1892

    
1893
        comparator: function(flv) {
1894
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1895
        },
1896

    
1897
        unavailable_values_for_quotas: function(quotas, flavors) {
1898
            var flavors = flavors || this.active();
1899
            var index = {cpu:[], disk:[], ram:[]};
1900
            
1901
            _.each(flavors, function(el) {
1902

    
1903
                var disk_available = quotas['disk'];
1904
                var disk_size = el.get_disk_size();
1905
                if (index.disk.indexOf(disk_size) == -1) {
1906
                  var disk = el.disk_to_bytes();
1907
                  if (disk > disk_available) {
1908
                    index.disk.push(disk_size);
1909
                  }
1910
                }
1911

    
1912
                var ram_available = quotas['ram'];
1913
                var ram_size = el.get_ram_size();
1914
                if (index.ram.indexOf(ram_size) == -1) {
1915
                  var ram = el.ram_to_bytes();
1916
                  if (ram > ram_available) {
1917
                    index.ram.push(el.get('ram'))
1918
                  }
1919
                }
1920

    
1921
                var cpu = el.get('cpu');
1922
                var cpu_available = quotas['cpu'];
1923
                if (index.ram.indexOf(cpu) == -1) {
1924
                  if (cpu > cpu_available) {
1925
                    index.cpu.push(el.get('cpu'))
1926
                  }
1927
                }
1928
            });
1929
            return index;
1930
        },
1931

    
1932
        unavailable_values_for_image: function(img, flavors) {
1933
            var flavors = flavors || this.active();
1934
            var size = img.get_size();
1935
            
1936
            var index = {cpu:[], disk:[], ram:[]};
1937

    
1938
            _.each(this.active(), function(el) {
1939
                var img_size = size;
1940
                var flv_size = el.get_disk_size();
1941
                if (flv_size < img_size) {
1942
                    if (index.disk.indexOf(flv_size) == -1) {
1943
                        index.disk.push(flv_size);
1944
                    }
1945
                };
1946
            });
1947
            
1948
            return index;
1949
        },
1950

    
1951
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
1952
            if (!filter_list) { filter_list = this.models };
1953
            
1954
            return this.select(function(flv){
1955
                if (flv.get("cpu") == cpu + "" &&
1956
                   flv.get("ram") == mem + "" &&
1957
                   flv.get("disk") == disk + "" &&
1958
                   flv.get("disk_template") == disk_template &&
1959
                   filter_list.indexOf(flv) > -1) { return true; }
1960
            })[0];
1961
        },
1962
        
1963
        get_data: function(lst) {
1964
            var data = {'cpu': [], 'mem':[], 'disk':[]};
1965

    
1966
            _.each(lst, function(flv) {
1967
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
1968
                    data.cpu.push(flv.get("cpu"));
1969
                }
1970
                if (data.mem.indexOf(flv.get("ram")) == -1) {
1971
                    data.mem.push(flv.get("ram"));
1972
                }
1973
                if (data.disk.indexOf(flv.get("disk")) == -1) {
1974
                    data.disk.push(flv.get("disk"));
1975
                }
1976
            })
1977
            
1978
            return data;
1979
        },
1980

    
1981
        active: function() {
1982
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
1983
        }
1984
            
1985
    })
1986

    
1987
    models.VMS = models.Collection.extend({
1988
        model: models.VM,
1989
        path: 'servers',
1990
        details: true,
1991
        copy_image_meta: true,
1992

    
1993
        parse: function (resp, xhr) {
1994
            var data = resp;
1995
            if (!resp) { return [] };
1996
            data = _.filter(_.map(resp.servers.values, _.bind(this.parse_vm_api_data, this)), function(v){return v});
1997
            return data;
1998
        },
1999

    
2000
        parse_vm_api_data: function(data) {
2001
            // do not add non existing DELETED entries
2002
            if (data.status && data.status == "DELETED") {
2003
                if (!this.get(data.id)) {
2004
                    return false;
2005
                }
2006
            }
2007

    
2008
            // OS attribute
2009
            if (this.has_meta(data)) {
2010
                data['OS'] = data.metadata.values.OS || snf.config.unknown_os;
2011
            }
2012
            
2013
            if (!data.diagnostics) {
2014
                data.diagnostics = [];
2015
            }
2016

    
2017
            // network metadata
2018
            data['firewalls'] = {};
2019
            data['nics'] = {};
2020
            data['linked_to'] = [];
2021

    
2022
            if (data['attachments'] && data['attachments'].values) {
2023
                var nics = data['attachments'].values;
2024
                _.each(nics, function(nic) {
2025
                    var net_id = nic.network_id;
2026
                    var index = parseInt(NIC_REGEX.exec(nic.id)[2]);
2027
                    if (data['linked_to'].indexOf(net_id) == -1) {
2028
                        data['linked_to'].push(net_id);
2029
                    }
2030

    
2031
                    data['nics'][nic.id] = nic;
2032
                })
2033
            }
2034
            
2035
            // if vm has no metadata, no metadata object
2036
            // is in json response, reset it to force
2037
            // value update
2038
            if (!data['metadata']) {
2039
                data['metadata'] = {values:{}};
2040
            }
2041

    
2042
            return data;
2043
        },
2044

    
2045
        add: function() {
2046
            ret = models.VMS.__super__.add.apply(this, arguments);
2047
            ret.each(function(r){
2048
                synnefo.storage.nics.update_vm_nics(r);
2049
            });
2050
        },
2051
        
2052
        get_reboot_required: function() {
2053
            return this.filter(function(vm){return vm.get("reboot_required") == true})
2054
        },
2055

    
2056
        has_pending_actions: function() {
2057
            return this.filter(function(vm){return vm.pending_action}).length > 0;
2058
        },
2059

    
2060
        reset_pending_actions: function() {
2061
            this.each(function(vm) {
2062
                vm.clear_pending_action();
2063
            })
2064
        },
2065

    
2066
        do_all_pending_actions: function(success, error) {
2067
            this.each(function(vm) {
2068
                if (vm.has_pending_action()) {
2069
                    vm.call(vm.pending_action, success, error);
2070
                    vm.clear_pending_action();
2071
                }
2072
            })
2073
        },
2074
        
2075
        do_all_reboots: function(success, error) {
2076
            this.each(function(vm) {
2077
                if (vm.get("reboot_required")) {
2078
                    vm.call("reboot", success, error);
2079
                }
2080
            });
2081
        },
2082

    
2083
        reset_reboot_required: function() {
2084
            this.each(function(vm) {
2085
                vm.set({'reboot_required': undefined});
2086
            })
2087
        },
2088
        
2089
        stop_stats_update: function(exclude) {
2090
            var exclude = exclude || [];
2091
            this.each(function(vm) {
2092
                if (exclude.indexOf(vm) > -1) {
2093
                    return;
2094
                }
2095
                vm.stop_stats_update();
2096
            })
2097
        },
2098
        
2099
        has_meta: function(vm_data) {
2100
            return vm_data.metadata && vm_data.metadata.values
2101
        },
2102

    
2103
        has_addresses: function(vm_data) {
2104
            return vm_data.metadata && vm_data.metadata.values
2105
        },
2106

    
2107
        create: function (name, image, flavor, meta, extra, callback) {
2108

    
2109
            if (this.copy_image_meta) {
2110
                if (synnefo.config.vm_image_common_metadata) {
2111
                    _.each(synnefo.config.vm_image_common_metadata, 
2112
                        function(key){
2113
                            if (image.get_meta(key)) {
2114
                                meta[key] = image.get_meta(key);
2115
                            }
2116
                    });
2117
                }
2118

    
2119
                if (image.get("OS")) {
2120
                    meta['OS'] = image.get("OS");
2121
                }
2122
            }
2123
            
2124
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, 
2125
                    metadata:meta}
2126
            opts = _.extend(opts, extra);
2127
            
2128
            var cb = function(data) {
2129
              synnefo.storage.quotas.get('cyclades.vm').increase();
2130
              callback(data);
2131
            }
2132

    
2133
            this.api_call(this.path, "create", {'server': opts}, undefined, 
2134
                          undefined, cb, {critical: true});
2135
        },
2136

    
2137
        load_missing_images: function(callback) {
2138
          var missing_ids = [];
2139
          this.each(function(el) {
2140
            var imgid = el.get("imageRef");
2141
            var existing = synnefo.storage.images.get(imgid);
2142
            if (!existing && missing_ids.indexOf(imgid) == -1) {
2143
                missing_ids.push(imgid);
2144
                synnefo.storage.images.update_unknown_id(imgid, function(){});
2145
            }
2146
          });
2147
          callback(missing_ids);
2148
        }
2149

    
2150
    })
2151
    
2152
    models.NIC = models.Model.extend({
2153
        
2154
        initialize: function() {
2155
            models.NIC.__super__.initialize.apply(this, arguments);
2156
            this.pending_for_firewall = false;
2157
            this.bind("change:firewallProfile", _.bind(this.check_firewall, this));
2158
            this.bind("change:pending_firewall", function(nic) {
2159
                nic.get_network().update_state();
2160
            });
2161
            this.get_vm().bind("remove", function(){
2162
                try {
2163
                    this.collection.remove(this);
2164
                } catch (err) {};
2165
            }, this);
2166
            this.get_network().bind("remove", function(){
2167
                try {
2168
                    this.collection.remove(this);
2169
                } catch (err) {};
2170
            }, this);
2171

    
2172
        },
2173

    
2174
        get_vm: function() {
2175
            return synnefo.storage.vms.get(parseInt(this.get('vm_id')));
2176
        },
2177

    
2178
        get_network: function() {
2179
            return synnefo.storage.networks.get(this.get('network_id'));
2180
        },
2181

    
2182
        get_v6_address: function() {
2183
            return this.get("ipv6");
2184
        },
2185

    
2186
        get_v4_address: function() {
2187
            return this.get("ipv4");
2188
        },
2189

    
2190
        set_firewall: function(value, callback, error, options) {
2191
            var net_id = this.get('network_id');
2192
            var self = this;
2193

    
2194
            // api call data
2195
            var payload = {"firewallProfile":{"profile":value}};
2196
            payload._options = _.extend({critical: false}, options);
2197
            
2198
            this.set({'pending_firewall': value});
2199
            this.set({'pending_firewall_sending': true});
2200
            this.set({'pending_firewall_from': this.get('firewallProfile')});
2201

    
2202
            var success_cb = function() {
2203
                if (callback) {
2204
                    callback();
2205
                }
2206
                self.set({'pending_firewall_sending': false});
2207
            };
2208

    
2209
            var error_cb = function() {
2210
                self.reset_pending_firewall();
2211
            }
2212
            
2213
            this.get_vm().api_call(this.get_vm().api_path() + "/action", "create", payload, success_cb, error_cb);
2214
        },
2215

    
2216
        reset_pending_firewall: function() {
2217
            this.set({'pending_firewall': false});
2218
            this.set({'pending_firewall': false});
2219
        },
2220

    
2221
        check_firewall: function() {
2222
            var firewall = this.get('firewallProfile');
2223
            var pending = this.get('pending_firewall');
2224
            var previous = this.get('pending_firewall_from');
2225
            if (previous != firewall) { this.get_vm().require_reboot() };
2226
            this.reset_pending_firewall();
2227
        }
2228
        
2229
    });
2230

    
2231
    models.NICs = models.Collection.extend({
2232
        model: models.NIC,
2233
        
2234
        add_or_update: function(nic_id, data, vm) {
2235
            var params = _.clone(data);
2236
            var vm;
2237
            params.attachment_id = params.id;
2238
            params.id = params.id + '-' + params.network_id;
2239
            params.vm_id = parseInt(NIC_REGEX.exec(nic_id)[1]);
2240

    
2241
            if (!this.get(params.id)) {
2242
                this.add(params);
2243
                var nic = this.get(params.id);
2244
                vm = nic.get_vm();
2245
                nic.get_network().decrease_connecting();
2246
                nic.bind("remove", function() {
2247
                    nic.set({"removing": 0});
2248
                    if (this.get_network()) {
2249
                        // network might got removed before nic
2250
                        nic.get_network().update_state();
2251
                    }
2252
                });
2253

    
2254
            } else {
2255
                this.get(params.id).set(params);
2256
                vm = this.get(params.id).get_vm();
2257
            }
2258
            
2259
            // vm nics changed, trigger vm update
2260
            if (vm) { vm.trigger("change", vm)};
2261
        },
2262
        
2263
        reset_nics: function(nics, filter_attr, filter_val) {
2264
            var nics_to_check = this.filter(function(nic) {
2265
                return nic.get(filter_attr) == filter_val;
2266
            });
2267
            
2268
            _.each(nics_to_check, function(nic) {
2269
                if (nics.indexOf(nic.get('id')) == -1) {
2270
                    this.remove(nic);
2271
                } else {
2272
                }
2273
            }, this);
2274
        },
2275

    
2276
        update_vm_nics: function(vm) {
2277
            var nics = vm.get('nics');
2278
            this.reset_nics(_.map(nics, function(nic, key){
2279
                return key + "-" + nic.network_id;
2280
            }), 'vm_id', vm.id);
2281

    
2282
            _.each(nics, function(val, key) {
2283
                var net = synnefo.storage.networks.get(val.network_id);
2284
                if (net && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
2285
                    this.add_or_update(key, vm.get('nics')[key], vm);
2286
                }
2287
            }, this);
2288
        },
2289

    
2290
        update_net_nics: function(net) {
2291
            var nics = net.get('nics');
2292
            this.reset_nics(_.map(nics, function(nic, key){
2293
                return key + "-" + net.get('id');
2294
            }), 'network_id', net.id);
2295

    
2296
            _.each(nics, function(val, key) {
2297
                var vm = synnefo.storage.vms.get(val.vm_id);
2298
                if (vm && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
2299
                    this.add_or_update(key, vm.get('nics')[key], vm);
2300
                }
2301
            }, this);
2302
        }
2303
    });
2304

    
2305
    models.PublicKey = models.Model.extend({
2306
        path: 'keys',
2307
        base_url: '/ui/userdata',
2308
        details: false,
2309
        noUpdate: true,
2310

    
2311

    
2312
        get_public_key: function() {
2313
            return cryptico.publicKeyFromString(this.get("content"));
2314
        },
2315

    
2316
        get_filename: function() {
2317
            return "{0}.pub".format(this.get("name"));
2318
        },
2319

    
2320
        identify_type: function() {
2321
            try {
2322
                var cont = snf.util.validatePublicKey(this.get("content"));
2323
                var type = cont.split(" ")[0];
2324
                return synnefo.util.publicKeyTypesMap[type];
2325
            } catch (err) { return false };
2326
        }
2327

    
2328
    })
2329
    
2330
    models.PublicKeys = models.Collection.extend({
2331
        model: models.PublicKey,
2332
        details: false,
2333
        path: 'keys',
2334
        base_url: '/ui/userdata',
2335
        noUpdate: true,
2336

    
2337
        generate_new: function(success, error) {
2338
            snf.api.sync('create', undefined, {
2339
                url: getUrl.call(this, this.base_url) + "/generate", 
2340
                success: success, 
2341
                error: error,
2342
                skip_api_error: true
2343
            });
2344
        },
2345

    
2346
        add_crypto_key: function(key, success, error, options) {
2347
            var options = options || {};
2348
            var m = new models.PublicKey();
2349

    
2350
            // guess a name
2351
            var name_tpl = "my generated public key";
2352
            var name = name_tpl;
2353
            var name_count = 1;
2354
            
2355
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
2356
                name = name_tpl + " " + name_count;
2357
                name_count++;
2358
            }
2359
            
2360
            m.set({name: name});
2361
            m.set({content: key});
2362
            
2363
            options.success = function () { return success(m) };
2364
            options.errror = error;
2365
            options.skip_api_error = true;
2366
            
2367
            this.create(m.attributes, options);
2368
        }
2369
    });
2370

    
2371
  
2372
    models.Quota = models.Model.extend({
2373

    
2374
        initialize: function() {
2375
            models.Quota.__super__.initialize.apply(this, arguments);
2376
            this.bind("change", this.check, this);
2377
            this.check();
2378
        },
2379
        
2380
        check: function() {
2381
            var usage, limit;
2382
            usage = this.get('usage');
2383
            limit = this.get('limit');
2384
            if (usage >= limit) {
2385
                this.trigger("available");
2386
            } else {
2387
                this.trigger("unavailable");
2388
            }
2389
        },
2390

    
2391
        increase: function(val) {
2392
            if (val === undefined) { val = 1};
2393
            this.set({'usage': this.get('usage') + val})
2394
        },
2395

    
2396
        decrease: function(val) {
2397
            if (val === undefined) { val = 1};
2398
            this.set({'usage': this.get('usage') - val})
2399
        },
2400

    
2401
        can_consume: function() {
2402
            var usage, limit;
2403
            usage = this.get('usage');
2404
            limit = this.get('limit');
2405
            if (usage >= limit) {
2406
                return false
2407
            } else {
2408
                return true
2409
            }
2410
        },
2411
        
2412
        is_bytes: function() {
2413
            return this.get('resource').get('unit') == 'bytes';
2414
        },
2415
        
2416
        get_available: function() {
2417
            var value = this.get('limit') - this.get('usage');
2418
            if (value < 0) { return value }
2419
            return value
2420
        },
2421

    
2422
        get_readable: function(key) {
2423
            var value;
2424
            if (key == 'available') {
2425
                value = this.get_available();
2426
            } else {
2427
                value = this.get(key)
2428
            }
2429
            if (!this.is_bytes()) {
2430
              return value + "";
2431
            }
2432
            return snf.util.readablizeBytes(value);
2433
        }
2434
    });
2435

    
2436
    models.Quotas = models.Collection.extend({
2437
        model: models.Quota,
2438
        api_type: 'accounts',
2439
        path: 'quotas',
2440
        parse: function(resp) {
2441
            return _.map(resp.system, function(value, key) {
2442
                var available = (value.limit - value.usage) || 0;
2443
                return _.extend(value, {'name': key, 'id': key, 
2444
                          'available': available,
2445
                          'resource': snf.storage.resources.get(key)});
2446
            })
2447
        }
2448
    })
2449

    
2450
    models.Resource = models.Model.extend({
2451
        api_type: 'accounts',
2452
        path: 'resources'
2453
    });
2454

    
2455
    models.Resources = models.Collection.extend({
2456
        api_type: 'accounts',
2457
        path: 'resources',
2458
        model: models.Network,
2459

    
2460
        parse: function(resp) {
2461
            return _.map(resp, function(value, key) {
2462
                return _.extend(value, {'name': key, 'id': key});
2463
            })
2464
        }
2465
    });
2466
    
2467
    // storage initialization
2468
    snf.storage.images = new models.Images();
2469
    snf.storage.flavors = new models.Flavors();
2470
    snf.storage.networks = new models.Networks();
2471
    snf.storage.vms = new models.VMS();
2472
    snf.storage.keys = new models.PublicKeys();
2473
    snf.storage.nics = new models.NICs();
2474
    snf.storage.resources = new models.Resources();
2475
    snf.storage.quotas = new models.Quotas();
2476

    
2477
})(this);