Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (93.7 kB)

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

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

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

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

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

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

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

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

    
111
        url: function(options) {
112
            return getUrl.call(this, this.base_url) + "/" + this.id;
113
        },
114

    
115
        api_path: function(options) {
116
            return this.path + "/" + this.id;
117
        },
118

    
119
        parse: function(resp, xhr) {
120
        },
121

    
122
        remove: function(complete, error, success) {
123
            this.api_call(this.api_path(), "delete", undefined, complete, error, success);
124
        },
125

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

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

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

    
159
        initialize: function() {
160
            models.Collection.__super__.initialize.apply(this, arguments);
161
            this.api_call = _.bind(this.api.call, this);
162
        },
163

    
164
        url: function(options, method) {
165
            return getUrl.call(this, this.base_url) + (
166
                    options.details || this.details && method != 'create' ? '/detail' : '');
167
        },
168

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

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

    
197
        get_fetcher: function(interval, increase, fast, increase_after_calls, max, initial_call, params) {
198
            var fetch_params = params || {};
199
            var handler_options = {};
200

    
201
            fetch_params.skips_timeouts = true;
202
            handler_options.interval = interval;
203
            handler_options.increase = increase;
204
            handler_options.fast = fast;
205
            handler_options.increase_after_calls = increase_after_calls;
206
            handler_options.max= max;
207
            handler_options.id = "collection id";
208

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

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

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

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

    
255
        get_meta_keys: function() {
256
            if (this.get('metadata') && this.get('metadata')) {
257
                return _.keys(this.get('metadata'));
258
            } else {
259
                return [];
260
            }
261
        },
262

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

    
267
        get_owner_uuid: function() {
268
            return this.get('owner_uuid');
269
        },
270

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

    
276
        owned_by: function(user) {
277
          if (!user) { user = synnefo.user }
278
          return user.get_username() == this.get('owner_uuid');
279
        },
280

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

    
297
        get_os: function() {
298
            return this.get_meta('OS');
299
        },
300

    
301
        get_gui: function() {
302
            return this.get_meta('GUI');
303
        },
304

    
305
        get_created_users: function() {
306
            try {
307
              var users = this.get_meta('users').split(" ");
308
            } catch (err) { users = null }
309
            if (!users) {
310
                var osfamily = this.get_meta('osfamily');
311
                if (osfamily == 'windows') { 
312
                  users = ['Administrator'];
313
                } else {
314
                  users = ['root'];
315
                }
316
            }
317
            return users;
318
        },
319

    
320
        get_sort_order: function() {
321
            return parseInt(this.get('metadata') ? this.get('metadata').sortorder : -1)
322
        },
323

    
324
        get_vm: function() {
325
            var vm_id = this.get("serverRef");
326
            var vm = undefined;
327
            vm = storage.vms.get(vm_id);
328
            return vm;
329
        },
330

    
331
        is_public: function() {
332
            return this.get('is_public') == undefined ? true : this.get('is_public');
333
        },
334

    
335
        is_deleted: function() {
336
            return this.get('status') == "DELETED"
337
        },
338
        
339
        ssh_keys_paths: function() {
340
            return _.map(this.get_created_users(), function(username) {
341
                prepend = '';
342
                if (username != 'root') {
343
                    prepend = '/home'
344
                }
345
                return {'user': username, 'path': '{1}/{0}/.ssh/authorized_keys'.format(username, 
346
                                                             prepend)};
347
            });
348
        },
349

    
350
        _supports_ssh: function() {
351
            if (synnefo.config.support_ssh_os_list.indexOf(this.get_os()) > -1) {
352
                return true;
353
            }
354
            if (this.get_meta('osfamily') == 'linux') {
355
              return true;
356
            }
357
            return false;
358
        },
359

    
360
        supports: function(feature) {
361
            if (feature == "ssh") {
362
                return this._supports_ssh()
363
            }
364
            return false;
365
        },
366

    
367
        personality_data_for_keys: function(keys) {
368
            return _.map(this.ssh_keys_paths(), function(pathinfo) {
369
                var contents = '';
370
                _.each(keys, function(key){
371
                    contents = contents + key.get("content") + "\n"
372
                });
373
                contents = $.base64.encode(contents);
374

    
375
                return {
376
                    path: pathinfo.path,
377
                    contents: contents,
378
                    mode: 0600,
379
                    owner: pathinfo.user
380
                }
381
            });
382
        }
383
    });
384

    
385
    // Flavor model
386
    models.Flavor = models.Model.extend({
387
        path: 'flavors',
388

    
389
        details_string: function() {
390
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
391
        },
392

    
393
        get_disk_size: function() {
394
            return parseInt(this.get("disk") * 1000)
395
        },
396

    
397
        get_ram_size: function() {
398
            return parseInt(this.get("ram"))
399
        },
400

    
401
        get_disk_template_info: function() {
402
            var info = snf.config.flavors_disk_templates_info[this.get("disk_template")];
403
            if (!info) {
404
                info = { name: this.get("disk_template"), description:'' };
405
            }
406
            return info
407
        },
408

    
409
        disk_to_bytes: function() {
410
            return parseInt(this.get("disk")) * 1024 * 1024 * 1024;
411
        },
412

    
413
        ram_to_bytes: function() {
414
            return parseInt(this.get("ram")) * 1024 * 1024;
415
        },
416

    
417
    });
418
    
419
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
420
    _.extend(models.ParamsList.prototype, bb.Events, {
421

    
422
        initialize: function(parent, param_name) {
423
            this.parent = parent;
424
            this.actions = {};
425
            this.param_name = param_name;
426
            this.length = 0;
427
        },
428
        
429
        has_action: function(action) {
430
            return this.actions[action] ? true : false;
431
        },
432
            
433
        _parse_params: function(arguments) {
434
            if (arguments.length <= 1) {
435
                return [];
436
            }
437

    
438
            var args = _.toArray(arguments);
439
            return args.splice(1);
440
        },
441

    
442
        contains: function(action, params) {
443
            params = this._parse_params(arguments);
444
            var has_action = this.has_action(action);
445
            if (!has_action) { return false };
446

    
447
            var paramsEqual = false;
448
            _.each(this.actions[action], function(action_params) {
449
                if (_.isEqual(action_params, params)) {
450
                    paramsEqual = true;
451
                }
452
            });
453
                
454
            return paramsEqual;
455
        },
456
        
457
        is_empty: function() {
458
            return _.isEmpty(this.actions);
459
        },
460

    
461
        add: function(action, params) {
462
            params = this._parse_params(arguments);
463
            if (this.contains.apply(this, arguments)) { return this };
464
            var isnew = false
465
            if (!this.has_action(action)) {
466
                this.actions[action] = [];
467
                isnew = true;
468
            };
469

    
470
            this.actions[action].push(params);
471
            this.parent.trigger("change:" + this.param_name, this.parent, this);
472
            if (isnew) {
473
                this.trigger("add", action, params);
474
            } else {
475
                this.trigger("change", action, params);
476
            }
477
            return this;
478
        },
479
        
480
        remove_all: function(action) {
481
            if (this.has_action(action)) {
482
                delete this.actions[action];
483
                this.parent.trigger("change:" + this.param_name, this.parent, this);
484
                this.trigger("remove", action);
485
            }
486
            return this;
487
        },
488

    
489
        reset: function() {
490
            this.actions = {};
491
            this.parent.trigger("change:" + this.param_name, this.parent, this);
492
            this.trigger("reset");
493
            this.trigger("remove");
494
        },
495

    
496
        remove: function(action, params) {
497
            params = this._parse_params(arguments);
498
            if (!this.has_action(action)) { return this };
499
            var index = -1;
500
            _.each(this.actions[action], _.bind(function(action_params) {
501
                if (_.isEqual(action_params, params)) {
502
                    index = this.actions[action].indexOf(action_params);
503
                }
504
            }, this));
505
            
506
            if (index > -1) {
507
                this.actions[action].splice(index, 1);
508
                if (_.isEmpty(this.actions[action])) {
509
                    delete this.actions[action];
510
                }
511
                this.parent.trigger("change:" + this.param_name, this.parent, this);
512
                this.trigger("remove", action, params);
513
            }
514
        }
515

    
516
    });
517

    
518
    // Image model
