Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (89.2 kB)

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

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

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

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

    
64
    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
                
223
                last_ajax = this.fetch(params);
224
            }, this);
225
            handler_options.callback = callback;
226

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

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

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

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

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

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

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

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

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

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

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

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

    
314
        get_sort_order: function() {
315
            return parseInt(this.get('metadata') ? this.get('metadata').sortorder : -1)
316
        },
317

    
318
        get_vm: function() {
319
            var vm_id = this.get("serverRef");
320
            var vm = undefined;
321
            vm = storage.vms.get(vm_id);
322
            return vm;
323
        },
324

    
325
        is_public: function() {
326
            return this.get('is_public') == undefined ? true : this.get('is_public');
327
        },
328

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

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

    
354
        supports: function(feature) {
355
            if (feature == "ssh") {
356
                return this._supports_ssh()
357
            }
358
            return false;
359
        },
360

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

    
369
                return {
370
                    path: pathinfo.path,
371
                    contents: contents,
372
                    mode: 0600,
373
                    owner: pathinfo.user
374
                }
375
            });
376
        }
377
    });
378

    
379
    // Flavor model
380
    models.Flavor = models.Model.extend({
381
        path: 'flavors',
382

    
383
        details_string: function() {
384
            return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk'));
385
        },
386

    
387
        get_disk_size: function() {
388
            return parseInt(this.get("disk") * 1000)
389
        },
390

    
391
        get_ram_size: function() {
392
            return parseInt(this.get("ram"))
393
        },
394

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

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

    
407
        ram_to_bytes: function() {
408
            return parseInt(this.get("ram")) * 1024 * 1024;
409
        },
410

    
411
    });
412
    
413
    models.ParamsList = function(){this.initialize.apply(this, arguments)};
414
    _.extend(models.ParamsList.prototype, bb.Events, {
415

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

    
432
            var args = _.toArray(arguments);
433
            return args.splice(1);
434
        },
435

    
436
        contains: function(action, params) {
437
            params = this._parse_params(arguments);
438
            var has_action = this.has_action(action);
439
            if (!has_action) { return false };
440

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

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

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

    
483
        reset: function() {
484
            this.actions = {};
485
            this.parent.trigger("change:" + this.param_name, this.parent, this);
486
            this.trigger("reset");
487
            this.trigger("remove");
488
        },
489

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

    
510
    });
511

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

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

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

    
567
        is_public: function() {
568
            return this.get("public");
569
        },
570

    
571
        decrease_connecting: function() {
572
            var conn = this.get("connecting");
573
            if (!conn) { conn = 0 };
574
            if (conn > 0) {
575
                conn--;
576
            }
577
            this.set({"connecting": conn});
578
            this.update_state();
579
        },
580

    
581
        increase_connecting: function() {
582
            var conn = this.get("connecting");
583
            if (!conn) { conn = 0 };
584
            conn++;
585
            this.set({"connecting": conn});
586
            this.update_state();
587
        },
588

    
589
        connected_to: function(vm) {
590
            return this.get('linked_to').indexOf(""+vm.id) > -1;
591
        },
592

    
593
        connected_with_nic_id: function(nic_id) {
594
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
595
        },
596

    
597
        get_nics: function(filter) {
598
            var nics = synnefo.storage.nics.filter(function(nic) {
599
                return nic.get('network_id') == this.id;
600
            }, this);
601

    
602
            if (filter) {
603
                return _.filter(nics, filter);
604
            }
605
            return nics;
606
        },
607

    
608
        contains_firewalling_nics: function() {
609
            return this.get_nics(function(n){return n.get('pending_firewall')}).length
610
        },
611

    
612
        call: function(action, params, success, error) {
613
            if (action == "destroy") {
614
                var previous_state = this.get('state');
615
                var previous_status = this.get('status');
616

    
617
                this.set({state:"DESTROY"});
618

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

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

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

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

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

    
688
        get_connectable_vms: function() {
689
            return storage.vms.filter(function(vm){
690
                return !vm.in_error_state() && !vm.is_building();
691
            })
692
        },
693

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

    
706
            return models.Network.STATES[this.get("state")];
707
        },
708

    
709
        in_progress: function() {
710
            return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined;
711
        },
712

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

    
736
    models.Network.STATES_TRANSITIONS = {
737
        'CONNECTING': ['ACTIVE'],
738
        'DISCONNECTING': ['ACTIVE'],
739
        'PENDING': ['ACTIVE'],
740
        'FIREWALLING': ['ACTIVE']
741
    }
742

    
743
    // Virtualmachine model
744
    models.VM = models.Model.extend({
745

    
746
        path: 'servers',
747
        has_status: true,
748
        initialize: function(params) {
749
            
750
            this.pending_firewalls = {};
751
            
752
            models.VM.__super__.initialize.apply(this, arguments);
753

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

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

    
787
            this.bind("change:nics", _.bind(synnefo.storage.nics.update_vm_nics, synnefo.storage.nics));
788
        },
789

    
790
        status: function(st) {
791
            if (!st) { return this.get("status")}
792
            return this.set({status:st});
793
        },
794
        
795
        update_status: function() {
796
            this.set_status(this.get('status'));
797
        },
798

    
799
        set_status: function(st) {
800
            var new_state = this.state_for_api_status(st);
801
            var transition = false;
802

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

    
830
        has_diagnostics: function() {
831
            return this.get("diagnostics") && this.get("diagnostics").length;
832
        },
833

    
834
        get_progress_info: function() {
835
            // details about progress message
836
            // contains a list of diagnostic messages
837
            return this.get("status_messages");
838
        },
839

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

    
855
            var msg = messages[0];
856
            if (msg) {
857
              var message = msg.message;
858
              var message_tpl = snf.config.diagnostic_messages_tpls[msg.source];
859

    
860
              if (message_tpl) {
861
                  message = message_tpl.replace('MESSAGE', msg.message);
862
              }
863
              return message;
864
            }
865
            
866
            // no message to display, but vm in build state, display
867
            // finalizing message.
868
            if (this.is_building() == 'BUILD') {
869
                return synnefo.config.BUILDING_MESSAGES['FINAL'];
870
            }
871
            return null;
872
        },
873

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

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

    
935
            // copy finished display FINAL message or identify status message
936
            // from diagnostics.
937
            if (progress >= 100) {
938
                if (!this.has_diagnostics()) {
939
                        callback(BUILDING_MESSAGES['FINAL']);
940
                } else {
941
                        var d = this.get("diagnostics");
942
                        var msg = this.status_message_from_diagnostics(d);
943
                        if (msg) {
944
                              callback(msg);
945
                        }
946
                }
947
            }
948
        },
949

    
950
        get_copy_details: function(human, image, callback) {
951
            var human = human || false;
952
            var image = image || this.get_image(_.bind(function(image){
953
                var progress = this.get('progress');
954
                var size = image.get_size();
955
                var size_copied = (size * progress / 100).toFixed(2);
956
                
957
                if (human) {
958
                    size = util.readablizeBytes(size*1024*1024);
959
                    size_copied = util.readablizeBytes(size_copied*1024*1024);
960
                }
961

    
962
                callback({'progress': progress, 'size': size, 'copy': size_copied})
963
            }, this));
964
        },
965

    
966
        start_stats_update: function(force_if_empty) {
967
            var prev_state = this.do_update_stats;
968

    
969
            this.do_update_stats = true;
970
            
971
            // fetcher initialized ??
972
            if (!this.stats_fetcher) {
973
                this.init_stats_intervals();
974
            }
975

    
976

    
977
            // fetcher running ???
978
            if (!this.stats_fetcher.running || !prev_state) {
979
                this.stats_fetcher.start();
980
            }
981

    
982
            if (force_if_empty && this.get("stats") == undefined) {
983
                this.update_stats(true);
984
            }
985
        },
986

    
987
        stop_stats_update: function(stop_calls) {
988
            this.do_update_stats = false;
989

    
990
            if (stop_calls) {
991
                this.stats_fetcher.stop();
992
            }
993
        },
994

    
995
        // clear and reinitialize update interval
996
        init_stats_intervals: function (interval) {
997
            this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval);
998
            this.stats_fetcher.start();
999
        },
1000
        
1001
        get_stats_fetcher: function(timeout) {
1002
            var cb = _.bind(function(data){
1003
                this.update_stats();
1004
            }, this);
1005
            var fetcher = new snf.api.updateHandler({'callback': cb, interval: timeout, id:'stats'});
1006
            return fetcher;
1007
        },
1008

    
1009
        // do the api call
1010
        update_stats: function(force) {
1011
            // do not update stats if flag not set
1012
            if ((!this.do_update_stats && !force) || this.updating_stats) {
1013
                return;
1014
            }
1015

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

    
1033
        get_stats_image: function(stat, type) {
1034
        },
1035
        
1036
        _set_stats: function(stats) {
1037
            var silent = silent === undefined ? false : silent;
1038
            // unavailable stats while building
1039
            if (this.get("status") == "BUILD") { 
1040
                this.stats_available = false;
1041
            } else { this.stats_available = true; }
1042

    
1043
            if (this.get("status") == "DESTROY") { this.stats_available = false; }
1044
            
1045
            this.set({stats: stats}, {silent:true});
1046
            this.trigger("stats:update", stats);
1047
        },
1048

    
1049
        unbind: function() {
1050
            models.VM.__super__.unbind.apply(this, arguments);
1051
        },
1052

    
1053
        can_resize: function() {
1054
          return this.get('status') == 'STOPPED';
1055
        },
1056

    
1057
        handle_stats_error: function() {
1058
            stats = {};
1059
            _.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) {
1060
                stats[k] = false;
1061
            });
1062

    
1063
            this.set({'stats': stats});
1064
        },
