Revision 426e1fb9

b/snf-cyclades-app/conf/20-snf-cyclades-app-ui.conf
39 39
## consecutive API calls (aligning changes-since attribute).
40 40
#UI_CHANGES_SINCE_ALIGNMENT = 0
41 41
#
42
## How often to check for user usage changes
43
#UI_QUOTAS_UPDATE_INTERVAL = 10000
44
#
45 42
## URL to redirect not authenticated users
46 43
#UI_LOGIN_URL = "/im/login"
47 44
#
......
183 180
## The name of the grouped network view
184 181
#UI_GROUPED_PUBLIC_NETWORK_NAME = 'Internet'
185 182
#
183
## Endpoint to make account specific requests (resources/quotas)
184
#UI_ACCOUNTS_API_URL = '/astakos/api'
186 185
#
187 186
################
188 187
## UI EXTENSIONS
b/snf-cyclades-app/synnefo/api/delegate.py
42 42
ASTAKOS_URL = getattr(settings, 'ASTAKOS_URL', None)
43 43
USER_CATALOG_URL = urlparse.urljoin(ASTAKOS_URL, "user_catalogs")
44 44
USER_FEEDBACK_URL = urlparse.urljoin(ASTAKOS_URL, "feedback")
45
USER_QUOTA_URL = urlparse.urljoin(ASTAKOS_URL, "astakos/api/quotas")
46
RESOURCES_URL = urlparse.urljoin(ASTAKOS_URL, "astakos/api/resources")
45 47

  
46 48
from objpool.http import PooledHTTPConnection
47 49

  
......
68 70

  
69 71

  
70 72
@csrf_exempt
73
def delegate_to_resources_service(request):
74
    logger.debug("Delegate resources request to %s" % RESOURCES_URL)
75
    token = request.META.get('HTTP_X_AUTH_TOKEN')
76
    headers = {'X-Auth-Token': token}
77
    return proxy(request, RESOURCES_URL, headers=headers,
78
                 body=request.raw_post_data)
79

  
80

  
81
@csrf_exempt
82
def delegate_to_user_quota_service(request):
83
    logger.debug("Delegate quotas request to %s" % USER_QUOTA_URL)
84
    token = request.META.get('HTTP_X_AUTH_TOKEN')
85
    headers = {'X-Auth-Token': token}
86
    return proxy(request, USER_QUOTA_URL, headers=headers,
87
                 body=request.raw_post_data)
88

  
89

  
90
@csrf_exempt
71 91
def delegate_to_feedback_service(request):
72 92
    logger.debug("Delegate feedback request to %s" % USER_FEEDBACK_URL)
73 93
    token = request.META.get('HTTP_X_AUTH_TOKEN')
b/snf-cyclades-app/synnefo/app_settings/urls.py
51 51
    urlpatterns += patterns(
52 52
        '',
53 53
        (r'^feedback/?$', 'synnefo.api.delegate.delegate_to_feedback_service'),
54
        (r'^user_catalogs/?$', 'synnefo.api.delegate.delegate_to_user_catalogs_service'))
55

  
54
        (r'^user_catalogs/?$',
55
         'synnefo.api.delegate.delegate_to_user_catalogs_service'),
56
        (r'^astakos/api/resources/?$',
57
         'synnefo.api.delegate.delegate_to_resources_service'),
58
        (r'^astakos/api/quotas/?$',
59
         'synnefo.api.delegate.delegate_to_user_quota_service'))
b/snf-cyclades-app/synnefo/ui/static/snf/js/models.js
615 615

  
616 616
                var _success = _.bind(function() {
617 617
                    if (success) { success() };
618
                    snf.ui.main.load_user_quotas();
618
                    synnefo.storage.quotas.get('cyclades.network.private').decrease();
619 619
                }, this);
620 620
                var _error = _.bind(function() {
621 621
                    this.set({state: previous_state, status: previous_status})
......
1441 1441
                                             // set state after successful call
1442 1442
                                             self.state('DESTROY');
1443 1443
                                             success.apply(this, arguments);
1444
                                             snf.ui.main.load_user_quotas();
1444
                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
1445 1445

  
1446 1446
                                         },  
1447 1447
                                         error, 'destroy', params);