519
    models.Network = models.Model.extend({
520
        path: 'networks',
521
        has_status: true,
522
        defaults: {'connecting':0},
523
        
524
        initialize: function() {
525
            var ret = models.Network.__super__.initialize.apply(this, arguments);
526
            this.set({"actions": new models.ParamsList(this, "actions")});
527
            this.update_state();
528
            this.bind("change:nics", _.bind(synnefo.storage.nics.update_net_nics, synnefo.storage.nics));
529
            this.bind("change:status", _.bind(this.update_state, this));
530
            return ret;
531
        },
532
        
533
        is_deleted: function() {
534
          return this.get('status') == 'DELETED';
535
        },
536

    
537
        toJSON: function() {
538
            var attrs = _.clone(this.attributes);
539
            attrs.actions = _.clone(this.get("actions").actions);
540
            return attrs;
541
        },
542
        
543
        set_state: function(val) {
544
            if (val == "PENDING" && this.get("state") == "DESTORY") {
545
                return "DESTROY";
546
            }
547
            return val;
548
        },
549

    
550
        update_state: function() {
551
            if (this.get("connecting") > 0) {
552
                this.set({state: "CONNECTING"});
553
                return
554
            }
555
            
556
            if (this.get_nics(function(nic){ return nic.get("removing") == 1}).length > 0) {
557
                this.set({state: "DISCONNECTING"});
558
                return
559
            }   
560
            
561
            if (this.contains_firewalling_nics() > 0) {
562
                this.set({state: "FIREWALLING"});
563
                return
564
            }   
565
            
566
            if (this.get("state") == "DESTROY") { 
567
                this.set({"destroyed":1});
568
            }
569
            
570
            this.set({state:this.get('status')});
571
        },
572

    
573
        is_public: function() {
574
            return this.get("public");
575
        },
576

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

    
587
        increase_connecting: function() {
588
            var conn = this.get("connecting");
589
            if (!conn) { conn = 0 };
590
            conn++;
591
            this.set({"connecting": conn});
592
            this.update_state();
593
        },
594

    
595
        connected_to: function(vm) {
596
            return this.get('linked_to').indexOf(""+vm.id) > -1;
597
        },
598

    
599
        connected_with_nic_id: function(nic_id) {
600
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
601
        },
602

    
603
        get_nics: function(filter) {
604
            var nics = synnefo.storage.nics.filter(function(nic) {
605
                return nic.get('network_id') == this.id;
606
            }, this);
607

    
608
            if (filter) {
609
                return _.filter(nics, filter);
610
            }
611
            return nics;
612
        },
613

    
614
        contains_firewalling_nics: function() {
615
            return this.get_nics(function(n){return n.get('pending_firewall')}).length
616
        },
617

    
618
        call: function(action, params, success, error) {
619
            if (action == "destroy") {
620
                var previous_state = this.get('state');
621
                var previous_status = this.get('status');
622

    
623
                this.set({state:"DESTROY"});
624

    
625
                var _success = _.bind(function() {
626
                    if (success) { success() };
627
                    synnefo.storage.quotas.get('cyclades.network.private').decrease();
628
                }, this);
629
                var _error = _.bind(function() {
630
                    this.set({state: previous_state, status: previous_status})
631
                    if (error) { error() };
632
                }, this);
633

    
634
                this.remove(undefined, _error, _success);
635
            }
636
            
637
            if (action == "disconnect") {
638
                if (this.get("state") == "DESTROY") {
639
                    return;
640
                }
641
                
642
                _.each(params, _.bind(function(nic_id) {
643
                    var nic = snf.storage.nics.get(nic_id);
644
                    this.get("actions").remove("disconnect", nic_id);
645
                    if (nic) {
646
                        this.remove_nic(nic, success, error);
647
                    }
648
                }, this));
649
            }
650
        },
651

    
652
        add_vm: function (vm, callback, error, options) {
653
            var payload = {add:{serverRef:"" + vm.id}};
654
            payload._options = options || {};
655
            return this.api_call(this.api_path() + "/action", "create", 
656
                                 payload,
657
                                 undefined,
658
                                 error,
659
                                 _.bind(function(){
660
                                     //this.vms.add_pending(vm.id);
661
                                     this.increase_connecting();
662
                                     if (callback) {callback()}
663
                                 },this), error);
664
        },
665

    
666
        remove_nic: function (nic, callback, error, options) {
667
            var payload = {remove:{attachment:"" + nic.get("attachment_id")}};
668
            payload._options = options || {};
669
            return this.api_call(this.api_path() + "/action", "create", 
670
                                 payload,
671
                                 undefined,
672
                                 error,
673
                                 _.bind(function(){
674
                                     nic.set({"removing": 1});
675
                                     nic.get_network().update_state();
676
                                     //this.vms.add_pending_for_remove(vm.id);
677
                                     if (callback) {callback()}
678
                                 },this), error);
679
        },
680

    
681
        rename: function(name, callback) {
682
            return this.api_call(this.api_path(), "update", {
683
                network:{name:name}, 
684
                _options:{
685
                    critical: false, 
686
                    error_params:{
687
                        title: "Network action failed",
688
                        ns: "Networks",
689
                        extra_details: {"Network id": this.id}
690
                    }
691
                }}, callback);
692
        },
693

    
694
        get_connectable_vms: function() {
695
            return storage.vms.filter(function(vm){
696
                return !vm.in_error_state() && !vm.is_building() && !vm.is_rebooting();
697
            });
698
        },
699

    
700
        state_message: function() {
701
            if (this.get("state") == "ACTIVE" && !this.is_public()) {
702
                if (this.get("cidr") && this.get("dhcp") == true) {
703
                    return this.get("cidr");
704
                } else {
705
                    return "Private network";
706
                }
707
            }
708
            if (this.get("state") == "ACTIVE" && this.is_public()) {
709
                  return "Public network";
710
            }
711

    
712
            return models.Network.STATES[this.get("state")];
713
        },
714

    
715
        in_progress: function() {
716
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
717
        },
718

    
719
        do_all_pending_actions: function(success, error) {
720
          var params, actions, action_params;
721
          actions = _.clone(this.get("actions").actions);
722
            _.each(actions, _.bind(function(params, action) {
723
                action_params = _.map(actions[action], function(a){ return _.clone(a)});
724
                _.each(action_params, _.bind(function(params) {
725
                    this.call(action, params, success, error);
726
                }, this));
727
            }, this));
728
            this.get("actions").reset();
729
        }
730
    });
731
    
732
    models.Network.STATES = {
733
        'ACTIVE': 'Private network',
734
        'CONNECTING': 'Connecting...',
735
        'DISCONNECTING': 'Disconnecting...',
736
        'FIREWALLING': 'Firewall update...',
737
        'DESTROY': 'Destroying...',
738
        'PENDING': 'Pending...',
739
        'ERROR': 'Error'
740
    }
741

    
742
    models.Network.STATES_TRANSITIONS = {
743
        'CONNECTING': ['ACTIVE'],
744
        'DISCONNECTING': ['ACTIVE'],
745
        'PENDING': ['ACTIVE'],
746
        'FIREWALLING': ['ACTIVE']
747
    }
748

    
749
    // Virtualmachine model