1065

    
1066
        // this method gets executed after a successful vm stats api call
1067
        handle_stats_update: function(data) {
1068
            var self = this;
1069
            // avoid browser caching
1070
            
1071
            if (data.stats && _.size(data.stats) > 0) {
1072
                var ts = $.now();
1073
                var stats = data.stats;
1074
                var images_loaded = 0;
1075
                var images = {};
1076

    
1077
                function check_images_loaded() {
1078
                    images_loaded++;
1079

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

    
1100
                    img.error(function() {
1101
                        images[stat + type] = false;
1102
                        check_images_loaded();
1103
                    });
1104

    
1105
                    img.attr({'src': stats[k]});
1106
                })
1107
                data.stats = stats;
1108
            }
1109

    
1110
            // do we need to change the interval ??
1111
            if (data.stats.refresh * 1000 != this.stats_update_interval) {
1112
                this.stats_update_interval = data.stats.refresh * 1000;
1113
                this.stats_fetcher.interval = this.stats_update_interval;
1114
                this.stats_fetcher.maximum_interval = this.stats_update_interval;
1115
                this.stats_fetcher.stop();
1116
                this.stats_fetcher.start(false);
1117
            }
1118
        },
1119

    
1120
        // helper method that sets the do_update_stats
1121
        // in the future this method could also make an api call
1122
        // immediaetly if needed
1123
        enable_stats_update: function() {
1124
            this.do_update_stats = true;
1125
        },
1126
        
1127
        handle_destroy: function() {
1128
            this.stats_fetcher.stop();
1129
        },
1130

    
1131
        require_reboot: function() {
1132
            if (this.is_active()) {
1133
                this.set({'reboot_required': true});
1134
            }
1135
        },
1136
        
1137
        set_pending_action: function(data) {
1138
            this.pending_action = data;
1139
            return data;
1140
        },
1141

    
1142
        // machine has pending action
1143
        update_pending_action: function(action, force) {
1144
            this.set({pending_action: action});
1145
        },
1146

    
1147
        clear_pending_action: function() {
1148
            this.set({pending_action: undefined});
1149
        },
1150

    
1151
        has_pending_action: function() {
1152
            return this.get("pending_action") ? this.get("pending_action") : false;
1153
        },
1154
        
1155
        // machine is active
1156
        is_active: function() {
1157
            return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1;
1158
        },
1159
        
1160
        // machine is building 
1161
        is_building: function() {
1162
            return models.VM.BUILDING_STATES.indexOf(this.state()) > -1;
1163
        },
1164
        
1165
        in_error_state: function() {
1166
            return this.state() === "ERROR"
1167
        },
1168

    
1169
        // user can connect to machine
1170
        is_connectable: function() {
1171
            // check if ips exist
1172
            if (!this.get_addresses().ip4 && !this.get_addresses().ip6) {
1173
                return false;
1174
            }
1175
            return models.VM.CONNECT_STATES.indexOf(this.state()) > -1;
1176
        },
1177
        
1178
        remove_meta: function(key, complete, error) {
1179
            var url = this.api_path() + "/metadata/" + key;
1180
            this.api_call(url, "delete", undefined, complete, error);
1181
        },
1182

    
1183
        save_meta: function(meta, complete, error) {
1184
            var url = this.api_path() + "/metadata/" + meta.key;
1185
            var payload = {meta:{}};
1186
            payload.meta[meta.key] = meta.value;
1187
            payload._options = {
1188
                critical:false, 
1189
                error_params: {
1190
                    title: "Machine metadata error",
1191
                    extra_details: {"Machine id": this.id}
1192
            }};
1193

    
1194
            this.api_call(url, "update", payload, complete, error);
1195
        },
1196

    
1197

    
1198
        // update/get the state of the machine
1199
        state: function() {
1200
            var args = slice.call(arguments);
1201
                
1202
            // TODO: it might not be a good idea to set the state in set_state method
1203
            if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) {
1204
                this.set({'state': args[0]});
1205
            }