......
1693 1693
            
1694 1694
            var cb = function() {
1695 1695
              callback();
1696
              snf.ui.main.load_user_quotas();
1696
              synnefo.storage.quotas.get('cyclades.network.private').increase();
1697 1697
            }
1698 1698
            return this.api_call(this.path, "create", params, cb);
1699 1699
        },
......
2122 2122
                }
2123 2123
            }
2124 2124
            
2125
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta}
2125
            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, 
2126
                    metadata:meta}
2126 2127
            opts = _.extend(opts, extra);
2127 2128
            
2128 2129
            var cb = function(data) {
2129
              snf.ui.main.load_user_quotas();
2130
              synnefo.storage.quotas.get('cyclades.vm').increase();
2130 2131
              callback(data);
2131 2132
            }
2132 2133

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

  
2136 2138
        load_missing_images: function(callback) {
......
2365 2367
            
2366 2368
            this.create(m.attributes, options);
2367 2369
        }
2370
    });
2371

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

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

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

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

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

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

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

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

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

  
2461
        parse: function(resp) {
2462
            return _.map(resp, function(value, key) {
2463
                return _.extend(value, {'name': key, 'id': key});
2464
            })
2465
        }
2466
    });
2369 2467
    
2370 2468
    // storage initialization
2371 2469
    snf.storage.images = new models.Images();
......
2374 2472
    snf.storage.vms = new models.VMS();
2375 2473
    snf.storage.keys = new models.PublicKeys();
2376 2474
    snf.storage.nics = new models.NICs();
2377

  
2378
    //snf.storage.vms.fetch({update:true});
2379
    //snf.storage.images.fetch({update:true});
2380
    //snf.storage.flavors.fetch({update:true});
2475
    snf.storage.resources = new models.Resources();
2476
    snf.storage.quotas = new models.Quotas();
2381 2477

  
2382 2478
})(this);
b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_create_view.js
678 678
              image_excluded = storage.flavors.unavailable_values_for_image(this.current_image);
679 679
            }
680 680

  
681
            if (snf.user.quota) {
682
              quotas = this.get_vm_params_quotas();
683
              user_excluded = storage.flavors.unavailable_values_for_quotas(quotas);
684
            }
681
            quotas = this.get_vm_params_quotas();
682
            user_excluded = storage.flavors.unavailable_values_for_quotas(quotas);
685 683

  
686 684
            unavailable.disk = user_excluded.disk.concat(image_excluded.disk);
687 685
            unavailable.ram = user_excluded.ram.concat(image_excluded.ram);
......
691 689
        },
692 690
        
693 691
        get_vm_params_quotas: function() {
692
          var quotas = synnefo.storage.quotas;
694 693
          var quota = {
695
            'ram': snf.user.quota.get_available('cyclades.ram'),
696
            'cpu': snf.user.quota.get_available('cyclades.cpu'),
697
            'disk': snf.user.quota.get_available('cyclades.disk')
694
            'ram': quotas.get('cyclades.ram').get('available'),
695
            'cpu': quotas.get('cyclades.cpu').get('available'),
696
            'disk': quotas.get('cyclades.disk').get('available')
698 697
          }
699 698
          return quota;
700 699
        },
......
769 768
                };
770 769
            }, this));