750
    models.VM = models.Model.extend({
751

    
752
        path: 'servers',
753
        has_status: true,
754
        initialize: function(params) {
755
            
756
            this.pending_firewalls = {};
757
            
758
            models.VM.__super__.initialize.apply(this, arguments);
759

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

    
777
            // initialize interval
778
            this.init_stats_intervals(this.stats_update_interval);
779
            
780
            // handle progress message on instance change
781
            this.bind("change", _.bind(this.update_status_message, this));
782
            this.bind("change:task_state", _.bind(this.update_status, this));
783
            // force update of progress message
784
            this.update_status_message(true);
785
            
786
            // default values
787
            this.bind("change:state", _.bind(function(){
788
                if (this.state() == "DESTROY") { 
789
                    this.handle_destroy() 
790
                }
791
            }, this));
792

    
793
            this.bind("change:nics", _.bind(synnefo.storage.nics.update_vm_nics, synnefo.storage.nics));
794
        },
795

    
796
        status: function(st) {
797
            if (!st) { return this.get("status")}
798
            return this.set({status:st});
799
        },
800
        
801
        update_status: function() {
802
            this.set_status(this.get('status'));
803
        },
804

    
805
        set_status: function(st) {
806
            var new_state = this.state_for_api_status(st);
807
            var transition = false;
808

    
809
            if (this.state() != new_state) {
810
                if (models.VM.STATES_TRANSITIONS[this.state()]) {
811
                    transition = this.state();
812
                }
813
            }
814
            
815
            // call it silently to avoid double change trigger
816
            var state = this.state_for_api_status(st);
817
            this.set({'state': state}, {silent: true});
818
            
819
            // trigger transition
820
            if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { 
821
                this.trigger("transition", {from:transition, to:new_state}) 
822
            };
823
            return st;
824
        },
825
            
826
        get_diagnostics: function(success) {
827
            this.__make_api_call(this.get_diagnostics_url(),
828
                                 "read", // create so that sync later uses POST to make the call
829
                                 null, // payload
830
                                 function(data) {
831
                                     success(data);
832
                                 },  
833
                                 null, 'diagnostics');
834
        },
835

    
836
        has_diagnostics: function() {
837
            return this.get("diagnostics") && this.get("diagnostics").length;
838
        },
839

    
840
        get_progress_info: function() {
841
            // details about progress message
842
            // contains a list of diagnostic messages
843
            return this.get("status_messages");
844
        },
845

    
846
        get_status_message: function() {
847
            return this.get('status_message');
848
        },
849
        
850
        // extract status message from diagnostics
851
        status_message_from_diagnostics: function(diagnostics) {
852
            var valid_sources_map = synnefo.config.diagnostics_status_messages_map;
853
            var valid_sources = valid_sources_map[this.get('status')];
854
            if (!valid_sources) { return null };
855
            
856
            // filter messsages based on diagnostic source
857
            var messages = _.filter(diagnostics, function(diag) {
858
                return valid_sources.indexOf(diag.source) > -1;
859
            });
860

    
861
            var msg = messages[0];
862
            if (msg) {
863
              var message = msg.message;
864
              var message_tpl = snf.config.diagnostic_messages_tpls[msg.source];
865

    
866
              if (message_tpl) {
867
                  message = message_tpl.replace('MESSAGE', msg.message);
868
              }
869
              return message;
870
            }
871
            
872
            // no message to display, but vm in build state, display
873
            // finalizing message.
874
            if (this.is_building() == 'BUILD') {
875
                return synnefo.config.BUILDING_MESSAGES['FINAL'];
876
            }
877
            return null;
878
        },
879

    
880
        update_status_message: function(force) {
881
            // update only if one of the specified attributes has changed
882
            if (
883
              !this.keysChanged(['diagnostics', 'progress', 'status', 'state'])
884
                && !force
885
            ) { return };
886
            
887
            // if user requested to destroy the vm set the appropriate 
888
            // message.
889
            if (this.get('state') == "DESTROY") { 
890
                message = "Terminating..."
891
                this.set({status_message: message})
892
                return;
893
            }
894
            
895
            // set error message, if vm has diagnostic message display it as
896
            // progress message
897
            if (this.in_error_state()) {
898
                var d = this.get('diagnostics');
899
                if (d && d.length) {
900
                    var message = this.status_message_from_diagnostics(d);
901
                    this.set({status_message: message});
902
                } else {
903
                    this.set({status_message: null});
904
                }
905
                return;
906
            }
907
            
908
            // identify building status message
909
            if (this.is_building()) {
910
                var self = this;
911
                var success = function(msg) {
912
                    self.set({status_message: msg});
913
                }
914
                this.get_building_status_message(success);
915
                return;
916
            }
917

    
918
            this.set({status_message:null});
919
        },
920
            
921
        // get building status message. Asynchronous function since it requires
922
        // access to vm image.
923
        get_building_status_message: function(callback) {
924
            // no progress is set, vm is in initial build status
925
            var progress = this.get("progress");
926
            if (progress == 0 || !progress) {
927
                return callback(BUILDING_MESSAGES['INIT']);
928
            }
929
            
930
            // vm has copy progress, display copy percentage
931
            if (progress > 0 && progress <= 99) {
932
                this.get_copy_details(true, undefined, _.bind(
933
                    function(details){
934
                        callback(BUILDING_MESSAGES['COPY'].format(details.copy, 
935
                                                           details.size, 
936
                                                           details.progress));
937
                }, this));
938
                return;
939
            }
940

    
941
            // copy finished display FINAL message or identify status message
942
            // from diagnostics.
943
            if (progress >= 100) {
944
                if (!this.has_diagnostics()) {
945
                        callback(BUILDING_MESSAGES['FINAL']);
946
                } else {
947
                        var d = this.get("diagnostics");
948
                        var msg = this.status_message_from_diagnostics(d);
949
                        if (msg) {
950
                              callback(msg);
951
                        }
952
                }
953
            }
954
        },
955

    
956
        get_copy_details: function(human, image, callback) {
957
            var human = human || false;
958
            var image = image || this.get_image(_.bind(function(image){
959
                var progress = this.get('progress');
960
                var size = image.get_size();
961
                var size_copied = (size * progress / 100).toFixed(2);
962
                
963
                if (human) {
964
                    size = util.readablizeBytes(size*1024*1024);
965
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
966
                }
967

    
968
                callback({'progress': progress, 'size': size, 'copy': size_copied})
969
            }, this));
970
        },
971

    
972
        start_stats_update: function(force_if_empty) {
973
            var prev_state = this.do_update_stats;
974

    
975
            this.do_update_stats = true;
976
            
977
            // fetcher initialized ??
978
            if (!this.stats_fetcher) {
979
                this.init_stats_intervals();
980
            }
981

    
982

    
983
            // fetcher running ???
984
            if (!this.stats_fetcher.running || !prev_state) {
985
                this.stats_fetcher.start();
986
            }
987

    
988
            if (force_if_empty && this.get("stats") == undefined) {
989
                this.update_stats(true);
990
            }
991
        },
992

    
993
        stop_stats_update: function(stop_calls) {
994
            this.do_update_stats = false;
995

    
996
            if (stop_calls) {
997
                this.stats_fetcher.stop();
998
            }
999
        },
1000

    
1001
        // clear and reinitialize update interval
1002
        init_stats_intervals: function (interval) {
1003
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
1004
            this.stats_fetcher.start();
1005
        },
1006
        
1007
        get_stats_fetcher: function(timeout) {
1008
            var cb = _.bind(function(data){
1009
                this.update_stats();
1010
            }, this);
1011
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
1012
            return fetcher;
1013
        },
1014

    
1015
        // do the api call
1016
        update_stats: function(force) {
1017
            // do not update stats if flag not set
1018
            if ((!this.do_update_stats && !force) || this.updating_stats) {
1019
                return;
1020
            }
1021

    
1022
            // make the api call, execute handle_stats_update on sucess
1023
            // TODO: onError handler ???
1024
            stats_url = this.url() + "/stats";
1025
            this.updating_stats = true;
1026
            this.sync("read", this, {
1027
                handles_error:true, 
1028
                url: stats_url, 
1029
                refresh:true, 
1030
                success: _.bind(this.handle_stats_update, this),
1031
                error: _.bind(this.handle_stats_error, this),
1032
                complete: _.bind(function(){this.updating_stats = false;}, this),
1033
                critical: false,
1034
                log_error: false,
1035
                skips_timeouts: true
1036
            });
1037
        },
1038

    
1039
        get_stats_image: function(stat, type) {
1040
        },
1041
        
1042
        _set_stats: function(stats) {
1043
            var silent = silent === undefined ? false : silent;
1044
            // unavailable stats while building
1045
            if (this.get("status") == "BUILD") { 
1046
                this.stats_available = false;
1047
            } else { this.stats_available = true; }
1048

    
1049
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
1050
            
1051
            this.set({stats: stats}, {silent:true});
1052
            this.trigger("stats:update", stats);
1053
        },
1054

    
1055
        unbind: function() {
1056
            models.VM.__super__.unbind.apply(this, arguments);
1057
        },
1058

    
1059
        can_resize: function() {
1060
          return this.get('status') == 'STOPPED';
1061
        },
1062

    
1063
        handle_stats_error: function() {
1064
            stats = {};
1065
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1066
                stats[k] = false;
1067
            });
1068

    
1069
            this.set({'stats': stats});
1070
        },
1071

    
1072
        // this method gets executed after a successful vm stats api call
1073
        handle_stats_update: function(data) {
1074
            var self = this;
1075
            // avoid browser caching
1076
            
1077
            if (data.stats && _.size(data.stats) > 0) {
1078
                var ts = $.now();
1079
                var stats = data.stats;
1080
                var images_loaded = 0;
1081
                var images = {};
1082

    
1083
                function check_images_loaded() {
1084
                    images_loaded++;
1085

    
1086
                    if (images_loaded == 4) {
1087
                        self._set_stats(images);
1088
                    }
1089
                }
1090
                _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1091
                    
1092
                    stats[k] = stats[k] + "?_=" + ts;
1093
                    
1094
                    var stat = k.slice(0,3);
1095
                    var type = k.slice(3,6) == "Bar" ? "bar" : "time";
1096
                    var img = $("<img />");
1097
                    var val = stats[k];
1098
                    
1099
                    // load stat image to a temporary dom element
1100
                    // update model stats on image load/error events
1101
                    img.load(function() {
1102
                        images[k] = val;
1103
                        check_images_loaded();
1104
                    });
1105

    
1106
                    img.error(function() {
1107
                        images[stat + type] = false;
1108
                        check_images_loaded();
1109
                    });
1110

    
1111
                    img.attr({'src': stats[k]});
1112
                })