1206

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

    
1257
        get_resize_flavors: function() {
1258
          var vm_flavor = this.get_flavor();
1259
          var flavors = synnefo.storage.flavors.filter(function(f){
1260
              return f.get('disk_template') ==
1261
              vm_flavor.get('disk_template') && f.get('disk') ==
1262
              vm_flavor.get('disk');
1263
          });
1264
          return flavors;
1265
        },
1266

    
1267
        get_flavor_quotas: function() {
1268
          var flavor = this.get_flavor();
1269
          return {
1270
            cpu: flavor.get('cpu') + 1, 
1271
            ram: flavor.get_ram_size() + 1, 
1272
            disk:flavor.get_disk_size() + 1
1273
          }
1274
        },
1275

    
1276
        get_meta: function(key, deflt) {
1277
            if (this.get('metadata') && this.get('metadata')) {
1278
                if (!this.get('metadata')[key]) { return deflt }
1279
                return _.escape(this.get('metadata')[key]);
1280
            } else {
1281
                return deflt;
1282
            }
1283
        },
1284

    
1285
        get_meta_keys: function() {
1286
            if (this.get('metadata') && this.get('metadata')) {
1287
                return _.keys(this.get('metadata'));
1288
            } else {
1289
                return [];
1290
            }
1291
        },
1292
        
1293
        // get metadata OS value
1294
        get_os: function() {
1295
            var image = this.get_image();
1296
            return this.get_meta('OS') || (image ? 
1297
                                            image.get_os() || "okeanos" : "okeanos");
1298
        },
1299

    
1300
        get_gui: function() {
1301
            return this.get_meta('GUI');
1302
        },
1303
        
1304
        connected_to: function(net) {
1305
            return this.get('linked_to').indexOf(net.id) > -1;
1306
        },
1307

    
1308
        connected_with_nic_id: function(nic_id) {
1309
            return _.keys(this.get('nics')).indexOf(nic_id) > -1;
1310
        },
1311

    
1312
        get_nics: function(filter) {
1313
            ret = synnefo.storage.nics.filter(function(nic) {
1314
                return parseInt(nic.get('vm_id')) == this.id;
1315
            }, this);
1316

    
1317
            if (filter) {
1318
                return _.filter(ret, filter);
1319
            }
1320

    
1321
            return ret;
1322
        },
1323

    
1324
        get_net_nics: function(net_id) {
1325
            return this.get_nics(function(n){return n.get('network_id') == net_id});
1326
        },
1327

    
1328
        get_public_nic: function() {
1329
            return this.get_nics(function(n){ return n.get_network().is_public() === true })[0];
1330
        },
1331

    
1332
        get_hostname: function() {
1333
          var hostname = this.get_meta('hostname');
1334
          if (!hostname) {
1335
            if (synnefo.config.vm_hostname_format) {
1336
              hostname = synnefo.config.vm_hostname_format.format(this.id);
1337
            } else {
1338
              hostname = this.get_public_nic().get('ipv4');
1339
            }
1340
          }
1341
          return hostname;
1342
        },
1343

    
1344
        get_nic: function(net_id) {
1345
        },
1346

    
1347
        has_firewall: function() {
1348
            var nic = this.get_public_nic();
1349
            if (nic) {
1350
                var profile = nic.get('firewallProfile'); 
1351
                return ['ENABLED', 'PROTECTED'].indexOf(profile) > -1;
1352
            }
1353
            return false;
1354
        },
1355

    
1356
        get_firewall_profile: function() {
1357
            var nic = this.get_public_nic();
1358
            if (nic) {
1359
                return nic.get('firewallProfile');
1360
            }
1361
            return null;
1362
        },
1363

    
1364
        get_addresses: function() {
1365
            var pnic = this.get_public_nic();
1366
            if (!pnic) { return {'ip4': undefined, 'ip6': undefined }};
1367
            return {'ip4': pnic.get('ipv4'), 'ip6': pnic.get('ipv6')};
1368
        },
1369
    
1370
        // get actions that the user can execute
1371
        // depending on the vm state/status
1372
        get_available_actions: function() {
1373
            return models.VM.AVAILABLE_ACTIONS[this.state()];
1374
        },
1375

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

    
1409
        // action helper
1410
        call: function(action_name, success, error, params) {
1411
            var id_param = [this.id];
1412
            
1413
            params = params || {};
1414
            success = success || function() {};
1415
            error = error || function() {};
1416

    
1417
            var self = this;
1418

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

    
1473
                                         },  
1474
                                         error, 'destroy', params);
1475
                    break;
1476
                case 'resize':
1477
                    this.__make_api_call(this.get_action_url(), // vm actions url
1478
                                         "create", // create so that sync later uses POST to make the call
1479
                                         {resize: {flavorRef:params.flavor}}, // payload
1480
                                         function() {
1481
                                             self.state('RESIZE');
1482
                                             success.apply(this, arguments);
1483
                                             snf.api.trigger("call");
1484
                                         },  
1485
                                         error, 'resize', params);
1486
                    break;
1487
                case 'destroy':
1488
                    this.__make_api_call(this.url(), // vm actions url
1489
                                         "delete", // create so that sync later uses POST to make the call
1490
                                         undefined, // payload
1491
                                         function() {
1492
                                             // set state after successful call
1493
                                             self.state('DESTROY');
1494
                                             success.apply(this, arguments);
1495
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1496

    
1497
                                         },  
1498
                                         error, 'destroy', params);
1499
                    break;
1500
                default:
1501
                    throw "Invalid VM action ("+action_name+")";
1502
            }
1503
        },
1504
        
1505
        __make_api_call: function(url, method, data, success, error, action, extra_params) {
1506
            var self = this;
1507
            error = error || function(){};
1508
            success = success || function(){};
1509

    
1510
            var params = {
1511
                url: url,
1512
                data: data,
1513
                success: function(){ self.handle_action_succeed.apply(self, arguments); success.apply(this, arguments)},
1514
                error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)},
1515
                error_params: { ns: "Machines actions", 
1516
                                title: "'" + this.get("name") + "'" + " " + action + " failed", 
1517
                                extra_details: { 'Machine ID': this.id, 'URL': url, 'Action': action || "undefined" },
1518
                                allow_reload: false
1519
                              },
1520
                display: false,
1521
                critical: false
1522
            }
1523
            _.extend(params, extra_params)
1524
            this.sync(method, this, params);
1525
        },
1526

    
1527
        handle_action_succeed: function() {
1528
            this.trigger("action:success", arguments);
1529
        },
1530
        
1531
        reset_action_error: function() {
1532
            this.action_error = false;
1533
            this.trigger("action:fail:reset", this.action_error);
1534
        },
1535

    
1536
        handle_action_fail: function() {
1537
            this.action_error = arguments;
1538
            this.trigger("action:fail", arguments);
1539
        },