771 770

  
772
            this.$("#create-vm-flavor-options .flavor-options.ram li").each(_.bind(function(i, el){
771
            this.$("#create-vm-flavor-options .flavor-options.mem li").each(_.bind(function(i, el){
773 772
                var el_value = $(el).data("value");
774 773
                if (this.unavailable_values.ram.indexOf(el_value) > -1) {
775 774
                    $(el).addClass("disabled");
......
941 940
        },
942 941
        
943 942
        update_quota_display: function() {
944
          if (!snf.user.quota || !snf.user.quota.data) { return };
945 943

  
944
          var quotas = synnefo.storage.quotas;
946 945
          _.each(["disk", "ram", "cpu"], function(type) {
947
            var available_dsp = snf.user.quota.get_available_readable(type);
948
            var available = snf.user.quota.get_available(type);
946
            var available_dsp = quotas.get('cyclades.'+type).get_readable('available');
947
            var available = quotas.get('cyclades.'+type).get('available');
949 948
            var content = "({0} left)".format(available_dsp);
950 949
            if (available <= 0) { content = "(None left)" }
951 950
            
b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_main_view.js
231 231
                this.fix_position();
232 232
            }, this));
233 233

  
234
            storage.vms.bind("change:pending_action", _.bind(this.handle_action_add, this, "vms"));
235
            storage.vms.bind("change:reboot_required", _.bind(this.handle_action_add, this, "reboots"));
236
            storage.networks.bind("change:actions", _.bind(this.handle_action_add, this, "nets"));
234
            storage.vms.bind("change:pending_action", 
235
                             _.bind(this.handle_action_add, this, "vms"));
236
            storage.vms.bind("change:reboot_required", 
237
                             _.bind(this.handle_action_add, this, "reboots"));
238
            storage.networks.bind("change:actions", 
239
                                  _.bind(this.handle_action_add, this, "nets"));
237 240
        },
238 241

  
239 242
        handle_action_add: function(type, model, action) {
......
532 535
            storage.vms.bind("add", _.bind(this.check_empty, this));
533 536
            storage.vms.bind("change:status", _.bind(this.check_empty, this));
534 537
            storage.vms.bind("reset", _.bind(this.check_empty, this));
538
            storage.quotas.bind("change", _.bind(this.update_create_buttons_status, this));
535 539
            
536 540
        },
537 541
        
......
592 596
            $(".css-panes").show();
593 597
        },
594 598
        
595
        items_to_load: 4,
599
        items_to_load: 6,
596 600
        completed_items: 0,
597 601
        check_status: function(loaded) {
598 602
            this.completed_items++;
......
629 633
                self.update_status("networks", 1);
630 634
                self.check_status();
631 635
            }});
636

  
632 637
        },  
633 638

  
634 639
        init_intervals: function() {
......
642 647
            
643 648
            this._networks = storage.networks.get_fetcher.apply(storage.networks, _.clone(fetcher_params));
644 649
            this._vms = storage.vms.get_fetcher.apply(storage.vms, _.clone(fetcher_params));
650
            this._quotas = storage.quotas.get_fetcher.apply(storage.quotas, _.clone(fetcher_params));
645 651
        },
646 652

  
647 653
        stop_intervals: function() {
648 654
            if (this._networks) { this._networks.stop(); }
649 655
            if (this._vms) { this._vms.stop(); }
656
            if (this._quotas) { this._quotas.stop(); }
650 657
            this.intervals_stopped = true;
651 658
        },
652 659

  
......
665 672
                this.init_intervals();
666 673
            }
667 674

  
675
            if (this._quotas) {
676
                this._quotas.stop();
677
                this._quotas.start();
678
            } else {
679
                this.init_intervals();
680
            }
681

  
668 682
            this.intervals_stopped = false;
669 683
        },
670 684

  
......
683 697
            snf.config.update_hidden_views = uhv;
684 698

  
685 699
            window.setTimeout(function() {
686
                self.update_status("layout", 0);
687 700
                self.load_initialize_overlays();
688 701
            }, 20);
689 702
        },
......
732 745
                self.update_status("flavors", 1);
733 746
                self.check_status()
734 747
            }});
748

  
749
            this.update_status("resources", 0);
750
            storage.resources.fetch({refresh:true, update:false, success: function(){
751
                self.update_status("resources", 1);
752
                self.update_status("quotas", 0);
753
                self.check_status();
754
                storage.quotas.fetch({refresh:true, update:true, success: function() {
755
                  self.update_status("quotas", 1);
756
                  self.update_status("layout", 1);
757
                  self.check_status()
758
                }})
759
            }})
735 760
        },
736 761

  
737 762
        update_status: function(ns, state) {
......
779 804
            }
780 805
        },
781 806
        
782
        quota_handlers_initialized: false,