1113
                data.stats = stats;
1114
            }
1115

    
1116
            // do we need to change the interval ??
1117
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
1118
                this.stats_update_interval = data.stats.refresh * 1000;
1119
                this.stats_fetcher.interval = this.stats_update_interval;
1120
                this.stats_fetcher.maximum_interval = this.stats_update_interval;
1121
                this.stats_fetcher.stop();
1122
                this.stats_fetcher.start(false);
1123
            }
1124
        },
1125

    
1126
        // helper method that sets the do_update_stats
1127
        // in the future this method could also make an api call
1128
        // immediaetly if needed
1129
        enable_stats_update: function() {
1130
            this.do_update_stats = true;
1131
        },
1132
        
1133
        handle_destroy: function() {
1134
            this.stats_fetcher.stop();
1135
        },
1136

    
1137
        require_reboot: function() {
1138
            if (this.is_active()) {
1139
                this.set({'reboot_required': true});
1140
            }
1141
        },
1142
        
1143
        set_pending_action: function(data) {
1144
            this.pending_action = data;
1145
            return data;
1146
        },
1147

    
1148
        // machine has pending action
1149
        update_pending_action: function(action, force) {
1150
            this.set({pending_action: action});
1151
        },
1152

    
1153
        clear_pending_action: function() {
1154
            this.set({pending_action: undefined});
1155
        },
1156

    
1157
        has_pending_action: function() {
1158
            return this.get("pending_action") ? this.get("pending_action") : false;
1159
        },
1160
        
1161
        // machine is active
1162
        is_active: function() {
1163
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
1164
        },
1165
        
1166
        // machine is building 
1167
        is_building: function() {
1168
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
1169
        },
1170
        
1171
        is_rebooting: function() {
1172
            return this.state() == 'REBOOT';
1173
        },
1174

    
1175
        in_error_state: function() {
1176
            return this.state() === "ERROR"
1177
        },
1178

    
1179
        // user can connect to machine
1180
        is_connectable: function() {
1181
            // check if ips exist
1182
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
1183
                return false;
1184
            }
1185
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1186
        },
1187
        
1188
        remove_meta: function(key, complete, error) {
1189
            var url = this.api_path() + "/metadata/" + key;
1190
            this.api_call(url, "delete", undefined, complete, error);
1191
        },
1192

    
1193
        save_meta: function(meta, complete, error) {
1194
            var url = this.api_path() + "/metadata/" + meta.key;
1195
            var payload = {meta:{}};
1196
            payload.meta[meta.key] = meta.value;
1197
            payload._options = {
1198
                critical:false, 
1199
                error_params: {
1200
                    title: "Machine metadata error",
1201
                    extra_details: {"Machine id": this.id}
1202
            }};
1203

    
1204
            this.api_call(url, "update", payload, complete, error);
1205
        },
1206

    
1207

    
1208
        // update/get the state of the machine
1209
        state: function() {
1210
            var args = slice.call(arguments);
1211
                
1212
            // TODO: it might not be a good idea to set the state in set_state method
1213
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1214
                this.set({'state': args[0]});
1215
            }
1216

    
1217
            return this.get('state');
1218
        },
1219
        
1220
        // get the state that the api status corresponds to
1221
        state_for_api_status: function(status) {
1222
            return this.state_transition(this.state(), status);
1223
        },
1224
        
1225
        // get transition state for the corresponging api status
1226
        state_transition: function(state, new_status) {
1227
            var statuses = models.VM.STATES_TRANSITIONS[state];
1228
            if (statuses) {
1229
                if (statuses.indexOf(new_status) > -1) {
1230
                    return new_status;
1231
                } else {
1232
                    return state;
1233
                }
1234
            } else {
1235
                return new_status;
1236
            }
1237
        },
1238
        
1239
        // the current vm state is a transition state
1240
        in_transition: function() {
1241
            return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || 
1242
                models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1;
1243
        },
1244
        
1245
        // get image object
1246
        get_image: function(callback) {
1247
            if (callback == undefined) { callback = function(){} }
1248
            var image = storage.images.get(this.get('image'));
1249
            if (!image) {
1250
                storage.images.update_unknown_id(this.get('image'), callback);
1251
                return;
1252
            }
1253
            callback(image);
1254
            return image;
1255
        },
1256
        
1257
        // get flavor object
1258
        get_flavor: function() {
1259
            var flv = storage.flavors.get(this.get('flavor'));
1260
            if (!flv) {
1261
                storage.flavors.update_unknown_id(this.get('flavor'));
1262
                flv = storage.flavors.get(this.get('flavor'));
1263
            }
1264
            return flv;
1265
        },
1266

    
1267
        get_resize_flavors: function() {
1268
          var vm_flavor = this.get_flavor();
1269
          var flavors = synnefo.storage.flavors.filter(function(f){
1270
              return f.get('disk_template') ==
1271
              vm_flavor.get('disk_template') && f.get('disk') ==
1272
              vm_flavor.get('disk');
1273
          });
1274
          return flavors;
1275
        },
1276

    
1277
        get_flavor_quotas: function() {
1278
          var flavor = this.get_flavor();
1279
          return {
1280
            cpu: flavor.get('cpu') + 1, 
1281
            ram: flavor.get_ram_size() + 1, 
1282
            disk:flavor.get_disk_size() + 1
1283
          }
1284
        },
1285

    
1286
        get_meta: function(key, deflt) {
1287
            if (this.get('metadata') && this.get('metadata')) {
1288
                if (!this.get('metadata')[key]) { return deflt }
1289
                return _.escape(this.get('metadata')[key]);
1290
            } else {
1291
                return deflt;
1292
            }
1293
        },
1294

    
1295
        get_meta_keys: function() {
1296
            if (this.get('metadata') && this.get('metadata')) {
1297
                return _.keys(this.get('metadata'));
1298
            } else {
1299
                return [];
1300
            }
1301
        },
1302
        
1303
        // get metadata OS value
1304
        get_os: function() {
1305
            var image = this.get_image();
1306
            return this.get_meta('OS') || (image ? 
1307
                                            image.get_os() || "okeanos" : "okeanos");
1308
        },
1309

    
1310
        get_gui: function() {
1311
            return this.get_meta('GUI');
1312
        },
1313
        
1314
        connected_to: function(net) {
1315
            return this.get('linked_to').indexOf(net.id) > -1;
1316
        },
1317

    
1318
        connected_with_nic_id: function(nic_id) {
1319
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
1320
        },
1321

    
1322
        get_nics: function(filter) {
1323
            ret = synnefo.storage.nics.filter(function(nic) {
1324
                return parseInt(nic.get('vm_id')) == this.id;
1325
            }, this);
1326

    
1327
            if (filter) {
1328
                return _.filter(ret, filter);
1329
            }
1330

    
1331
            return ret;
1332
        },
1333

    
1334
        get_net_nics: function(net_id) {
1335
            return this.get_nics(function(n){return n.get('network_id') == net_id});
1336
        },
1337

    
1338
        get_public_nic: function() {
1339
            return this.get_nics(function(n){ return n.get_network().is_public() === true })[0];
1340
        },
1341

    
1342
        get_hostname: function() {
1343
          var hostname = this.get_meta('hostname');
1344
          if (!hostname) {
1345
            if (synnefo.config.vm_hostname_format) {
1346
              hostname = synnefo.config.vm_hostname_format.format(this.id);
1347
            } else {
1348
              hostname = this.get_public_nic().get('ipv4');
1349
            }
1350
          }
1351
          return hostname;
1352
        },
1353

    
1354
        get_nic: function(net_id) {
1355
        },
1356

    
1357
        has_firewall: function() {
1358
            var nic = this.get_public_nic();
1359
            if (nic) {
1360
                var profile = nic.get('firewallProfile'); 
1361
                return ['ENABLED', 'PROTECTED'].indexOf(profile) > -1;
1362
            }
1363
            return false;
1364
        },
1365

    
1366
        get_firewall_profile: function() {
1367
            var nic = this.get_public_nic();
1368
            if (nic) {
1369
                return nic.get('firewallProfile');
1370
            }
1371
            return null;
1372
        },
1373

    
1374
        get_addresses: function() {
1375
            var pnic = this.get_public_nic();
1376
            if (!pnic) { return {'ip4': undefined, 'ip6': undefined }};
1377
            return {'ip4': pnic.get('ipv4'), 'ip6': pnic.get('ipv6')};
1378
        },
1379
    
1380
        // get actions that the user can execute
1381
        // depending on the vm state/status