1540

    
1541
        get_action_url: function(name) {
1542
            return this.url() + "/action";
1543
        },
1544

    
1545
        get_diagnostics_url: function() {
1546
            return this.url() + "/diagnostics";
1547
        },
1548

    
1549
        get_connection_info: function(host_os, success, error) {
1550
            var url = synnefo.config.ui_connect_url;
1551
            params = {
1552
                ip_address: this.get_public_nic().get('ipv4'),
1553
                hostname: this.get_hostname(),
1554
                os: this.get_os(),
1555
                host_os: host_os,
1556
                srv: this.id
1557
            }
1558

    
1559
            url = url + "?" + $.param(params);
1560

    
1561
            var ajax = snf.api.sync("read", undefined, { url: url, 
1562
                                                         error:error, 
1563
                                                         success:success, 
1564
                                                         handles_error:1});
1565
        }
1566
    })
1567
    
1568
    models.VM.ACTIONS = [
1569
        'start',
1570
        'shutdown',
1571
        'reboot',
1572
        'console',
1573
        'destroy',
1574
        'resize'
1575
    ]
1576

    
1577
    models.VM.TASK_STATE_STATUS_MAP = {
1578
      'BULDING': 'BUILD',
1579
      'REBOOTING': 'REBOOT',
1580
      'STOPPING': 'SHUTDOWN',
1581
      'STARTING': 'START',
1582
      'RESIZING': 'RESIZE',
1583
      'CONNECTING': 'CONNECT',
1584
      'DISCONNECTING': 'DISCONNECT',
1585
      'DESTROYING': 'DESTROY'
1586
    }
1587

    
1588
    models.VM.AVAILABLE_ACTIONS = {
1589
        'UNKNWON'       : [],
1590
        'BUILD'         : [],
1591
        'REBOOT'        : [],
1592
        'STOPPED'       : ['start', 'destroy'],
1593
        'ACTIVE'        : ['shutdown', 'destroy', 'reboot', 'console'],
1594
        'ERROR'         : ['destroy'],
1595
        'DELETED'       : [],
1596
        'DESTROY'       : [],
1597
        'SHUTDOWN'      : [],
1598
        'START'         : [],
1599
        'CONNECT'       : [],
1600
        'DISCONNECT'    : [],
1601
        'RESIZE'        : []
1602
    }
1603

    
1604
    // api status values
1605
    models.VM.STATUSES = [
1606
        'UNKNWON',
1607
        'BUILD',
1608
        'REBOOT',
1609
        'STOPPED',
1610
        'ACTIVE',
1611
        'ERROR',
1612
        'DELETED',
1613
        'RESIZE'
1614
    ]
1615

    
1616
    // api status values
1617
    models.VM.CONNECT_STATES = [
1618
        'ACTIVE',
1619
        'REBOOT',
1620
        'SHUTDOWN'
1621
    ]
1622

    
1623
    // vm states
1624
    models.VM.STATES = models.VM.STATUSES.concat([
1625
        'DESTROY',
1626
        'SHUTDOWN',
1627
        'START',
1628
        'CONNECT',
1629
        'DISCONNECT',
1630
        'FIREWALL',
1631
        'RESIZE'
1632
    ]);
1633
    
1634
    models.VM.STATES_TRANSITIONS = {
1635
        'DESTROY' : ['DELETED'],
1636
        'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'],
1637
        'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'],
1638
        'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'],
1639
        'START': ['ERROR', 'ACTIVE', 'DESTROY'],
1640
        'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'],
1641
        'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'],
1642
        'RESIZE': ['ERROR', 'STOPPED']
1643
    }
1644

    
1645
    models.VM.TRANSITION_STATES = [
1646
        'DESTROY',
1647
        'SHUTDOWN',
1648
        'START',
1649
        'REBOOT',
1650
        'BUILD',
1651
        'RESIZE'
1652
    ]
1653

    
1654
    models.VM.ACTIVE_STATES = [
1655
        'BUILD', 'REBOOT', 'ACTIVE',
1656
        'SHUTDOWN', 'CONNECT', 'DISCONNECT'
1657
    ]
1658

    
1659
    models.VM.BUILDING_STATES = [
1660
        'BUILD'
1661
    ]
1662

    
1663
    models.Networks = models.Collection.extend({
1664
        model: models.Network,
1665
        path: 'networks',
1666
        details: true,
1667
        //noUpdate: true,
1668
        defaults: {'nics':[],'linked_to':[]},
1669
        
1670
        parse: function (resp, xhr) {
1671
            // FIXME: depricated global var
1672
            if (!resp) { return []};
1673
            var data = _.filter(_.map(resp.networks, _.bind(this.parse_net_api_data, this)),
1674
                               function(e){ return e });
1675
            return data;
1676
        },
1677

    
1678
        add: function() {
1679
            ret = models.Networks.__super__.add.apply(this, arguments);
1680
            // update nics after each network addition
1681
            ret.each(function(r){
1682
                synnefo.storage.nics.update_net_nics(r);
1683
            });
1684
        },
1685

    
1686
        reset_pending_actions: function() {
1687
            this.each(function(net) {
1688
                net.get("actions").reset();
1689
            });
1690
        },
1691

    
1692
        do_all_pending_actions: function() {
1693
            this.each(function(net) {
1694
                net.do_all_pending_actions();
1695
            })
1696
        },
1697

    
1698
        parse_net_api_data: function(data) {
1699
            // append nic metadata
1700
            // net.get('nics') contains a list of vm/index objects 
1701
            // e.g. {'vm_id':12231, 'index':1}
1702
            // net.get('linked_to') contains a list of vms the network is 
1703
            // connected to e.g. [1001, 1002]
1704
            if (data.attachments && data.attachments) {
1705
                data['nics'] = {};
1706
                data['linked_to'] = [];
1707
                _.each(data.attachments, function(nic_id){
1708
                  
1709
                  var vm_id = NIC_REGEX.exec(nic_id)[1];
1710
                  var nic_index = parseInt(NIC_REGEX.exec(nic_id)[2]);
1711

    
1712
                  if (vm_id !== undefined && nic_index !== undefined) {
1713
                      data['nics'][nic_id] = {
1714
                          'vm_id': vm_id, 
1715
                          'index': nic_index, 
1716
                          'id': nic_id
1717
                      };
1718
                      if (data['linked_to'].indexOf(vm_id) == -1) {
1719
                        data['linked_to'].push(vm_id);
1720
                      }
1721
                  }
1722
                });
1723
            }
1724

    
1725
            if (data.status == "DELETED" && !this.get(parseInt(data.id))) {
1726
              return false;
1727
            }
1728
            return data;
1729
        },
1730

    
1731
        create: function (name, type, cidr, dhcp, callback) {
1732
            var params = {
1733
                network:{
1734
                    name:name
1735
                }
1736
            };
1737

    
1738
            if (!type) {
1739
                throw "Network type cannot be empty";
1740
            }
1741
            params.network.type = type;
1742
            if (cidr) {
1743
                params.network.cidr = cidr;
1744
            }
1745
            if (dhcp) {
1746
                params.network.dhcp = dhcp;
1747
            }
1748

    
1749
            if (dhcp === false) {
1750
                params.network.dhcp = false;
1751
            }
1752
            
1753
            var cb = function() {
1754
              callback();
1755
              synnefo.storage.quotas.get('cyclades.network.private').increase();
1756
            }
1757
            return this.api_call(this.path, "create", params, cb);
1758
        },
1759

    
1760
        get_public: function(){
1761
          return this.filter(function(n){return n.get('public')});
1762
        }
1763
    })