783

  
784
        load_user_quotas: function(repeat) {
785
          var main_view = this;
786
          if (!snf.user.quota) {
787
            snf.user.quota = new snf.quota.Quota("cyclades");
788
            main_view.init_quotas_handlers();
789
      
790
          }
791

  
792
          snf.api.sync('read', undefined, {
793
            url: synnefo.config.quota_url, 
794
            success: function(d) {
795
              snf.user.quota.load(d);
796
            },
797
            complete: function() {
798
                if (repeat) {
799
                  setTimeout(function(){
800
                      main_view.load_user_quotas(1);
801
                  }, synnefo.config.quotas_update_interval || 10000);
802
                }
803
            }
804
          });
805
        },
806
        
807
        check_quotas: function(type) {
808
          var storage = synnefo.storage[type];
809
          var consumed = storage.length;
810
          var quotakey = {
811
            'networks': 'cyclades.network.private',
812
            'vms': 'cyclades.vm'
813
          }
814
          if (type == "networks") {
815
            consumed = storage.filter(function(net){
816
              return !net.is_public() && !net.is_deleted();
817
            }).length;
818
          }
819
          
820
          var limit = snf.user.quota.get_limit(quotakey[type]);
821
          if (snf.user.quota && snf.user.quota.data && consumed >= limit) {
822
            storage.trigger("quota_reached");
823
          } else {
824
            storage.trigger("quota_free");
825
          }
826
        },
827

  
828
        init_quotas_handlers: function() {
829
          var self = this, event;
830
          snf.user.quota.bind("cyclades.vm.quota.changed", function() {
831
            this.check_quotas("vms");
832
          }, this);
833

  
834
          var event = "cyclades.network.private.quota.changed";
835
          snf.user.quota.bind(event, function() {
836
            this.check_quotas("networks");
837
          }, this);
838
        },
839

  
840 807
        // initial view based on user cookie
841 808
        show_initial_view: function() {
842 809
          this.set_vm_view_handlers();
843
          this.load_user_quotas(1);
844 810
          this.hide_loading_view();
845
          
846 811
          bb.history.start();
847

  
848 812
          this.trigger("ready");
849 813
        },
850 814

  
......
853 817
              this.router.vm_details_view(vm.id);
854 818
            }
855 819
        },
820
        
821
        update_create_buttons_status: function() {
822
            var nets = storage.quotas.get('cyclades.network.private');
823
            var vms = storage.quotas.get('cyclades.vm');
824
            
825
            if (!nets || !vms) { return }
826

  
827
            if (!nets.can_consume()) {
828
                $("#networks-pane a.createbutton").addClass("disabled");
829
            } else {
830
                $("#networks-pane a.createbutton").removeClass("disabled");
831
            }
832

  
833
            if (!vms.can_consume()) {
834
                $("#createcontainer #create").addClass("disabled");
835
            } else {
836
                $("#createcontainer #create").removeClass("disabled");
837
            }
838
        },
856 839

  
857 840
        set_vm_view_handlers: function() {
858 841
            var self = this;
......
861 844
                if ($(this).hasClass("disabled")) { return }
862 845
                self.router.vm_create_view();
863 846
            });
864

  
865
            synnefo.storage.vms.bind("quota_reached", function(){
866
              $("#createcontainer #create").addClass("disabled");
867
              $("#createcontainer #create").attr("title", "Machines limit reached");
868
            });
869

  
870
            synnefo.storage.vms.bind("quota_free", function(){
871
              $("#createcontainer #create").removeClass("disabled");
872
              $("#createcontainer #create").attr("title", "");
873
            });
874

  
875 847
        },