1382
        get_available_actions: function() {
1383
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1384
        },
1385

    
1386
        set_profile: function(profile, net_id) {
1387
        },
1388
        
1389
        // call rename api
1390
        rename: function(new_name) {
1391
            //this.set({'name': new_name});
1392
            this.sync("update", this, {
1393
                critical: true,
1394
                data: {
1395
                    'server': {
1396
                        'name': new_name
1397
                    }
1398
                }, 
1399
                // do the rename after the method succeeds
1400
                success: _.bind(function(){
1401
                    //this.set({name: new_name});
1402
                    snf.api.trigger("call");
1403
                }, this)
1404
            });
1405
        },
1406
        
1407
        get_console_url: function(data) {
1408
            var url_params = {
1409
                machine: this.get("name"),
1410
                host_ip: this.get_addresses().ip4,
1411
                host_ip_v6: this.get_addresses().ip6,
1412
                host: data.host,
1413
                port: data.port,
1414
                password: data.password
1415
            }
1416
            return synnefo.config.ui_console_url + '?' + $.param(url_params);
1417
        },
1418

    
1419
        // action helper
1420
        call: function(action_name, success, error, params) {
1421
            var id_param = [this.id];
1422
            
1423
            params = params || {};
1424
            success = success || function() {};
1425
            error = error || function() {};
1426

    
1427
            var self = this;
1428

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

    
1485
                                         },  
1486
                                         error, 'destroy', params);
1487
                    break;
1488
                case 'resize':
1489
                    this.__make_api_call(this.get_action_url(), // vm actions url
1490
                                         "create", // create so that sync later uses POST to make the call
1491
                                         {resize: {flavorRef:params.flavor}}, // payload
1492
                                         function() {
1493
                                             self.state('RESIZE');
1494
                                             success.apply(this, arguments);
1495
                                             snf.api.trigger("call");
1496
                                         },  
1497
                                         error, 'resize', params);
1498
                    break;
1499
                case 'addFloatingIp':
1500
                    this.__make_api_call(this.get_action_url(), // vm actions url
1501
                                         "create", // create so that sync later uses POST to make the call
1502
                                         {addFloatingIp: {address:params.address}}, // payload
1503
                                         function() {
1504
                                             self.state('CONNECT');
1505
                                             success.apply(this, arguments);
1506
                                             snf.api.trigger("call");
1507
                                         },  
1508
                                         error, 'addFloatingIp', params);
1509
                    break;
1510
                case 'removeFloatingIp':
1511
                    this.__make_api_call(this.get_action_url(), // vm actions url
1512
                                         "create", // create so that sync later uses POST to make the call
1513
                                         {removeFloatingIp: {address:params.address}}, // payload
1514
                                         function() {
1515
                                             self.state('DISCONNECT');
1516
                                             success.apply(this, arguments);
1517
                                             snf.api.trigger("call");
1518
                                         },  
1519
                                         error, 'addFloatingIp', params);
1520
                    break;
1521
                case 'destroy':
1522
                    this.__make_api_call(this.url(), // vm actions url
1523
                                         "delete", // create so that sync later uses POST to make the call
1524
                                         undefined, // payload
1525
                                         function() {
1526
                                             // set state after successful call
1527
                                             self.state('DESTROY');
1528
                                             success.apply(this, arguments);
1529
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1530

    
1531
                                         },  
1532
                                         error, 'destroy', params);
1533
                    break;
1534
                default:
1535
                    throw "Invalid VM action ("+action_name+")";
1536
            }
1537
        },
1538
        
1539
        __make_api_call: function(url, method, data, success, error, action, 
1540
                                  extra_params) {
1541
            var self = this;
1542
            error = error || function(){};
1543
            success = success || function(){};
1544

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

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

    
1576
        handle_action_fail: function() {
1577
            this.action_error = arguments;
1578
            this.trigger("action:fail", arguments);
1579
        },
1580

    
1581
        get_action_url: function(name) {
1582
            return this.url() + "/action";
1583
        },
1584

    
1585
        get_diagnostics_url: function() {
1586
            return this.url() + "/diagnostics";
1587
        },
1588

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

    
1604
        get_connection_info: function(host_os, success, error) {
1605
            var url = synnefo.config.ui_connect_url;
1606
            var users = this.get_users();
1607

    
1608
            params = {
1609
                ip_address: this.get_public_nic().get('ipv4'),
1610
                hostname: this.get_hostname(),
1611
                os: this.get_os(),
1612
                host_os: host_os,
1613
                srv: this.id
1614
            }
1615
            
1616
            if (users.length) { 
1617
                params['username'] = _.last(users)
1618
            }
1619

    
1620
            url = url + "?" + $.param(params);
1621

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

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

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

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

    
1677
    // api status values
1678
    models.VM.CONNECT_STATES = [
1679
        'ACTIVE',
1680
        'REBOOT',
1681
        'SHUTDOWN'
1682
    ]
1683

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

    
1706
    models.VM.TRANSITION_STATES = [
1707
        'DESTROY',
1708
        'SHUTDOWN',
1709
        'START',
1710
        'REBOOT',
1711
        'BUILD',
1712
        'RESIZE'
1713
    ]
1714

    
1715
    models.VM.ACTIVE_STATES = [
1716
        'BUILD', 'REBOOT', 'ACTIVE',
1717
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1718
    ]
1719

    
1720
    models.VM.BUILDING_STATES = [
1721
        'BUILD'
1722
    ]
1723

    
1724
    models.Networks = models.Collection.extend({
1725
        model: models.Network,
1726
        path: 'networks',
1727
        details: true,
1728
        //noUpdate: true,
1729
        defaults: {'nics':[],'linked_to':[]},
1730
        
1731
        parse: function (resp, xhr) {
1732
            // FIXME: depricated global var
1733
            if (!resp) { return []};
1734
            var data = _.filter(_.map(resp.networks, _.bind(this.parse_net_api_data, this)),
1735
                               function(e){ return e });
1736
            return data;
1737
        },
1738

    
1739
        add: function() {
1740
            ret = models.Networks.__super__.add.apply(this, arguments);
1741
            // update nics after each network addition
1742
            ret.each(function(r){
1743
                synnefo.storage.nics.update_net_nics(r);
1744
            });
1745
        },
1746

    
1747
        reset_pending_actions: function() {
1748
            this.each(function(net) {
1749
                net.get("actions").reset();
1750
            });
1751
        },
1752

    
1753
        do_all_pending_actions: function() {
1754
            this.each(function(net) {
1755
                net.do_all_pending_actions();
1756
            })
1757
        },
1758

    
1759
        parse_net_api_data: function(data) {
1760
            // append nic metadata
1761
            // net.get('nics') contains a list of vm/index objects 
1762
            // e.g. {'vm_id':12231, 'index':1}
1763
            // net.get('linked_to') contains a list of vms the network is 
1764
            // connected to e.g. [1001, 1002]
1765
            if (data.attachments && data.attachments) {
1766
                data['nics'] = {};
1767
                data['linked_to'] = [];
1768
                _.each(data.attachments, function(nic_id){
1769
                  
1770
                  var vm_id = NIC_REGEX.exec(nic_id)[1];
1771
                  var nic_index = parseInt(NIC_REGEX.exec(nic_id)[2]);
1772

    
1773
                  if (vm_id !== undefined && nic_index !== undefined) {
1774
                      data['nics'][nic_id] = {
1775
                          'vm_id': vm_id, 
1776
                          'index': nic_index, 
1777
                          'id': nic_id
1778
                      };
1779
                      if (data['linked_to'].indexOf(vm_id) == -1) {
1780
                        data['linked_to'].push(vm_id);
1781
                      }
1782
                  }
1783
                });
1784
            }
1785

    
1786
            if (data.status == "DELETED" && !this.get(parseInt(data.id))) {
1787
              return false;
1788
            }
1789
            return data;
1790
        },
1791

    
1792
        create: function (name, type, cidr, dhcp, callback) {
1793
            var params = {
1794
                network:{
1795
                    name:name
1796
                }
1797
            };
1798

    
1799
            if (!type) {
1800
                throw "Network type cannot be empty";
1801
            }
1802
            params.network.type = type;
1803
            if (cidr) {
1804
                params.network.cidr = cidr;
1805
            }
1806
            if (dhcp) {
1807
                params.network.dhcp = dhcp;
1808
            }
1809

    
1810
            if (dhcp === false) {
1811
                params.network.dhcp = false;
1812
            }
1813
            
1814
            var cb = function() {
1815
              callback();
1816
              synnefo.storage.quotas.get('cyclades.network.private').increase();
1817
            }
1818
            return this.api_call(this.path, "create", params, cb);
1819
        },
1820

    
1821
        get_public: function(){
1822
          return this.filter(function(n){return n.get('public')});
1823
        }
1824
    })
1825

    
1826
    models.Images = models.Collection.extend({
1827
        model: models.Image,
1828
        path: 'images',
1829
        details: true,
1830
        noUpdate: true,
1831
        supportIncUpdates: false,
1832
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1833
        meta_labels: {},
1834
        read_method: 'read',
1835

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

    
1881
        _read_image_from_request: function(image, msg, xhr) {
1882
            return image.image;
1883
        },
1884

    
1885
        parse: function (resp, xhr) {
1886
            var parsed = _.map(resp.images, _.bind(this.parse_meta, this));
1887
            parsed = this.fill_owners(parsed);
1888
            return parsed;
1889
        },
1890

    
1891
        fill_owners: function(images) {
1892
            // do translate uuid->displayname if needed
1893
            // store display name in owner attribute for compatibility
1894
            var uuids = [];
1895

    
1896
            var images = _.map(images, function(img, index) {
1897
                if (synnefo.config.translate_uuids) {
1898
                    uuids.push(img['owner']);
1899
                }
1900
                img['owner_uuid'] = img['owner'];
1901
                return img;
1902
            });
1903
            
1904
            if (uuids.length > 0) {
1905
                var handle_results = function(data) {
1906
                    _.each(images, function (img) {
1907
                        img['owner'] = data.uuid_catalog[img['owner_uuid']];
1908
                    });
1909
                }
1910
                // notice the async false
1911
                var uuid_map = this.translate_uuids(uuids, false, 
1912
                                                    handle_results)
1913
            }
1914
            return images;
1915
        },
1916

    
1917
        translate_uuids: function(uuids, async, cb) {
1918
            var url = synnefo.config.user_catalog_url;
1919
            var data = JSON.stringify({'uuids': uuids});
1920
          
1921
            // post to user_catalogs api
1922
            snf.api.sync('create', undefined, {
1923
                url: url,
1924
                data: data,
1925
                async: async,
1926
                success:  cb
1927
            });
1928
        },
1929

    
1930
        get_meta_key: function(img, key) {
1931
            if (img.metadata && img.metadata && img.metadata[key]) {
1932
                return _.escape(img.metadata[key]);
1933
            }
1934
            return undefined;
1935
        },
1936

    
1937
        comparator: function(img) {
1938
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1939
        },
1940

    
1941
        parse_meta: function(img) {
1942
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1943
                if (img[key]) { return };
1944
                img[key] = this.get_meta_key(img, key) || "";
1945
            }, this));
1946
            return img;
1947
        },
1948

    
1949
        active: function() {
1950
            return this.filter(function(img){return img.get('status') != "DELETED"});
1951
        },
1952

    
1953
        predefined: function() {
1954
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
1955
        },
1956
        
1957
        fetch_for_type: function(type, complete, error) {
1958
            this.fetch({update:true, 
1959
                        success: complete, 
1960
                        error: error, 
1961
                        skip_api_error: true });
1962
        },
1963
        
1964
        get_images_for_type: function(type) {
1965
            if (this['get_{0}_images'.format(type)]) {
1966
                return this['get_{0}_images'.format(type)]();
1967
            }
1968

    
1969
            return this.active();
1970
        },
1971

    
1972
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
1973
            var load = false;
1974
            error = onError || function() {};
1975
            function complete(collection) { 
1976
                onComplete(collection.get_images_for_type(type)); 
1977
            }
1978
            
1979
            // do we need to fetch/update current collection entries
1980
            if (load) {
1981
                onStart();
1982
                this.fetch_for_type(type, complete, error);
1983
            } else {
1984
                // fallback to complete
1985
                complete(this);
1986
            }
1987
        }