1764

    
1765
    models.Images = models.Collection.extend({
1766
        model: models.Image,
1767
        path: 'images',
1768
        details: true,
1769
        noUpdate: true,
1770
        supportIncUpdates: false,
1771
        meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"],
1772
        meta_labels: {},
1773
        read_method: 'read',
1774

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

    
1820
        _read_image_from_request: function(image, msg, xhr) {
1821
            return image.image;
1822
        },
1823

    
1824
        parse: function (resp, xhr) {
1825
            var parsed = _.map(resp.images, _.bind(this.parse_meta, this));
1826
            parsed = this.fill_owners(parsed);
1827
            return parsed;
1828
        },
1829

    
1830
        fill_owners: function(images) {
1831
            // do translate uuid->displayname if needed
1832
            // store display name in owner attribute for compatibility
1833
            var uuids = [];
1834

    
1835
            var images = _.map(images, function(img, index) {
1836
                if (synnefo.config.translate_uuids) {
1837
                    uuids.push(img['owner']);
1838
                }
1839
                img['owner_uuid'] = img['owner'];
1840
                return img;
1841
            });
1842
            
1843
            if (uuids.length > 0) {
1844
                var handle_results = function(data) {
1845
                    _.each(images, function (img) {
1846
                        img['owner'] = data.uuid_catalog[img['owner_uuid']];
1847
                    });
1848
                }
1849
                // notice the async false
1850
                var uuid_map = this.translate_uuids(uuids, false, 
1851
                                                    handle_results)
1852
            }
1853
            return images;
1854
        },
1855

    
1856
        translate_uuids: function(uuids, async, cb) {
1857
            var url = synnefo.config.user_catalog_url;
1858
            var data = JSON.stringify({'uuids': uuids});
1859
          
1860
            // post to user_catalogs api
1861
            snf.api.sync('create', undefined, {
1862
                url: url,
1863
                data: data,
1864
                async: async,
1865
                success:  cb
1866
            });
1867
        },
1868

    
1869
        get_meta_key: function(img, key) {
1870
            if (img.metadata && img.metadata && img.metadata[key]) {
1871
                return _.escape(img.metadata[key]);
1872
            }
1873
            return undefined;
1874
        },
1875

    
1876
        comparator: function(img) {
1877
            return -img.get_sort_order("sortorder") || 1000 * img.id;
1878
        },
1879

    
1880
        parse_meta: function(img) {
1881
            _.each(this.meta_keys_as_attrs, _.bind(function(key){
1882
                if (img[key]) { return };
1883
                img[key] = this.get_meta_key(img, key) || "";
1884
            }, this));
1885
            return img;
1886
        },
1887

    
1888
        active: function() {
1889
            return this.filter(function(img){return img.get('status') != "DELETED"});
1890
        },
1891

    
1892
        predefined: function() {
1893
            return _.filter(this.active(), function(i) { return !i.get("serverRef")});
1894
        },
1895
        
1896
        fetch_for_type: function(type, complete, error) {
1897
            this.fetch({update:true, 
1898
                        success: complete, 
1899
                        error: error, 
1900
                        skip_api_error: true });
1901
        },
1902
        
1903
        get_images_for_type: function(type) {
1904
            if (this['get_{0}_images'.format(type)]) {
1905
                return this['get_{0}_images'.format(type)]();
1906
            }
1907

    
1908
            return this.active();
1909
        },
1910

    
1911
        update_images_for_type: function(type, onStart, onComplete, onError, force_load) {
1912
            var load = false;
1913
            error = onError || function() {};
1914
            function complete(collection) { 
1915
                onComplete(collection.get_images_for_type(type)); 
1916
            }
1917
            
1918
            // do we need to fetch/update current collection entries
1919
            if (load) {
1920
                onStart();
1921
                this.fetch_for_type(type, complete, error);
1922
            } else {
1923
                // fallback to complete
1924
                complete(this);
1925
            }
1926
        }
1927
    })
1928

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

    
1949
        parse: function (resp, xhr) {
1950
            return _.map(resp.flavors, function(o) {
1951
              o.cpu = o['vcpus'];
1952
              o.disk_template = o['SNF:disk_template'];
1953
              return o
1954
            });
1955
        },
1956

    
1957
        comparator: function(flv) {
1958
            return flv.get("disk") * flv.get("cpu") * flv.get("ram");
1959
        },
1960
          
1961
        unavailable_values_for_quotas: function(quotas, flavors, extra) {
1962
            var flavors = flavors || this.active();
1963
            var index = {cpu:[], disk:[], ram:[]};
1964
            var extra = extra == undefined ? {cpu:0, disk:0, ram:0} : extra;
1965
            
1966
            _.each(flavors, function(el) {
1967

    
1968
                var disk_available = quotas['disk'] + extra.disk;
1969
                var disk_size = el.get_disk_size();
1970
                if (index.disk.indexOf(disk_size) == -1) {
1971
                  var disk = el.disk_to_bytes();
1972
                  if (disk > disk_available) {
1973
                    index.disk.push(disk_size);
1974
                  }
1975
                }
1976
                
1977
                var ram_available = quotas['ram'] + extra.ram * 1024 * 1024;
1978
                var ram_size = el.get_ram_size();
1979
                if (index.ram.indexOf(ram_size) == -1) {
1980
                  var ram = el.ram_to_bytes();
1981
                  if (ram > ram_available) {
1982
                    index.ram.push(el.get('ram'))
1983
                  }
1984
                }
1985

    
1986
                var cpu = el.get('cpu');
1987
                var cpu_available = quotas['cpu'] + extra.cpu;
1988
                if (index.cpu.indexOf(cpu) == -1) {
1989
                  if (cpu > cpu_available) {
1990
                    index.cpu.push(el.get('cpu'))
1991
                  }
1992
                }
1993
            });
1994
            return index;
1995
        },