876 848

  
877 849
        check_empty: function() {
b/snf-cyclades-app/synnefo/ui/templates/home.html
558 558
    <div id="loading-view" class="hidden clearfix">
559 559
        <div class="header clearfix images off">Loading images...<span></span></div>
560 560
        <div class="header clearfix flavors off">Loading flavors...<span></span></div>
561
        <div class="header clearfix resources off">Loading resources...<span></span></div>
562
        <div class="header clearfix quotas off">Loading quotas...<span></span></div>
561 563
        <div class="header clearfix vms off">Loading machines...<span></span></div>
562 564
        <div class="header clearfix networks off">Loading networks...<span></span></div>
563 565
        <div class="header clearfix layout off">Rendering layout...<span></span></div>
......
611 613
   			// TODO: make it dynamic
612 614
            synnefo.config.api_urls = {
613 615
                'compute':  {{ compute_api_url|safe }}, 
614
                'glance': {{ glance_api_url|safe }}
616
                'glance': {{ glance_api_url|safe }},
617
                'accounts': {{ accounts_api_url|safe }},
615 618
            };
616 619
            
617 620
            // TODO: configurable userdata urls in models.js
......
649 652
            synnefo.config.grouped_public_network_name = {{ grouped_public_network_name|safe }};
650 653
            synnefo.config.vm_hostname_format = {{ vm_hostname_format|safe }};
651 654
            synnefo.config.automatic_network_range_format = {{ automatic_network_range_format|safe }};
652
            synnefo.config.quota_url = '{% url synnefo.ui.views.user_quota %}';
653 655
            synnefo.config.custom_image_help_url = '{{ custom_image_help_url|safe }}';
654 656
            synnefo.config.quotas_update_interval = {{ quotas_update_interval }};
655 657
            
b/snf-cyclades-app/synnefo/ui/urls.py
37 37

  
38 38
urlpatterns = patterns('',
39 39
    url(r'^$', 'synnefo.ui.views.home', name='ui_index'),
40
    url(r'^userquota$', 'synnefo.ui.views.user_quota', name='ui_userquota'),
41 40
    url(r'userdata/', include('synnefo.ui.userdata.urls'))
42 41
)
43 42

  
b/snf-cyclades-app/synnefo/ui/views.py
62 62
    getattr(settings, "UI_UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT", 3)
63 63
UPDATE_INTERVAL_FAST = getattr(settings, "UI_UPDATE_INTERVAL_FAST", 2500)
64 64
UPDATE_INTERVAL_MAX = getattr(settings, "UI_UPDATE_INTERVAL_MAX", 10000)
65
QUOTAS_UPDATE_INTERVAL = getattr(settings, "UI_QUOTAS_UPDATE_INTERVAL", 10000)
66 65

  
67 66
# predefined values settings
68 67
VM_IMAGE_COMMON_METADATA = \
......
161 160
USER_CATALOG_URL = getattr(settings, 'UI_USER_CATALOG_URL', '/user_catalogs')
162 161
FEEDBACK_POST_URL = getattr(settings, 'UI_FEEDBACK_POST_URL', '/feedback')
163 162
TRANSLATE_UUIDS = not getattr(settings, 'TRANSLATE_UUIDS', False)
163
ACCOUNTS_API_URL = getattr(settings, 'UI_ACCOUNTS_API_URL', '/astakos/api')
164 164

  
165 165

  
166 166
def template(name, request, context):
......
189 189
               'compute_api_url': json.dumps(COMPUTE_API_URL),
190 190
               'user_catalog_url': json.dumps(USER_CATALOG_URL),
191 191
               'feedback_post_url': json.dumps(FEEDBACK_POST_URL),
192
               'accounts_api_url': json.dumps(ACCOUNTS_API_URL),
192 193
               'translate_uuids': json.dumps(TRANSLATE_UUIDS),
193 194
               # update interval settings
194 195
               'update_interval': UPDATE_INTERVAL,
195 196
               'update_interval_increase': UPDATE_INTERVAL_INCREASE,
196 197
               'update_interval_increase_after_calls':
197
               UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT,
198
                UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT,
198 199
               'update_interval_fast': UPDATE_INTERVAL_FAST,
199 200
               'update_interval_max': UPDATE_INTERVAL_MAX,
200 201
               'changes_since_alignment': CHANGES_SINCE_ALIGNMENT,
201
               'quotas_update_interval': QUOTAS_UPDATE_INTERVAL,
202 202
               'image_icons': IMAGE_ICONS,
203 203
               'logout_redirect': LOGOUT_URL,
204 204
               'login_redirect': LOGIN_URL,
......
255 255
    return template('machines_console', request, context)
256 256

  
257 257

  
258
def user_quota(request):
259
    get_user(request, settings.ASTAKOS_URL, usage=True)
260

  
261
    response = json.dumps(request.user['usage'])
262

  
263
    return HttpResponse(response, mimetype="application/json")
264

  
265

  
266 258
def js_tests(request):
267 259
    return template('tests', request, {})
268 260

  

Also available in: Unified diff