1988
    })
1989

    
1990
    models.Flavors = models.Collection.extend({
1991
        model: models.Flavor,
1992
        path: 'flavors',
1993
        details: true,
1994
        noUpdate: true,
1995
        supportIncUpdates: false,
1996
        // update collection model with id passed
1997
        // making a direct call to the flavor
1998
        // api url
1999
        update_unknown_id: function(id, callback) {
2000
            var url = getUrl.call(this) + "/" + id;
2001
            this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, 
2002
            _.bind(function() {
2003
                this.add({id:id, cpu:"Unknown", ram:"Unknown", disk:"Unknown", name: "Unknown", status:"DELETED"})
2004
            }, this), _.bind(function(flv) {
2005
                if (!flv.flavor.status) { flv.flavor.status = "DELETED" };
2006
                this.add(flv.flavor);
2007
            }, this));
2008
        },
2009

    
2010
        parse: function (resp, xhr) {
2011
            return _.map(resp.flavors, function(o) {
2012
              o.cpu = o['vcpus'];
2013
              o.disk_template = o['SNF:disk_template'];
2014
              return o
2015
            });
2016
        },
2017

    
2018
        comparator: function(flv) {
2019
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
2020
        },
2021
          
2022
        unavailable_values_for_quotas: function(quotas, flavors, extra) {
2023
            var flavors = flavors || this.active();
2024
            var index = {cpu:[], disk:[], ram:[]};
2025
            var extra = extra == undefined ? {cpu:0, disk:0, ram:0} : extra;
2026
            
2027
            _.each(flavors, function(el) {
2028

    
2029
                var disk_available = quotas['disk'] + extra.disk;
2030
                var disk_size = el.get_disk_size();
2031
                if (index.disk.indexOf(disk_size) == -1) {
2032
                  var disk = el.disk_to_bytes();
2033
                  if (disk > disk_available) {
2034
                    index.disk.push(disk_size);
2035
                  }
2036
                }
2037
                
2038
                var ram_available = quotas['ram'] + extra.ram * 1024 * 1024;
2039
                var ram_size = el.get_ram_size();
2040
                if (index.ram.indexOf(ram_size) == -1) {
2041
                  var ram = el.ram_to_bytes();
2042
                  if (ram > ram_available) {
2043
                    index.ram.push(el.get('ram'))
2044
                  }
2045
                }
2046

    
2047
                var cpu = el.get('cpu');
2048
                var cpu_available = quotas['cpu'] + extra.cpu;
2049
                if (index.cpu.indexOf(cpu) == -1) {
2050
                  if (cpu > cpu_available) {
2051
                    index.cpu.push(el.get('cpu'))
2052
                  }
2053
                }
2054
            });
2055
            return index;
2056
        },
2057

    
2058
        unavailable_values_for_image: function(img, flavors) {
2059
            var flavors = flavors || this.active();
2060
            var size = img.get_size();
2061
            
2062
            var index = {cpu:[], disk:[], ram:[]};
2063

    
2064
            _.each(this.active(), function(el) {
2065
                var img_size = size;
2066
                var flv_size = el.get_disk_size();
2067
                if (flv_size < img_size) {
2068
                    if (index.disk.indexOf(flv_size) == -1) {
2069
                        index.disk.push(flv_size);
2070
                    }
2071
                };
2072
            });
2073
            
2074
            return index;
2075
        },
2076

    
2077
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
2078
            if (!filter_list) { filter_list = this.models };
2079
            
2080
            return this.select(function(flv){
2081
                if (flv.get("cpu") == cpu + "" &&
2082
                   flv.get("ram") == mem + "" &&
2083
                   flv.get("disk") == disk + "" &&
2084
                   flv.get("disk_template") == disk_template &&
2085
                   filter_list.indexOf(flv) > -1) { return true; }
2086
            })[0];
2087
        },
2088
        
2089
        get_data: function(lst) {
2090
            var data = {'cpu': [], 'mem':[], 'disk':[], 'disk_template':[]};
2091

    
2092
            _.each(lst, function(flv) {
2093
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
2094
                    data.cpu.push(flv.get("cpu"));
2095
                }
2096
                if (data.mem.indexOf(flv.get("ram")) == -1) {
2097
                    data.mem.push(flv.get("ram"));
2098
                }
2099
                if (data.disk.indexOf(flv.get("disk")) == -1) {
2100
                    data.disk.push(flv.get("disk"));
2101
                }
2102
                if (data.disk_template.indexOf(flv.get("disk_template")) == -1) {
2103
                    data.disk_template.push(flv.get("disk_template"));
2104
                }
2105
            })
2106
            
2107
            return data;
2108
        },
2109

    
2110
        active: function() {
2111
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
2112
        }
2113
            
2114
    })