1996

    
1997
        unavailable_values_for_image: function(img, flavors) {
1998
            var flavors = flavors || this.active();
1999
            var size = img.get_size();
2000
            
2001
            var index = {cpu:[], disk:[], ram:[]};
2002

    
2003
            _.each(this.active(), function(el) {
2004
                var img_size = size;
2005
                var flv_size = el.get_disk_size();
2006
                if (flv_size < img_size) {
2007
                    if (index.disk.indexOf(flv_size) == -1) {
2008
                        index.disk.push(flv_size);
2009
                    }
2010
                };
2011
            });
2012
            
2013
            return index;
2014
        },
2015

    
2016
        get_flavor: function(cpu, mem, disk, disk_template, filter_list) {
2017
            if (!filter_list) { filter_list = this.models };
2018
            
2019
            return this.select(function(flv){
2020
                if (flv.get("cpu") == cpu + "" &&
2021
                   flv.get("ram") == mem + "" &&
2022
                   flv.get("disk") == disk + "" &&
2023
                   flv.get("disk_template") == disk_template &&
2024
                   filter_list.indexOf(flv) > -1) { return true; }
2025
            })[0];
2026
        },
2027
        
2028
        get_data: function(lst) {
2029
            var data = {'cpu': [], 'mem':[], 'disk':[], 'disk_template':[]};
2030

    
2031
            _.each(lst, function(flv) {
2032
                if (data.cpu.indexOf(flv.get("cpu")) == -1) {
2033
                    data.cpu.push(flv.get("cpu"));
2034
                }
2035
                if (data.mem.indexOf(flv.get("ram")) == -1) {
2036
                    data.mem.push(flv.get("ram"));
2037
                }
2038
                if (data.disk.indexOf(flv.get("disk")) == -1) {
2039
                    data.disk.push(flv.get("disk"));
2040
                }
2041
                if (data.disk_template.indexOf(flv.get("disk_template")) == -1) {
2042
                    data.disk_template.push(flv.get("disk_template"));
2043
                }
2044
            })
2045
            
2046
            return data;
2047
        },
2048

    
2049
        active: function() {
2050
            return this.filter(function(flv){return flv.get('status') != "DELETED"});
2051
        }
2052
            
2053
    })
2054

    
2055
    models.VMS = models.Collection.extend({
2056
        model: models.VM,
2057
        path: 'servers',
2058
        details: true,
2059
        copy_image_meta: true,
2060

    
2061
        parse: function (resp, xhr) {
2062
            var data = resp;
2063
            if (!resp) { return [] };
2064
            data = _.filter(_.map(resp.servers, _.bind(this.parse_vm_api_data, this)), function(v){return v});
2065
            return data;
2066
        },
2067

    
2068
        parse_vm_api_data: function(data) {
2069
            // do not add non existing DELETED entries
2070
            if (data.status && data.status == "DELETED") {
2071
                if (!this.get(data.id)) {
2072
                    return false;
2073
                }
2074
            }
2075
            
2076
            if ('SNF:task_state' in data) { 
2077
                data['task_state'] = data['SNF:task_state'];
2078
                if (data['task_state']) {
2079
                    var status = models.VM.TASK_STATE_STATUS_MAP[data['task_state']];
2080
                    if (status) { data['status'] = status }
2081
                }
2082
            }
2083

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

    
2093
            // network metadata
2094
            data['firewalls'] = {};
2095
            data['nics'] = {};
2096
            data['linked_to'] = [];
2097

    
2098
            if (data['attachments'] && data['attachments']) {
2099
                var nics = data['attachments'];
2100
                _.each(nics, function(nic) {
2101
                    var net_id = nic.network_id;
2102
                    var index = parseInt(NIC_REGEX.exec(nic.id)[2]);
2103
                    if (data['linked_to'].indexOf(net_id) == -1) {
2104
                        data['linked_to'].push(net_id);
2105
                    }
2106

    
2107
                    data['nics'][nic.id] = nic;
2108
                })
2109
            }
2110
            
2111
            // if vm has no metadata, no metadata object
2112
            // is in json response, reset it to force
2113
            // value update
2114
            if (!data['metadata']) {
2115
                data['metadata'] = {};
2116
            }
2117
            
2118
            // v2.0 API returns objects
2119
            data.image_obj = data.image;
2120
            data.image = data.image_obj.id;
2121
            data.flavor_obj = data.flavor;
2122
            data.flavor = data.flavor_obj.id;
2123

    
2124
            return data;
2125
        },
2126

    
2127
        add: function() {
2128
            ret = models.VMS.__super__.add.apply(this, arguments);
2129
            ret.each(function(r){
2130
                synnefo.storage.nics.update_vm_nics(r);
2131
            });
2132
        },
2133
        
2134
        get_reboot_required: function() {
2135
            return this.filter(function(vm){return vm.get("reboot_required") == true})
2136
        },
2137

    
2138
        has_pending_actions: function() {
2139
            return this.filter(function(vm){return vm.pending_action}).length > 0;
2140
        },
2141

    
2142
        reset_pending_actions: function() {
2143
            this.each(function(vm) {
2144
                vm.clear_pending_action();
2145
            })
2146
        },
2147

    
2148
        do_all_pending_actions: function(success, error) {
2149
            this.each(function(vm) {
2150
                if (vm.has_pending_action()) {
2151
                    vm.call(vm.pending_action, success, error);
2152
                    vm.clear_pending_action();
2153
                }
2154
            })
2155
        },
2156
        
2157
        do_all_reboots: function(success, error) {
2158
            this.each(function(vm) {
2159
                if (vm.get("reboot_required")) {
2160
                    vm.call("reboot", success, error);
2161
                }
2162
            });
2163
        },
2164

    
2165
        reset_reboot_required: function() {
2166
            this.each(function(vm) {
2167
                vm.set({'reboot_required': undefined});
2168
            })
2169
        },
2170
        
2171
        stop_stats_update: function(exclude) {
2172
            var exclude = exclude || [];
2173
            this.each(function(vm) {
2174
                if (exclude.indexOf(vm) > -1) {
2175
                    return;
2176
                }
2177
                vm.stop_stats_update();
2178
            })
2179
        },
2180
        
2181
        has_meta: function(vm_data) {
2182
            return vm_data.metadata && vm_data.metadata
2183
        },
2184

    
2185
        has_addresses: function(vm_data) {
2186
            return vm_data.metadata && vm_data.metadata
2187
        },
2188

    
2189
        create: function (name, image, flavor, meta, extra, callback) {
2190

    
2191
            if (this.copy_image_meta) {
2192
                if (synnefo.config.vm_image_common_metadata) {
2193
                    _.each(synnefo.config.vm_image_common_metadata, 
2194
                        function(key){
2195
                            if (image.get_meta(key)) {
2196
                                meta[key] = image.get_meta(key);
2197
                            }
2198
                    });
2199
                }
2200

    
2201
                if (image.get("OS")) {
2202
                    meta['OS'] = image.get("OS");
2203
                }
2204
            }
2205
            
2206
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, 
2207
                    metadata:meta}
2208
            opts = _.extend(opts, extra);
2209
            
2210
            var cb = function(data) {
2211
              synnefo.storage.quotas.get('cyclades.vm').increase();
2212
              callback(data);
2213
            }
2214

    
2215
            this.api_call(this.path, "create", {'server': opts}, undefined, 
2216
                          undefined, cb, {critical: true});
2217
        },
2218

    
2219
        load_missing_images: function(callback) {
2220
          var missing_ids = [];
2221
          var resolved = 0;
2222

    
2223
          // fill missing_ids
2224
          this.each(function(el) {
2225
            var imgid = el.get("image");
2226
            var existing = synnefo.storage.images.get(imgid);
2227
            if (!existing && missing_ids.indexOf(imgid) == -1) {
2228
              missing_ids.push(imgid);
2229
            }
2230
          });
2231
          var check = function() {
2232
            // once all missing ids where resolved continue calling the 
2233
            // callback
2234
            resolved++;
2235
            if (resolved == missing_ids.length) {
2236
              callback(missing_ids)
2237
            }
2238
          }
2239
          if (missing_ids.length == 0) {
2240
            callback(missing_ids);
2241
            return;
2242
          }
2243
          // start resolving missing image ids
2244
          _(missing_ids).each(function(imgid){
2245
            synnefo.storage.images.update_unknown_id(imgid, check);
2246
          });
2247
        }
2248
    })
