Statistics
| Branch: | Tag: | Revision:

root / ui / static / synnefo.js @ adab5d39

History | View | Annotate | Download (19.9 kB)

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

    
42

    
43
Object.prototype.toString = function(o){
44
    
45
    var parse = function(_o){
46
        var a = [], t;
47
        for(var p in _o){
48
            if(_o.hasOwnProperty(p)){
49
                t = _o[p];
50
                if(t && typeof t == "object"){
51
                    a[a.length]= p + ":{ " + arguments.callee(t).join(", ") + "}";
52
                }
53
                else {
54
                    if(typeof t == "string"){
55
                        a[a.length] = [ p+ ": \"" + t.toString() + "\"" ];
56
                    }
57
                    else{
58
                        a[a.length] = [ p+ ": " + t.toString()];
59
                    }
60
                }
61
            }
62
        }
63
        return a;
64
        
65
    }
66
    return "{" + parse(o).join(", ") + "}";
67
   
68
}
69

    
70
// http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area 
71
$.fn.setCursorPosition = function(pos) {
72
    if ($(this).get(0).setSelectionRange) {
73
      $(this).get(0).setSelectionRange(pos, pos);
74
    } else if ($(this).get(0).createTextRange) {
75
      var range = $(this).get(0).createTextRange();
76
      range.collapse(true);
77
      range.moveEnd('character', pos);
78
      range.moveStart('character', pos);
79
      range.select();
80
    }
81
}
82

    
83
// indexOf prototype for IE
84
if (!Array.prototype.indexOf) {
85
  Array.prototype.indexOf = function(elt /*, from*/) {
86
    var len = this.length;
87
    var from = Number(arguments[1]) || 0;
88
    from = (from < 0)
89
         ? Math.ceil(from)
90
         : Math.floor(from);
91
    if (from < 0)
92
      from += len;
93

    
94
    for (; from < len; from++) {
95
      if (from in this &&
96
          this[from] === elt)
97
        return from;
98
    }
99
    return -1;
100
  };
101
}
102

    
103
// trim prototype for IE
104
if(typeof String.prototype.trim !== 'function') {
105
    String.prototype.trim = function() {
106
        return this.replace(/^\s+|\s+$/g, '');
107
    }
108
}
109

    
110
// simple string format helper (http://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format)
111
String.prototype.format = function() {
112
    var formatted = this;
113
    for (var i = 0; i < arguments.length; i++) {
114
        var regexp = new RegExp('\\{'+i+'\\}', 'gi');
115
        formatted = formatted.replace(regexp, arguments[i]);
116
    }
117
    return formatted;
118
};
119

    
120

    
121
// Show VNC console
122
function vnc_attachment(host, port, password) {
123
    // FIXME: Must be made into parameters, in settings.py
124
    //vnc = open("", "displayWindow",
125
    //    "status=yes,toolbar=yes,menubar=yes");
126
    vd = document.open("application/x-vnc");
127

    
128
    vd.writeln("[connection]");
129
    vd.writeln("host=" + host);
130
    vd.writeln("port=" + port);
131
    vd.writeln("password=" + password);
132

    
133
    vd.close();
134
}
135

    
136
// connect to machine action
137
function machine_connect(serverIDs){
138
    return;
139
    if (!serverIDs.length){
140
        //ajax_success('DEFAULT');
141
        return false;
142
    }
143
    
144
    // prefer metadata values for specific options (username, domain)
145
    var username_meta_key = 'user';
146
    var domain_meta_key = "domain";
147

    
148
    var serverID = serverIDs.pop();
149
    var machine = get_machine(serverID);
150
    var serverName = machine.name;
151

    
152
    try {
153
        var serverIP = machine.addresses.values[0].values[0].addr;
154
    } catch (err) { var serverIP = 'undefined'; }
155

    
156
    try {
157
        var os = os_icon(machine.metadata);
158
    } catch (err) { var os = 'undefined'; }
159

    
160
    var username = "";
161
    try {
162
        username = machine.metadata.values[username_meta_key];
163
    } catch (err) { username = undefined }
164

    
165
    var domain = "";
166
    try {
167
        domain = machine.metadata.values[domain_meta_key];
168
    } catch (erro) { domain = undefined }
169

    
170
    var params_url = '?ip_address=' + serverIP + '&os=' + os + "&host_os=" + $.client.os + "&srv=" + serverID;
171

    
172
    if (username) {
173
        params_url += "&username=" + username;
174
    }
175

    
176
    if (domain) {
177
        params_url += "&domain=" + domain;
178
    }
179
    
180
    //if ($.client.os == "Windows" && os == "windows") {
181
        //// request rdp file
182
        //window.open('machines/connect' + params_url + "&rdp=1");
183
        //return;
184
    //}
185
    
186
    // FIXME: I18n ???
187
    var title = 'Connect to: ' + 
188
        '<span class="machine-title"><img src="static/icons/machines/small/' + os + 
189
        '-on.png" /> ' + serverName + 
190
        '</span>';
191
    
192
    // open msg box and fill it with json data retrieved from connect machine view
193
    try {
194
        // open msg box
195
        msg_box({
196
            title:title, 
197
            fixed: true,
198
            content:'loading...',
199
            extra:'', 'ajax':'machines/connect' + params_url,
200
            parse_data:function(data){
201
                var box_content = "<a href='"+data.link.url+"'>"+data.link.title+"</a>";
202
                if (!data.link.url) {
203
                    box_content = "<span class='cmd'>"+data.link.title+"</span>";
204
                }
205
                data.title = false;
206
                data.content = data.info;
207
                data.extra = box_content;
208
                return data;
209
            }
210
        });
211
    } catch (error) {
212
        // if msg box fails fallback redirecting the user to the connect url
213
        window.open('machines/connect' + params_url);
214
    }
215

    
216

    
217
    // Restore os icon in list view
218
    osIcon = $('#'+serverID).parent().parent().find('.list-logo');
219
    osIcon.attr('src',osIcon.attr('os'));
220

    
221
    return false;
222
}
223

    
224

    
225
function msg_box(user_config) {
226
    var defaults = {'title':'Info message', 'content': 'this is an info message', 'ajax': false, 'extra':false};
227
    var config = $.extend(defaults, user_config);
228

    
229
    // prepare the error message
230
    // bring up success notification
231
    var box = $("#notification-box");
232
    box.addClass("notification-box");
233
    box.addClass('success');
234
    box.addClass(config.cls || '');
235
    box.removeClass('error');
236

    
237
    var sel = function(s){return $(s, box)};
238
    // reset texts
239
    sel("h3 span.header-box").html("");
240
    sel(".sub-text").html("");
241
    sel(".password-container .password").html("");
242
    sel("div.machine-now-building").html("");
243

    
244
    // apply msg box contents
245
    sel("h3 span.header-box").html(config.title);
246
    sel("div.machine-now-building").html(config.content);
247
    sel(".sub-text").html(config.sub_content || '');
248
    sel(".popup-header").removeClass("popup-header-error");
249
    box.removeClass("popup-border-error");
250
    sel(".popup-details").removeClass("popup-details-error");
251
    sel(".popup-separator").removeClass("popup-separator-error");
252
    
253
    sel(".password-container").hide();
254
    if (config.extra) {
255
        sel(".password-container .password").html(config.extra);
256
        sel(".password-container").show();
257
    }
258
    
259
    var conf = {
260
        // some mask tweaks suitable for modal dialogs
261
        mask: '#666',
262
        top: '10px',
263
        closeOnClick: false,
264
        oneInstance: false,
265
        load: false,
266
        onLoad: config.onLoad || false,
267
        fixed: config.fixed || false,
268
        onClose: function () {
269
            // With partial refresh working properly,
270
            // it is no longer necessary to refresh the whole page
271
            // choose_view();
272
        }
273
    }
274

    
275
    var triggers = $("a#msgbox").overlay(conf);
276

    
277
    try {
278
        conf = $("a#msgbox").data('overlay').getConf();
279
        conf.fixed = config.fixed || false;
280
    } catch (err) {}
281
    $("a#msgbox").data('overlay').load();
282
    
283
    var parse_data = config.parse_data || false;
284
    var load_html = config.html || false;
285
    var user_success = config.success || false;
286
    config.ajax = config.ajax || {};
287

    
288
    // requested to show remote data in msg_box
289
    if (config.ajax && !$.isEmptyObject(config.ajax)) {
290
        $.ajax($.extend({ 
291
            url:config.ajax, 
292
            success: function(data){
293
                // we want to get our data parsed before
294
                // placing them in content
295
                if (parse_data) {
296
                    data = parse_data(data);
297
                }
298

    
299
                // no json response
300
                // load html body
301
                if (load_html) {
302
                    sel("div.machine-now-building").html(data);
303
                } else {
304

    
305
                    if (data.title) {
306
                        sel("h3 span.header-box").text(data.title);
307
                    }
308

    
309
                    if (data.content) {
310
                        sel("div.machine-now-building").html(data.content);
311
                    }
312
                    if (data.extra) {
313
                        sel(".password-container .password").html(data.extra);
314
                        sel(".password-container").show();
315
                    }
316
                    if (data.subinfo) {
317
                        sel(".sub-text").html(data.subinfo);
318
                    } else {
319
                        sel(".sub-text").html("");
320
                    }
321
                }
322

    
323
                if (user_success) {
324
                    user_success($("div.machine-now-building"));
325
                }
326
            },
327
            error: function(xhr, status, err) {
328
                ajax_error(-519, "UI Error", "Machine connect", err, this);
329
            }
330
        }, config.ajax_config));
331
    }
332
    return false;
333
}
334

    
335

    
336
function show_api_overlay() {
337
    var config = {
338
        title: window.API_OVERLAY_TITLE,
339
        content: $(".api_overlay_content").html().replace("$api_key", $.cookie("X-Auth-Token")),
340
        extra: $.cookie("X-Auth-Token"),
341
        sub_content: window.API_OVERLAY_SUBCONTENT,
342
        cls: "api_content",
343
        ajax: false
344
    }
345
    msg_box(config);
346
}
347

    
348
function show_invitations() {
349
    
350
    function display_resend_success(msg) {
351
        clear_resend_messages();
352
        $("#invsent .message.success").text(msg).show();
353
    }
354

    
355
    function display_resend_error(msg) {
356
        clear_resend_messages();
357
        $("#invsent .message.errormsg").text(msg).show();
358
    }
359

    
360
    // clear resent messages
361
    function clear_resend_messages() {
362
        $("#invsent .message").hide();
363
    }
364

    
365
    // register resent click handlers
366
    function register_invitation_resends() {
367
        $(".invitations .resend-invitation").click(function() {
368
            var invid = $(this).attr("id");
369

    
370
            if (invid == null)
371
                return;
372

    
373
            var id = invid.split("-")[1];
374

    
375
            if (id == null)
376
                return;
377

    
378
            var child = $(this).find("img");
379
            child.attr('src', '/static/progress-tiny.gif');
380

    
381
            $.ajax({
382
                type: "POST",
383
                url : "/invitations/resend",
384
                data : {invid : id},
385
                success: function(msg) {
386
                    display_resend_success("Invitation has been resent");
387
                    child.attr('src', '/static/resend.png');
388
                },
389
                error : function(xhr, status, error) {
390
                    display_resend_error("Something seems to have gone wrong. " +
391
                          "Please try again in a few minutes.");
392
                    child.attr('src', '/static/resend.png');
393
                }
394
            });
395
        });
396
    }
397

    
398
    handle_invitations = function(el) {
399

    
400
        // proper class to identify the overlay block
401
        el.addClass("invitations");
402

    
403
        var cont = el;
404
        var form = $(el).find("form");
405

    
406
        // remove garbage rows that stay in DOM between requests
407
        $(".removable-field-row:hidden").remove();
408

    
409
        // avoid buggy behaviour, close all overlays if something went wrong
410
        try {
411
            // form is in content (form is not displayed if user has no invitations)
412
            if ($("#invform #removable-name-container-1").length) {
413
                $("#invform #removable-name-container-1").dynamicField();
414
            }
415
        } catch (err) {
416
            close_all_overlays();
417
        }
418
        
419
        // we copy/paste it on the title no need to show it twice
420
        $(".invitations-left").hide();
421
        
422
        // sending finished or first invitations view
423
        $(".invitations .sending").hide();
424
        $(".invitations .submit").show();
425
        $(".invitations #fieldheaders").show();
426
        $(".invitations #fields").show();
427

    
428
        // reset title
429
        $("#notification-box .header-box").html("");
430
        $("#notification-box .header-box").html(window.INVITATIONS_TITLE + " " + $($(".invitations-left")[0]).text());
431
    
432
        // resend buttons
433
        register_invitation_resends();
434
        clear_resend_messages();
435

    
436
        // handle form submit
437
        form.submit(function(evn){
438
            evn.preventDefault();
439
            
440
            // sending...
441
            $(".invitations .sending").show();
442
            $(".invitations .submit").hide();
443
            $(".invitations #fieldheaders").hide();
444
            $(".invitations #fields").hide();
445

    
446
            // do the post
447
            $.post(form.attr("action"), form.serialize(), function(data) {
448
                // replace data
449
                $(cont).html(data); 
450

    
451
                // append all handlers again (new html data need to redo all changes)
452
                handle_invitations(cont);
453
            });
454

    
455
            return false;
456
        });
457
    }
458
    
459
    // first time clicked (show the msg box with /invitations content)
460
    msg_box({
461
        title:window.INVITATIONS_TITLE, 
462
        content:'', 
463
        fixed: false,
464
        ajax:INVITATIONS_URL, 
465
        html:true, 
466
        success: function(el){ 
467
            handle_invitations(el)
468
        }
469
    });
470
}
471

    
472

    
473
function get_short_v6(v6, parts_to_keep) {
474
    var parts = v6.split(":");
475
    var new_parts = parts.slice(parts.length - parts_to_keep);
476
    return new_parts.join(":");
477
}
478

    
479
function fix_v6_addresses() {
480

    
481
    // what to prepend
482
    var match = "...";
483
    // long ip min length
484
    var limit = 20;
485
    // parts to show after the transformation
486
    // (from the end)
487
    var parts_to_keep_from_end = 4;
488

    
489
    $(".machine .ipv6-text").each(function(index, el){
490
        var el = $(el);
491
        var ip = $(el).text();
492
            
493
        // transformation not applyied
494
        // FIXME: use $.data for the condition
495
        if (ip.indexOf(match) == -1 && ip != "pending") {
496
            
497
            // only too long ips
498
            if (ip.length > 20) {
499
                $(el).data("ipstring", ip);
500
                $(el).text(match + get_short_v6(ip, parts_to_keep_from_end));
501
                $(el).attr("title", ip);
502
                $(el).tooltip({'tipClass':'tooltip ipv6-tip', 'position': 'center center'});
503
            }
504
        } else {
505
            if (ip.indexOf(match) == 0) {
506
            } else {
507
                // not a long ip anymore
508
                $(el).data("ipstring", undefined);
509
                $(el).css({'text-decoration':'none'});
510

    
511
                if ($(el).data('tooltip')) {
512
                    $(el).data('tooltip').show = function () {};
513
                }
514
            }
515
        }
516
    });
517
}
518

    
519
// get stats
520
function get_server_stats(serverID) {
521
    
522
    // do not update stats if machine in build state
523
    var vm = get_machine(serverID);
524
    if (vm.status == "BUILD" && vm.stats_timeout) {
525
        els = get_current_view_stats_elements(vm.id);
526
        els.cpu.img.hide();
527
        els.net.img.hide();
528

    
529
        els.cpu.busy.show();
530
        els.net.busy.show();
531
        return;
532
    }
533

    
534
    $.ajax({
535
        repeated: true,
536
        url: API_URL + '/servers/' + serverID + '/stats',
537
        cache: false,
538
        type: "GET",
539
        //async: false,
540
        dataType: "json",
541
        timeout: TIMEOUT,
542
        error: function(jqXHR, textStatus, errorThrown) {
543
            handle_api_error(-21, undefined, 'Get server stats', jqXHR, textStatus, errorThrown, this);
544
        },
545
        success: function(data, textStatus, jqXHR) {
546
            //update_machine_stats(serverID, data);
547
        },
548

    
549
        // pass server id to ajax settings
550
        serverID: serverID
551
    });
552
    return false;
553
}
554

    
555
function get_progress_details(id) {
556
    var vm = get_machine(id);
557
    var progress = vm.progress;
558

    
559
    // no details for active machines
560
    if (!vm.status == "BUILD") {
561
        return false;
562
    }
563
    
564
    // check if images not loaded yet
565
    try {
566
        var image = synnefo.storage.images.get(vm.imageRef).attributes;
567
        var size = image.size;
568
    } catch (err) {
569
        // images not loaded yet (can this really happen ??)
570
        return;
571
    }
572
    
573
    var to_copy = size;
574
    var copied = (size * progress / 100).toFixed(2);
575
    var status = "INIT"
576

    
577
    // apply state
578
    if (progress > 0) { status = "IMAGE_COPY" }
579
    if (progress >= 100) { status = "FINISH" }
580
    
581
    // user information
582
    var msg = BUILDING_MESSAGES[status];
583

    
584
    // image copy state display extended user information
585
    //if (status == "IMAGE_COPY") {
586
        //msg = msg.format(readablizeBytes(copied*(1024*1024)), readablizeBytes(to_copy*(1024*1024)), progress)
587
    //}
588

    
589
    var progress_data = {
590
        'percent': vm.progress,
591
        'build_status': status,
592
        'copied': copied,
593
        'to_copy': size,
594
        'msg': msg
595
    }
596

    
597
    return progress_data;
598
}
599

    
600
// display user friendly bytes amount
601
function readablizeBytes(bytes) {
602
    var s = ['bytes', 'kb', 'MB', 'GB', 'TB', 'PB'];
603
    var e = Math.floor(Math.log(bytes)/Math.log(1024));
604
    return (bytes/Math.pow(1024, Math.floor(e))).toFixed(2)+" "+s[e];
605
}
606

    
607
// machines images utils
608
function set_machine_os_image(machine, machines_view, state, os, skip_reset_states, remove_state) {
609
    var views_map = {'single': '.single-image', 'icon': '.logo'};
610
    var states_map = {'on': 'state1', 'off': 'state3', 'hover': 'state4', 'click': 'state2'}
611
    var sizes_map = {'single': 'large', 'icon': 'medium'}
612

    
613
    var size = sizes_map[machines_view];
614
    var img_selector = views_map[machines_view];
615
    var cls = states_map[state];
616

    
617
    if (os === "unknown") { os = "okeanos" } ;
618
    var new_img = 'url("./static/icons/machines/' + size + '/' + os + '-sprite.png")';
619

    
620
    var el = $(img_selector, machine);
621
    var current_img = el.css("backgroundImage");
622
    if (os == undefined){
623
        new_img = current_img;
624
    }
625

    
626
    // os changed
627
    el.css("backgroundImage", new_img);
628

    
629
    // reset current state
630
    if (skip_reset_states === undefined)
631
    {
632
        el.removeClass("single-image-state1");
633
        el.removeClass("single-image-state2");
634
        el.removeClass("single-image-state3");
635
        el.removeClass("single-image-state4");
636
    }
637

    
638
    if (remove_state !== undefined)
639
    {
640
        remove_state = "single-image-" + states_map[remove_state];
641
        el.removeClass(remove_state);
642
        return;
643
    }
644

    
645
    // set proper state
646
    el.addClass("single-image-" + cls);
647
}
648

    
649
// return machine entry from serverID
650
function get_machine(serverID) {
651
    return synnefo.storage.vms.get(serverID).attributes;
652
}
653