2115

    
2116
    models.VMS = models.Collection.extend({
2117
        model: models.VM,
2118
        path: 'servers',
2119
        details: true,
2120
        copy_image_meta: true,
2121

    
2122
        parse: function (resp, xhr) {
2123
            var data = resp;
2124
            if (!resp) { return [] };
2125
            data = _.filter(_.map(resp.servers, _.bind(this.parse_vm_api_data, this)), function(v){return v});
2126
            return data;
2127
        },
2128

    
2129
        parse_vm_api_data: function(data) {
2130
            // do not add non existing DELETED entries
2131
            if (data.status && data.status == "DELETED") {
2132
                if (!this.get(data.id)) {
2133
                    return false;
2134
                }
2135
            }
2136
            
2137
            if ('SNF:task_state' in data) { 
2138
                data['task_state'] = data['SNF:task_state'];
2139
                if (data['task_state']) {
2140
                    var status = models.VM.TASK_STATE_STATUS_MAP[data['task_state']];
2141
                    if (status) { data['status'] = status }
2142
                }
2143
            }
2144

    
2145
            // OS attribute
2146
            if (this.has_meta(data)) {
2147
                data['OS'] = data.metadata.OS || snf.config.unknown_os;
2148
            }
2149
            
2150
            if (!data.diagnostics) {
2151
                data.diagnostics = [];
2152
            }
2153

    
2154
            // network metadata
2155
            data['firewalls'] = {};
2156
            data['nics'] = {};
2157
            data['linked_to'] = [];
2158

    
2159
            if (data['attachments'] && data['attachments']) {
2160
                var nics = data['attachments'];
2161
                _.each(nics, function(nic) {
2162
                    var net_id = nic.network_id;
2163
                    var index = parseInt(NIC_REGEX.exec(nic.id)[2]);
2164
                    if (data['linked_to'].indexOf(net_id) == -1) {
2165
                        data['linked_to'].push(net_id);
2166
                    }
2167

    
2168
                    data['nics'][nic.id] = nic;
2169
                })
2170
            }
2171
            
2172
            // if vm has no metadata, no metadata object
2173
            // is in json response, reset it to force
2174
            // value update
2175
            if (!data['metadata']) {
2176
                data['metadata'] = {};
2177
            }
2178
            
2179
            // v2.0 API returns objects
2180
            data.image_obj = data.image;
2181
            data.image = data.image_obj.id;
2182
            data.flavor_obj = data.flavor;
2183
            data.flavor = data.flavor_obj.id;
2184

    
2185
            return data;
2186
        },
2187

    
2188
        add: function() {
2189
            ret = models.VMS.__super__.add.apply(this, arguments);
2190
            ret.each(function(r){
2191
                synnefo.storage.nics.update_vm_nics(r);
2192
            });
2193
        },
2194
        
2195
        get_reboot_required: function() {
2196
            return this.filter(function(vm){return vm.get("reboot_required") == true})
2197
        },
2198

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

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

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

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

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

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

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

    
2262
                if (image.get("OS")) {
2263
                    meta['OS'] = image.get("OS");
2264
                }
2265
            }
2266
            
2267
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, 
2268
                    metadata:meta}
2269
            opts = _.extend(opts, extra);
2270
            
2271
            var cb = function(data) {
2272
              synnefo.storage.quotas.get('cyclades.vm').increase();
2273
              callback(data);
2274
            }
2275

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

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

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

    
2310
        get_connectable: function() {
2311
            return storage.vms.filter(function(vm){
2312
                return !vm.in_error_state() && !vm.is_building();
2313
            });
2314
        }
2315
    })
2316
    
2317
    models.NIC = models.Model.extend({
2318
        
2319
        initialize: function() {
2320
            models.NIC.__super__.initialize.apply(this, arguments);
2321
            this.pending_for_firewall = false;
2322
            this.bind("change:firewallProfile", _.bind(this.check_firewall, this));
2323
            this.bind("change:pending_firewall", function(nic) {
2324
                nic.get_network().update_state();
2325
            });
2326
            this.get_vm().bind("remove", function(){
2327
                try {
2328
                    this.collection.remove(this);
2329
                } catch (err) {};
2330
            }, this);
2331
            this.get_network().bind("remove", function(){
2332
                try {
2333
                    this.collection.remove(this);
2334
                } catch (err) {};
2335
            }, this);
2336

    
2337
        },
2338

    
2339
        get_vm: function() {
2340
            return synnefo.storage.vms.get(parseInt(this.get('vm_id')));
2341
        },
2342

    
2343
        get_network: function() {
2344
            return synnefo.storage.networks.get(this.get('network_id'));
2345
        },
2346

    
2347
        get_v6_address: function() {
2348
            return this.get("ipv6");
2349
        },
2350

    
2351
        get_v4_address: function() {
2352
            return this.get("ipv4");
2353
        },
2354

    
2355
        set_firewall: function(value, callback, error, options) {
2356
            var net_id = this.get('network_id');
2357
            var self = this;
2358

    
2359
            // api call data
2360
            var payload = {"firewallProfile":{"profile":value}};
2361
            payload._options = _.extend({critical: false}, options);
2362
            
2363
            this.set({'pending_firewall': value});
2364
            this.set({'pending_firewall_sending': true});
2365
            this.set({'pending_firewall_from': this.get('firewallProfile')});
2366

    
2367
            var success_cb = function() {
2368
                if (callback) {
2369
                    callback();
2370
                }
2371
                self.set({'pending_firewall_sending': false});
2372
            };
2373

    
2374
            var error_cb = function() {
2375
                self.reset_pending_firewall();
2376
            }
2377
            
2378
            this.get_vm().api_call(this.get_vm().api_path() + "/action", "create", payload, success_cb, error_cb);
2379
        },
2380

    
2381
        reset_pending_firewall: function() {
2382
            this.set({'pending_firewall': false});
2383
            this.set({'pending_firewall': false});
2384
        },
2385

    
2386
        check_firewall: function() {
2387
            var firewall = this.get('firewallProfile');
2388
            var pending = this.get('pending_firewall');
2389
            var previous = this.get('pending_firewall_from');
2390
            if (previous != firewall) { this.get_vm().require_reboot() };
2391
            this.reset_pending_firewall();
2392
        }
2393
        
2394
    });
2395

    
2396
    models.NICs = models.Collection.extend({
2397
        model: models.NIC,
2398
        
2399
        add_or_update: function(nic_id, data, vm) {
2400
            var params = _.clone(data);
2401
            var vm;
2402
            params.attachment_id = params.id;
2403
            params.id = params.id + '-' + params.network_id;
2404
            params.vm_id = parseInt(NIC_REGEX.exec(nic_id)[1]);
2405

    
2406
            if (!this.get(params.id)) {
2407
                this.add(params);
2408
                var nic = this.get(params.id);
2409
                vm = nic.get_vm();
2410
                nic.get_network().decrease_connecting();
2411
                nic.bind("remove", function() {
2412
                    nic.set({"removing": 0});
2413
                    if (this.get_network()) {
2414
                        // network might got removed before nic
2415
                        nic.get_network().update_state();
2416
                    }
2417
                });
2418

    
2419
            } else {
2420
                this.get(params.id).set(params);
2421
                vm = this.get(params.id).get_vm();
2422
            }
2423
            
2424
            // vm nics changed, trigger vm update
2425
            if (vm) { vm.trigger("change", vm)};
2426
        },
2427
        
2428
        reset_nics: function(nics, filter_attr, filter_val) {
2429
            var nics_to_check = this.filter(function(nic) {
2430
                return nic.get(filter_attr) == filter_val;
2431
            });
2432
            
2433
            _.each(nics_to_check, function(nic) {
2434
                if (nics.indexOf(nic.get('id')) == -1) {
2435
                    this.remove(nic);
2436
                } else {
2437
                }
2438
            }, this);
2439
        },
2440

    
2441
        update_vm_nics: function(vm) {
2442
            var nics = vm.get('nics');
2443
            this.reset_nics(_.map(nics, function(nic, key){
2444
                return key + "-" + nic.network_id;
2445
            }), 'vm_id', vm.id);
2446

    
2447
            _.each(nics, function(val, key) {
2448
                var net = synnefo.storage.networks.get(val.network_id);
2449
                if (net && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
2450
                    this.add_or_update(key, vm.get('nics')[key], vm);
2451
                }
2452
            }, this);
2453
        },
2454

    
2455
        update_net_nics: function(net) {
2456
            var nics = net.get('nics');
2457
            this.reset_nics(_.map(nics, function(nic, key){
2458
                return key + "-" + net.get('id');
2459
            }), 'network_id', net.id);
2460

    
2461
            _.each(nics, function(val, key) {
2462
                var vm = synnefo.storage.vms.get(val.vm_id);
2463
                if (vm && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
2464
                    this.add_or_update(key, vm.get('nics')[key], vm);
2465
                }
2466
            }, this);
2467
        }
2468
    });