2249
    
2250
    models.NIC = models.Model.extend({
2251
        
2252
        initialize: function() {
2253
            models.NIC.__super__.initialize.apply(this, arguments);
2254
            this.pending_for_firewall = false;
2255
            this.bind("change:firewallProfile", _.bind(this.check_firewall, this));
2256
            this.bind("change:pending_firewall", function(nic) {
2257
                nic.get_network().update_state();
2258
            });
2259
            this.get_vm().bind("remove", function(){
2260
                try {
2261
                    this.collection.remove(this);
2262
                } catch (err) {};
2263
            }, this);
2264
            this.get_network().bind("remove", function(){
2265
                try {
2266
                    this.collection.remove(this);
2267
                } catch (err) {};
2268
            }, this);
2269

    
2270
        },
2271

    
2272
        get_vm: function() {
2273
            return synnefo.storage.vms.get(parseInt(this.get('vm_id')));
2274
        },
2275

    
2276
        get_network: function() {
2277
            return synnefo.storage.networks.get(this.get('network_id'));
2278
        },
2279

    
2280
        get_v6_address: function() {
2281
            return this.get("ipv6");
2282
        },
2283

    
2284
        get_v4_address: function() {
2285
            return this.get("ipv4");
2286
        },
2287

    
2288
        set_firewall: function(value, callback, error, options) {
2289
            var net_id = this.get('network_id');
2290
            var self = this;
2291

    
2292
            // api call data
2293
            var payload = {"firewallProfile":{"profile":value}};
2294
            payload._options = _.extend({critical: false}, options);
2295
            
2296
            this.set({'pending_firewall': value});
2297
            this.set({'pending_firewall_sending': true});
2298
            this.set({'pending_firewall_from': this.get('firewallProfile')});
2299

    
2300
            var success_cb = function() {
2301
                if (callback) {
2302
                    callback();
2303
                }
2304
                self.set({'pending_firewall_sending': false});
2305
            };
2306

    
2307
            var error_cb = function() {
2308
                self.reset_pending_firewall();
2309
            }
2310
            
2311
            this.get_vm().api_call(this.get_vm().api_path() + "/action", "create", payload, success_cb, error_cb);
2312
        },
2313

    
2314
        reset_pending_firewall: function() {
2315
            this.set({'pending_firewall': false});
2316
            this.set({'pending_firewall': false});
2317
        },
2318

    
2319
        check_firewall: function() {
2320
            var firewall = this.get('firewallProfile');
2321
            var pending = this.get('pending_firewall');
2322
            var previous = this.get('pending_firewall_from');
2323
            if (previous != firewall) { this.get_vm().require_reboot() };
2324
            this.reset_pending_firewall();
2325
        }
2326
        
2327
    });
2328

    
2329
    models.NICs = models.Collection.extend({
2330
        model: models.NIC,
2331
        
2332
        add_or_update: function(nic_id, data, vm) {
2333
            var params = _.clone(data);
2334
            var vm;
2335
            params.attachment_id = params.id;
2336
            params.id = params.id + '-' + params.network_id;
2337
            params.vm_id = parseInt(NIC_REGEX.exec(nic_id)[1]);
2338

    
2339
            if (!this.get(params.id)) {
2340
                this.add(params);
2341
                var nic = this.get(params.id);
2342
                vm = nic.get_vm();
2343
                nic.get_network().decrease_connecting();
2344
                nic.bind("remove", function() {
2345
                    nic.set({"removing": 0});
2346
                    if (this.get_network()) {
2347
                        // network might got removed before nic
2348
                        nic.get_network().update_state();
2349
                    }
2350
                });
2351

    
2352
            } else {
2353
                this.get(params.id).set(params);
2354
                vm = this.get(params.id).get_vm();
2355
            }
2356
            
2357
            // vm nics changed, trigger vm update
2358
            if (vm) { vm.trigger("change", vm)};
2359
        },
2360
        
2361
        reset_nics: function(nics, filter_attr, filter_val) {
2362
            var nics_to_check = this.filter(function(nic) {
2363
                return nic.get(filter_attr) == filter_val;
2364
            });
2365
            
2366
            _.each(nics_to_check, function(nic) {
2367
                if (nics.indexOf(nic.get('id')) == -1) {
2368
                    this.remove(nic);
2369
                } else {
2370
                }
2371
            }, this);
2372
        },
2373

    
2374
        update_vm_nics: function(vm) {
2375
            var nics = vm.get('nics');
2376
            this.reset_nics(_.map(nics, function(nic, key){
2377
                return key + "-" + nic.network_id;
2378
            }), 'vm_id', vm.id);
2379

    
2380
            _.each(nics, function(val, key) {
2381
                var net = synnefo.storage.networks.get(val.network_id);
2382
                if (net && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
2383
                    this.add_or_update(key, vm.get('nics')[key], vm);
2384
                }
2385
            }, this);
2386
        },
2387

    
2388
        update_net_nics: function(net) {
2389
            var nics = net.get('nics');
2390
            this.reset_nics(_.map(nics, function(nic, key){
2391
                return key + "-" + net.get('id');
2392
            }), 'network_id', net.id);
2393

    
2394
            _.each(nics, function(val, key) {
2395
                var vm = synnefo.storage.vms.get(val.vm_id);
2396
                if (vm && net.connected_with_nic_id(key) && vm.connected_with_nic_id(key)) {
2397
                    this.add_or_update(key, vm.get('nics')[key], vm);
2398
                }
2399
            }, this);
2400
        }
2401
    });
2402

    
2403
    models.PublicKey = models.Model.extend({
2404
        path: 'keys',
2405
        api_type: 'userdata',
2406
        details: false,
2407
        noUpdate: true,
2408

    
2409

    
2410
        get_public_key: function() {
2411
            return cryptico.publicKeyFromString(this.get("content"));
2412
        },
2413

    
2414
        get_filename: function() {
2415
            return "{0}.pub".format(this.get("name"));
2416
        },
2417

    
2418
        identify_type: function() {
2419
            try {
2420
                var cont = snf.util.validatePublicKey(this.get("content"));
2421
                var type = cont.split(" ")[0];
2422
                return synnefo.util.publicKeyTypesMap[type];
2423
            } catch (err) { return false };
2424
        }
2425

    
2426
    })
2427
    
2428
    models.PublicKeys = models.Collection.extend({
2429
        model: models.PublicKey,
2430
        details: false,
2431
        path: 'keys',
2432
        api_type: 'userdata',
2433
        noUpdate: true,
2434

    
2435
        generate_new: function(success, error) {
2436
            snf.api.sync('create', undefined, {
2437
                url: getUrl.call(this, this.base_url) + "/generate", 
2438
                success: success, 
2439
                error: error,
2440
                skip_api_error: true
2441
            });
2442
        },
2443

    
2444
        add_crypto_key: function(key, success, error, options) {
2445
            var options = options || {};
2446
            var m = new models.PublicKey();
2447

    
2448
            // guess a name
2449
            var name_tpl = "my generated public key";
2450
            var name = name_tpl;
2451
            var name_count = 1;
2452
            
2453
            while(this.filter(function(m){ return m.get("name") == name }).length > 0) {
2454
                name = name_tpl + " " + name_count;
2455
                name_count++;
2456
            }
2457
            
2458
            m.set({name: name});
2459
            m.set({content: key});
2460
            
2461
            options.success = function () { return success(m) };
2462
            options.errror = error;
2463
            options.skip_api_error = true;
2464
            
2465
            this.create(m.attributes, options);
2466
        }
2467
    });
2468

    
2469
  
2470
    models.Quota = models.Model.extend({
2471

    
2472
        initialize: function() {
2473
            models.Quota.__super__.initialize.apply(this, arguments);
2474
            this.bind("change", this.check, this);
2475
            this.check();
2476
        },
2477
        
2478
        check: function() {
2479
            var usage, limit;
2480
            usage = this.get('usage');
2481
            limit = this.get('limit');
2482
            if (usage >= limit) {
2483
                this.trigger("available");
2484
            } else {
2485
                this.trigger("unavailable");
2486
            }
2487
        },
2488

    
2489
        increase: function(val) {
2490
            if (val === undefined) { val = 1};
2491
            this.set({'usage': this.get('usage') + val})
2492
        },
2493

    
2494
        decrease: function(val) {
2495
            if (val === undefined) { val = 1};
2496
            this.set({'usage': this.get('usage') - val})
2497
        },
2498

    
2499
        can_consume: function() {
2500
            var usage, limit;
2501
            usage = this.get('usage');
2502
            limit = this.get('limit');
2503
            if (usage >= limit) {
2504
                return false
2505
            } else {
2506
                return true
2507
            }
2508
        },
2509
        
2510
        is_bytes: function() {
2511
            return this.get('resource').get('unit') == 'bytes';
2512
        },
2513
        
2514
        get_available: function(active) {
2515
            suffix = '';
2516
            if (active) { suffix = '_active'}
2517
            var value = this.get('limit'+suffix) - this.get('usage'+suffix);
2518
            if (value < 0) { return value }
2519
            return value
2520
        },
2521

    
2522
        get_readable: function(key, active) {
2523
            var value;
2524
            if (key == 'available') {
2525
                value = this.get_available(active);
2526
            } else {
2527
                value = this.get(key)
2528
            }
2529
            if (!this.is_bytes()) {
2530
              return value + "";
2531
            }
2532
            return snf.util.readablizeBytes(value);
2533
        }
2534
    });
2535

    
2536
    models.Quotas = models.Collection.extend({
2537
        model: models.Quota,
2538
        api_type: 'accounts',
2539
        path: 'quotas',
2540
        parse: function(resp) {
2541
            filtered = _.map(resp.system, function(value, key) {
2542
                var available = (value.limit - value.usage) || 0;
2543
                var available_active = available;
2544
                var keysplit = key.split(".");
2545
                var limit_active = value.limit;
2546
                var usage_active = value.usage;
2547
                keysplit[keysplit.length-1] = "active_" + keysplit[keysplit.length-1];
2548
                var activekey = keysplit.join(".");
2549
                var exists = resp.system[activekey];
2550
                if (exists) {
2551
                    available_active = exists.limit - exists.usage;
2552
                    limit_active = exists.limit;
2553
                    usage_active = exists.usage;
2554
                }
2555
                return _.extend(value, {'name': key, 'id': key, 
2556
                          'available': available,
2557
                          'available_active': available_active,
2558
                          'limit_active': limit_active,
2559
                          'usage_active': usage_active,
2560
                          'resource': snf.storage.resources.get(key)});
2561
            });
2562
            return filtered;
2563
        },
2564
        
2565
        get_by_id: function(k) {
2566
          return this.filter(function(q) { return q.get('name') == k})[0]
2567
        },
2568

    
2569
        get_available_for_vm: function(active) {
2570
          var quotas = synnefo.storage.quotas;
2571
          var key = 'available';
2572
          if (active) { key = 'available_active'; }
2573
          var quota = {
2574
            'ram': quotas.get('cyclades.ram').get(key),
2575
            'cpu': quotas.get('cyclades.cpu').get(key),
2576
            'disk': quotas.get('cyclades.disk').get(key)
2577
          }
2578
          return quota;
2579
        }
2580
    })
2581

    
2582
    models.Resource = models.Model.extend({
2583
        api_type: 'accounts',
2584
        path: 'resources'
2585
    });
2586

    
2587
    models.Resources = models.Collection.extend({
2588
        api_type: 'accounts',
2589
        path: 'resources',
2590
        model: models.Network,
2591

    
2592
        parse: function(resp) {
2593
            return _.map(resp, function(value, key) {
2594
                return _.extend(value, {'name': key, 'id': key});
2595
            })
2596
        }
2597
    });
2598
    
2599
    // storage initialization
2600
    snf.storage.images = new models.Images();
2601
    snf.storage.flavors = new models.Flavors();
2602
    snf.storage.networks = new models.Networks();
2603
    snf.storage.vms = new models.VMS();
2604
    snf.storage.keys = new models.PublicKeys();
2605
    snf.storage.nics = new models.NICs();
2606
    snf.storage.resources = new models.Resources();
2607
    snf.storage.quotas = new models.Quotas();
2608

    
2609
})(this);