2469

    
2470
    models.PublicKey = models.Model.extend({
2471
        path: 'keys',
2472
        api_type: 'userdata',
2473
        details: false,
2474
        noUpdate: true,
2475

    
2476

    
2477
        get_public_key: function() {
2478
            return cryptico.publicKeyFromString(this.get("content"));
2479
        },
2480

    
2481
        get_filename: function() {
2482
            return "{0}.pub".format(this.get("name"));
2483
        },
2484

    
2485
        identify_type: function() {
2486
            try {
2487
                var cont = snf.util.validatePublicKey(this.get("content"));
2488
                var type = cont.split(" ")[0];
2489
                return synnefo.util.publicKeyTypesMap[type];
2490
            } catch (err) { return false };
2491
        }
2492

    
2493
    })
2494
    
2495
    models.PublicPool = models.Model.extend({});
2496
    models.PublicPools = models.Collection.extend({
2497
      model: models.PublicPool,
2498
      path: 'os-floating-ip-pools',
2499
      api_type: 'compute',
2500
      noUpdate: true,
2501

    
2502
      parse: function(data) {
2503
        return _.map(data.floating_ip_pools, function(pool) {
2504
          pool.id = pool.name;
2505
          return pool;
2506
        });
2507
      }
2508
    });
2509

    
2510
    models.PublicIP = models.Model.extend({
2511
        path: 'os-floating-ips',
2512
        has_status: false,
2513
        
2514
        initialize: function() {
2515
            models.PublicIP.__super__.initialize.apply(this, arguments);
2516
            this.bind('change:instance_id', _.bind(this.handle_status_change, this));
2517
        },
2518

    
2519
        handle_status_change: function() {
2520
            this.set({state: null});
2521
        },
2522

    
2523
        get_vm: function() {
2524
            if (this.get('instance_id')) {
2525
                return synnefo.storage.vms.get(parseInt(this.get('instance_id')));
2526
            }
2527
            return null;
2528
        },
2529

    
2530
        connect_to: function(vm) {
2531
        }
2532
    });
2533

    
2534
    models.PublicIPs = models.Collection.extend({
2535
        model: models.PublicIP,
2536
        path: 'os-floating-ips',
2537
        api_type: 'compute',
2538
        noUpdate: true,
2539

    
2540
        parse: function(resp) {
2541
            resp = _.map(resp.floating_ips, function(ip) {
2542
              return ip;
2543
            });
2544

    
2545
            return resp;
2546
        }
2547
    });
2548

    
2549
    models.PublicKeys = models.Collection.extend({
2550
        model: models.PublicKey,
2551
        details: false,
2552
        path: 'keys',
2553
        api_type: 'userdata',
2554
        noUpdate: true,
2555

    
2556
        generate_new: function(success, error) {
2557
            snf.api.sync('create', undefined, {
2558
                url: getUrl.call(this, this.base_url) + "/generate", 
2559
                success: success, 
2560
                error: error,
2561
                skip_api_error: true
2562
            });
2563
        },
2564

    
2565
        add_crypto_key: function(key, success, error, options) {
2566
            var options = options || {};
2567
            var m = new models.PublicKey();
2568

    
2569
            // guess a name
2570
            var name_tpl = "my generated public key";
2571
            var name = name_tpl;
2572
            var name_count = 1;
2573
            
2574
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
2575
                name = name_tpl + " " + name_count;
2576
                name_count++;
2577
            }
2578
            
2579
            m.set({name: name});
2580
            m.set({content: key});
2581
            
2582
            options.success = function () { return success(m) };
2583
            options.errror = error;
2584
            options.skip_api_error = true;
2585
            
2586
            this.create(m.attributes, options);
2587
        }
2588
    });
2589

    
2590
  
2591
    models.Quota = models.Model.extend({
2592

    
2593
        initialize: function() {
2594
            models.Quota.__super__.initialize.apply(this, arguments);
2595
            this.bind("change", this.check, this);
2596
            this.check();
2597
        },
2598
        
2599
        check: function() {
2600
            var usage, limit;
2601
            usage = this.get('usage');
2602
            limit = this.get('limit');
2603
            if (usage >= limit) {
2604
                this.trigger("available");
2605
            } else {
2606
                this.trigger("unavailable");
2607
            }
2608
        },
2609

    
2610
        increase: function(val) {
2611
            if (val === undefined) { val = 1};
2612
            this.set({'usage': this.get('usage') + val})
2613
        },
2614

    
2615
        decrease: function(val) {
2616
            if (val === undefined) { val = 1};
2617
            this.set({'usage': this.get('usage') - val})
2618
        },
2619

    
2620
        can_consume: function() {
2621
            var usage, limit;
2622
            usage = this.get('usage');
2623
            limit = this.get('limit');
2624
            if (usage >= limit) {
2625
                return false
2626
            } else {
2627
                return true
2628
            }
2629
        },
2630
        
2631
        is_bytes: function() {
2632
            return this.get('resource').get('unit') == 'bytes';
2633
        },
2634
        
2635
        get_available: function(active) {
2636
            suffix = '';
2637
            if (active) { suffix = '_active'}
2638
            var value = this.get('limit'+suffix) - this.get('usage'+suffix);
2639
            if (active) {
2640
              if (this.get('available') <= value) {
2641
                value = this.get('available');
2642
              }
2643
            }
2644
            if (value < 0) { return value }
2645
            return value
2646
        },
2647

    
2648
        get_readable: function(key, active) {
2649
            var value;
2650
            if (key == 'available') {
2651
                value = this.get_available(active);
2652
            } else {
2653
                value = this.get(key)
2654
            }
2655
            if (value <= 0) { value = 0 }
2656
            if (!this.is_bytes()) {
2657
              return value + "";
2658
            }
2659
            return snf.util.readablizeBytes(value);
2660
        }
2661
    });
2662

    
2663
    models.Quotas = models.Collection.extend({
2664
        model: models.Quota,
2665
        api_type: 'accounts',
2666
        path: 'quotas',
2667
        parse: function(resp) {
2668
            filtered = _.map(resp.system, function(value, key) {
2669
                var available = (value.limit - value.usage) || 0;
2670
                var available_active = available;
2671
                var keysplit = key.split(".");
2672
                var limit_active = value.limit;
2673
                var usage_active = value.usage;
2674
                keysplit[keysplit.length-1] = "active_" + keysplit[keysplit.length-1];
2675
                var activekey = keysplit.join(".");
2676
                var exists = resp.system[activekey];
2677
                if (exists) {
2678
                    available_active = exists.limit - exists.usage;
2679
                    limit_active = exists.limit;
2680
                    usage_active = exists.usage;
2681
                }
2682
                return _.extend(value, {'name': key, 'id': key, 
2683
                          'available': available,
2684
                          'available_active': available_active,
2685
                          'limit_active': limit_active,
2686
                          'usage_active': usage_active,
2687
                          'resource': snf.storage.resources.get(key)});
2688
            });
2689
            return filtered;
2690
        },
2691
        
2692
        get_by_id: function(k) {
2693
          return this.filter(function(q) { return q.get('name') == k})[0]
2694
        },
2695

    
2696
        get_available_for_vm: function(options) {
2697
          var quotas = synnefo.storage.quotas;
2698
          var key = 'available';
2699
          var available_quota = {};
2700
          _.each(['cyclades.ram', 'cyclades.cpu', 'cyclades.disk'], 
2701
            function (key) {
2702
              var value = quotas.get(key).get_available(true);
2703
              available_quota[key.replace('cyclades.', '')] = value;
2704
          });
2705
          return available_quota;
2706
        }
2707
    })
2708

    
2709
    models.Resource = models.Model.extend({
2710
        api_type: 'accounts',
2711
        path: 'resources'
2712
    });
2713

    
2714
    models.Resources = models.Collection.extend({
2715
        api_type: 'accounts',
2716
        path: 'resources',
2717
        model: models.Network,
2718

    
2719
        parse: function(resp) {
2720
            return _.map(resp, function(value, key) {
2721
                return _.extend(value, {'name': key, 'id': key});
2722
            })
2723
        }
2724
    });
2725
    
2726
    // storage initialization
2727
    snf.storage.images = new models.Images();
2728
    snf.storage.flavors = new models.Flavors();
2729
    snf.storage.networks = new models.Networks();
2730
    snf.storage.vms = new models.VMS();
2731
    snf.storage.keys = new models.PublicKeys();
2732
    snf.storage.nics = new models.NICs();
2733
    snf.storage.resources = new models.Resources();
2734
    snf.storage.quotas = new models.Quotas();
2735
    snf.storage.public_ips = new models.PublicIPs();
2736
    snf.storage.public_pools = new models.PublicPools();
2737

    
2738
})(this);