This Series of Patches implements quota create/edit/delete actions in the UI. It also adds auditing to Quota to allow QoS Statistics. It also puts prevention mechanism to stop users creating instances, that would push the quota over its limits.
From: martyntaylor mtaylor@redhat.com
--- src/app/models/instance_observer.rb | 35 ++++++++++++++++++++++- src/app/models/quota.rb | 8 +++++ src/db/migrate/20090802000000_create_quotas.rb | 11 +++++++ src/spec/factories/hardware_profile.rb | 3 +- src/spec/factories/instance.rb | 4 +- src/spec/factories/quota.rb | 8 +++++ src/spec/models/instance_observer_spec.rb | 19 +++++++++++++ 7 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 src/spec/factories/quota.rb
diff --git a/src/app/models/instance_observer.rb b/src/app/models/instance_observer.rb index fa5b3d2..ab1a095 100644 --- a/src/app/models/instance_observer.rb +++ b/src/app/models/instance_observer.rb @@ -1,11 +1,14 @@ class InstanceObserver < ActiveRecord::Observer
+ ACTIVE_STATES = [ Instance::STATE_PENDING, Instance::STATE_RUNNING, Instance::STATE_SHUTTING_DOWN ] + def before_save(an_instance) if an_instance.changed? change = an_instance.changes['state'] if change update_state_timestamps(change[1], an_instance) update_accumulative_state_time(change[0], an_instance) + update_quota(change[0], change[1], an_instance) end end end @@ -26,7 +29,35 @@ class InstanceObserver < ActiveRecord::Observer when Instance::STATE_SHUTTING_DOWN then an_instance.acc_shutting_down_time += Time.now - an_instance.time_last_shutting_down when Instance::STATE_STOPPED then an_instance.acc_stopped_time += Time.now - an_instance.time_last_stopped end - end + end + + def update_quota(state_from, state_to, an_instance) + + hwp = HardwareProfile.find(an_instance.hardware_profile_id) + quota = Quota.find(:first, :conditions => { :pool_id => an_instance.pool_id }) + + if(quota) + if state_to == Instance::STATE_RUNNING + #TODO update running cpus + quota.running_instances += 1 + quota.running_memory += hwp.memory + elsif state_from == Instance::STATE_RUNNING + #TODO update running cpus + quota.running_instances -- + quota.running_memory -= hwp.memory + end + + if !ACTIVE_STATES.include?(state_from) && ACTIVE_STATES.include?(state_to) + quota.total_storage += hwp.storage + quota.total_instances += 1 + elsif ACTIVE_STATES.include?(state_from) && !ACTIVE_STATES.include?(state_to) + quota.total_storage -= hwp.storage + quota.total_instances -= 1 + end + + quota.save + end + end end
-InstanceObserver.instance \ No newline at end of file +InstanceObserver.instance diff --git a/src/app/models/quota.rb b/src/app/models/quota.rb index 23f9aee..c772c0b 100644 --- a/src/app/models/quota.rb +++ b/src/app/models/quota.rb @@ -22,4 +22,12 @@ class Quota < ActiveRecord::Base has_one :pool has_one :cloud_account + + validates_presence_of :maximum_running_instances + validates_presence_of :maximum_running_memory + validates_presence_of :maximum_running_cpus + + validates_presence_of :maximum_total_storage + validates_presence_of :maximum_total_instances + end diff --git a/src/db/migrate/20090802000000_create_quotas.rb b/src/db/migrate/20090802000000_create_quotas.rb index c61fb16..29cc5bf 100644 --- a/src/db/migrate/20090802000000_create_quotas.rb +++ b/src/db/migrate/20090802000000_create_quotas.rb @@ -25,6 +25,17 @@ class CreateQuotas < ActiveRecord::Migration t.integer :running_instances t.integer :total_storage t.integer :total_instances + t.integer :pool_id + t.integer :running_cpus, :default => 0 + t.integer :running_memory, :default => 0 + t.integer :running_instances, :default => 0 + t.integer :total_storage, :default => 0 + t.integer :total_instances, :default => 0 + t.integer :maximum_running_cpus + t.integer :maximum_running_memory + t.integer :maximum_running_instances + t.integer :maximum_total_storage + t.integer :maximum_total_instances t.integer :lock_version, :default => 0 t.timestamps end diff --git a/src/spec/factories/hardware_profile.rb b/src/spec/factories/hardware_profile.rb index 43f63f3..95ce13a 100644 --- a/src/spec/factories/hardware_profile.rb +++ b/src/spec/factories/hardware_profile.rb @@ -24,7 +24,8 @@ Factory.define :pool_hwp1, :parent => :hardware_profile do |p| end
Factory.define :hardware_profile_auto, :parent => :hardware_profile do |p| - p.external_key { |hp| hp.name + "_key" } + p.sequence(:name) { |n| "hardware_profile#{n}" } + p.external_key { |hp| "#{hp.name}_external_key" } p.storage 160 p.memory 1024 p.architecture "i386" diff --git a/src/spec/factories/instance.rb b/src/spec/factories/instance.rb index 3a453c3..890b6b9 100644 --- a/src/spec/factories/instance.rb +++ b/src/spec/factories/instance.rb @@ -1,7 +1,7 @@ Factory.define :instance do |i| i.sequence(:name) { |n| "instance#{n}" } i.sequence(:external_key) { |n| "key#{n}" } - i.hardware_profile_id 1 + i.association :hardware_profile, :factory => :hardware_profile_auto i.image_id 1 i.pool_id 1 i.state "running" @@ -13,4 +13,4 @@ end
Factory.define :new_instance, :parent => :instance do |i| i.state Instance::STATE_NEW -end \ No newline at end of file +end diff --git a/src/spec/factories/quota.rb b/src/spec/factories/quota.rb new file mode 100644 index 0000000..fd0f514 --- /dev/null +++ b/src/spec/factories/quota.rb @@ -0,0 +1,8 @@ +Factory.define :quota do |f| + f.association :pool + f.maximum_running_instances 10 + f.maximum_running_memory 102400 + f.maximum_running_cpus 20 + f.maximum_total_instances 15 + f.maximum_total_storage 8500 +end \ No newline at end of file diff --git a/src/spec/models/instance_observer_spec.rb b/src/spec/models/instance_observer_spec.rb index 9e3dcc7..76514e5 100644 --- a/src/spec/models/instance_observer_spec.rb +++ b/src/spec/models/instance_observer_spec.rb @@ -86,4 +86,23 @@ describe InstanceObserver do @instance.acc_stopped_time.should >= 1 @instance.acc_stopped_time.should <= 2 end + + it "should update a pools quota when an associated instance state goes to running" do + hwp = Factory :mock_hwp1 + pool = Factory :pool + quota = Factory(:quota, :pool_id => pool.id) + instance = Factory(:new_instance, :pool => pool, :hardware_profile => hwp) + + instance.state = Instance::STATE_RUNNING + instance.save + + quota = Quota.find_by_id(quota.id) + quota.running_instances.should == 1 + quota.running_memory.should == hwp.memory + #TODO test for cpus + + quota.total_storage.should == hwp.storage + quota.total_instances.should == 1 + end + end \ No newline at end of file
From: martyntaylor mtaylor@redhat.com
--- src/app/controllers/pool_controller.rb | 7 +++ src/app/views/pool/quota.html.erb | 55 +++++++++++++++++++++++++++ src/app/views/pool/show.html.erb | 1 + src/features/pool.feature | 27 +++++++++++++ src/features/step_definitions/pool_steps.rb | 10 +++++ src/spec/factories/quota.rb | 2 +- 6 files changed, 101 insertions(+), 1 deletions(-) create mode 100644 src/app/views/pool/quota.html.erb
diff --git a/src/app/controllers/pool_controller.rb b/src/app/controllers/pool_controller.rb index d557d57..d5fc8af 100644 --- a/src/app/controllers/pool_controller.rb +++ b/src/app/controllers/pool_controller.rb @@ -140,4 +140,11 @@ class PoolController < ApplicationController end redirect_to :action => 'show', :id => @pool.id end + + def quota + @pool = Pool.find(params[:id]) + puts @pool.id + @quota = Quota.find(:first, :conditions => { :pool_id => @pool }) + end + end diff --git a/src/app/views/pool/quota.html.erb b/src/app/views/pool/quota.html.erb new file mode 100644 index 0000000..edd4463 --- /dev/null +++ b/src/app/views/pool/quota.html.erb @@ -0,0 +1,55 @@ +<% if @quota == nil %> +<h1>There is no quota associated with this pool</h1> +<% else %> + <table> + <thead> + <tr> + <th scope="col">Resource</th> + <th scope="col">Max Capacity</th> + <th scope="col">Used Capacity</th> + <th scope="col">Free Capacity</th> + <th scope="col">% Used</th> + </tr> + </thead> + <tbody> + <tr> + <td>Running Instances</td> + <td><%= @quota.maximum_running_instances %></td> + <td><%= @quota.running_instances %></td> + <td><%= @quota.maximum_running_instances - @quota.running_instances %></td> + <td><%= percentage = (100.to_f / @quota.maximum_running_instances) * @quota.running_instances + sprintf("%.2f", percentage) %></td> + </tr> + <tr> + <td>Running Memory</td> + <td><%= @quota.maximum_running_memory %></td> + <td><%= @quota.running_memory %></td> + <td><%= @quota.maximum_running_memory - @quota.running_memory %></td> + <td><%= percentage = (100.to_f / @quota.maximum_running_memory) * @quota.running_memory + sprintf("%.2f", percentage) %></td> + <td></td> + </tr> + <tr> + <td>Running CPUs</td> + <td><%= @quota.maximum_running_cpus %></td> + <td><%= @quota.running_cpus %></td> + <td><%= @quota.maximum_running_cpus - @quota.running_cpus %></td> + <td><%= sprintf("%.2f", (100.to_f / @quota.maximum_running_cpus) * @quota.running_cpus) %></td> + </tr> + <tr> + <td>Total Instances</td> + <td><%= @quota.maximum_total_instances %></td> + <td><%= @quota.total_instances %></td> + <td><%= @quota.maximum_total_instances - @quota.total_instances %></td> + <td><%= sprintf("%.2f", (100.to_f / @quota.maximum_total_instances) * @quota.total_instances) %></td> + </tr> + <tr> + <td>Total Storage</td> + <td><%= @quota.maximum_total_storage %></td> + <td><%= @quota.total_storage %></td> + <td><%= @quota.maximum_total_storage - @quota.total_storage %></td> + <td><%= sprintf("%.2f", (100.to_f / @quota.maximum_total_storage) * @quota.total_storage) %></td> + </tr> + </tbody> + </table> +<% end %> \ No newline at end of file diff --git a/src/app/views/pool/show.html.erb b/src/app/views/pool/show.html.erb index 1230c64..2b46781 100644 --- a/src/app/views/pool/show.html.erb +++ b/src/app/views/pool/show.html.erb @@ -39,3 +39,4 @@ <%= link_to "User access", {:controller => "permissions", :action => "list", :pool_id => @pool.id}, :class=>"actionlink" if has_view_perms? %> <%= link_to "Hardware Profiles", {:action => "hardware_profiles", :id => @pool.id}, :class=>"actionlink"%> <%= link_to "Realms", {:action => "realms", :id => @pool.id}, :class=>"actionlink"%> +<%= link_to "Quota", {:action => "quota", :id => @pool}, :class=>"actionlink"%> diff --git a/src/features/pool.feature b/src/features/pool.feature index 5d8f219..7308dd4 100644 --- a/src/features/pool.feature +++ b/src/features/pool.feature @@ -45,3 +45,30 @@ Feature: Manage Pools When I follow "Realms" Then I should see "Europe" And I should see "United States" + + @tag + Scenario: View Pool's Quota Usage + Given I own a pool named "mockpool" + And the Pool has a quota with following capacities: + | resource | capacity | + | maximum_running_instances | 10 | + | maximum_running_memory | 10240 | + | maximum_running_cpus | 20 | + | maximum_total_instances | 15 | + | maximum_total_storage | 8500 | + | running_instances | 8 | + | running_memory | 9240 | + | running_cpus | 16 | + | total_instances | 15 | + | total_storage | 8400 | + And I am on the homepage + When I follow "mockpool" + Then I should be on the show pool page + When I follow "Quota" + Then I should see the following: + | Resource | Max Capacity | Used Capacity | Free Capacity | % Used | + | Running Instances | 10 | 8 | 2 | 80.00 | + | Running Memory | 10240 | 9240 | 1000 | 90.23 | + | Running CPUs | 20 | 16 | 4 | 80.00 | + | Total Instances | 15 | 15 | 0 | 100.00 | + | Total Storage | 8500 | 8400 | 100 | 98.82 | \ No newline at end of file diff --git a/src/features/step_definitions/pool_steps.rb b/src/features/step_definitions/pool_steps.rb index 3d97da6..3be5808 100644 --- a/src/features/step_definitions/pool_steps.rb +++ b/src/features/step_definitions/pool_steps.rb @@ -48,3 +48,13 @@ Then /^I should see the following:$/ do |table| end end end + +Given /^the Pool has a quota with following capacities:$/ do |table| + quota_hash = { "pool_id" => @pool, "pool" => @pool} + table.hashes.each do |hash| + quota_hash[hash["resource"]] = hash["capacity"] + end + + @quota = Factory(:quota, quota_hash) + puts @quota.pool_id +end \ No newline at end of file diff --git a/src/spec/factories/quota.rb b/src/spec/factories/quota.rb index fd0f514..ce29b9f 100644 --- a/src/spec/factories/quota.rb +++ b/src/spec/factories/quota.rb @@ -1,7 +1,7 @@ Factory.define :quota do |f| f.association :pool f.maximum_running_instances 10 - f.maximum_running_memory 102400 + f.maximum_running_memory 10240 f.maximum_running_cpus 20 f.maximum_total_instances 15 f.maximum_total_storage 8500
From: martyntaylor mtaylor@redhat.com
--- src/app/controllers/pool_controller.rb | 30 +++++- src/app/controllers/pool_controller.rb~ | 138 ++++++++++++++++++++++++ src/app/models/instance_observer.rb | 9 +- src/app/views/pool/new_quota.html.erb | 18 +++ src/app/views/pool/quota.html.erb | 3 +- src/app/views/pool/show.html.erb~ | 42 +++++++ src/config/environment.rb | 2 +- src/db/migrate/20090802000000_create_quotas.rb | 6 - src/spec/models/instance_observer_spec.rb | 60 ++++++++--- 9 files changed, 279 insertions(+), 29 deletions(-) create mode 100644 src/app/controllers/pool_controller.rb~ create mode 100644 src/app/views/pool/new_quota.html.erb create mode 100644 src/app/views/pool/show.html.erb~
diff --git a/src/app/controllers/pool_controller.rb b/src/app/controllers/pool_controller.rb index d5fc8af..632d0cb 100644 --- a/src/app/controllers/pool_controller.rb +++ b/src/app/controllers/pool_controller.rb @@ -143,8 +143,32 @@ class PoolController < ApplicationController
def quota @pool = Pool.find(params[:id]) - puts @pool.id - @quota = Quota.find(:first, :conditions => { :pool_id => @pool }) + if Quota.exists?(@pool.quota_id) + @quota = Quota.find(@pool.quota_id) + end + require_privilege(Privilege::QUOTA_VIEW, @pool) + end + + def new_quota + @pool = Pool.find(params[:id]) + require_privilege(Privilege::POOL_MODIFY, @pool) + end + + def create_quota + if !Quota.exists?(@pool.quota_id) + @pool = Pool.find(params[:pool][:id]) + require_privilege(Privilege::POOL_MODIFY, @pool) + + @quota = Quota.new(params[:quota]) + @quota.save! + + @pool.quota_id = @quota + @pool.save! + + redirect_to :action => 'quota', :id => @pool + else + errors.add("quota_id", "There already exists a quota for this pool") + end end
-end +end \ No newline at end of file diff --git a/src/app/controllers/pool_controller.rb~ b/src/app/controllers/pool_controller.rb~ new file mode 100644 index 0000000..137fdd5 --- /dev/null +++ b/src/app/controllers/pool_controller.rb~ @@ -0,0 +1,138 @@ +# +# Copyright (C) 2009 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. A copy of the GNU General Public License is +# also available at http://www.gnu.org/copyleft/gpl.html. + +# Filters added to this controller apply to all controllers in the application. +# Likewise, all the methods added will be available for all controllers. + +require 'util/taskomatic' + +class PoolController < ApplicationController + before_filter :require_user + + def index + render :action => 'new' + end + + def show + #FIXME: clean this up, many error cases here + @pool = Pool.find(params[:id]) + require_privilege(Privilege::INSTANCE_VIEW,@pool) + # pass nil into Taskomatic as we're not working off a task here + Taskomatic.new(nil,logger).pool_refresh(@pool) + @pool.reload + @instances = @pool.instances + end + + def hardware_profiles + @pool = Pool.find(params[:id]) + @hardware_profiles = @pool.hardware_profiles + require_privilege(Privilege::POOL_VIEW, @pool) + end + + def realms + @pool = Pool.find(params[:id]) + @realm_names = @pool.realms + require_privilege(Privilege::POOL_VIEW,@pool) + end + + def new + require_privilege(Privilege::POOL_MODIFY) + @pool = Pool.new + end + + def create + require_privilege(Privilege::POOL_MODIFY) + + #FIXME: owner is set to current user for self-service account creation, + # but in the more general case we need a way for the admin to pick + # a user + params[:pool][:owner_id] = @current_user.id + + #FIXME: This should probably be in a transaction + @pool = Pool.new(params[:pool]) + perm = Permission.new(:user => @pool.owner, + :role => Role.find_by_name("Instance Creator and User"), + :permission_object => @pool) + perm.save! + # FIXME: do we need any more handling around save failures? What if perm + # creation fails? + flash[:notice] = "Pool added." + redirect_to :action => 'show', :id => @pool.id + end + + def delete + end + + def instances_paginate + @pool = Pool.find(params[:id]) + require_privilege(Privilege::POOL_VIEW, @pool) + + # datatables sends pagination in format: + # iDisplayStart - start index + # iDisplayLength - num of recs + # => we need to count page num + page = params[:iDisplayStart].to_i / Instance::per_page + + order_col_rec = Instance::COLUMNS[params[:iSortCol_0].to_i] + order_col = Instance::COLUMNS[2] unless order_col_rec && order_col_rec[:opts][:searchable] + order = order_col[:id] + " " + (params[:sSortDir_0] == 'desc' ? 'desc' : 'asc') + + @instances = Instance.search_filter(params[:sSearch], Instance::SEARCHABLE_COLUMNS).paginate( + :page => page + 1, + :order => order, + :conditions => {:pool_id => @pool.id} + ) + + render :json => { + :sEcho => params[:sEcho], + :iTotalRecords => @instances.total_entries, + :iTotalDisplayRecords => @instances.total_entries, + :aaData => @instances.map {|i| [i.id, "", i.name, i.state, i.hardware_profile.name, i.image.name]} + } + end + + + def accounts_for_pool + @pool = Pool.find(params[:pool_id]) + require_privilege(Privilege::ACCOUNT_VIEW,@pool) + @cloud_accounts = [] + all_accounts = CloudAccount.list_for_user(@current_user, Privilege::ACCOUNT_ADD) + all_accounts.each {|account| + @cloud_accounts << account unless @pool.cloud_accounts.map{|x| x.id}.include?(account.id) + } + end + + def add_account + @pool = Pool.find(params[:pool]) + @cloud_account = CloudAccount.find(params[:cloud_account]) + require_privilege(Privilege::ACCOUNT_ADD,@pool) + require_privilege(Privilege::ACCOUNT_ADD,@cloud_account) + Pool.transaction do + @pool.cloud_accounts << @cloud_account unless @pool.cloud_accounts.map{|x| x.id}.include?(@cloud_account.id) + @pool.save! + @pool.populate_realms_and_images([@cloud_account]) + end + redirect_to :action => 'show', :id => @pool.id + end + + def quota + @pool = Pool.find(params[:id]) + puts @pool.id + @quota = Quota.find(:first, :conditions => { :pool_id => @pool }) + end +end diff --git a/src/app/models/instance_observer.rb b/src/app/models/instance_observer.rb index ab1a095..9b0461a 100644 --- a/src/app/models/instance_observer.rb +++ b/src/app/models/instance_observer.rb @@ -1,6 +1,6 @@ class InstanceObserver < ActiveRecord::Observer
- ACTIVE_STATES = [ Instance::STATE_PENDING, Instance::STATE_RUNNING, Instance::STATE_SHUTTING_DOWN ] + ACTIVE_STATES = [ Instance::STATE_NEW, Instance::STATE_PENDING, Instance::STATE_RUNNING, Instance::STATE_SHUTTING_DOWN, Instance::STATE_STOPPED ]
def before_save(an_instance) if an_instance.changed? @@ -34,7 +34,8 @@ class InstanceObserver < ActiveRecord::Observer def update_quota(state_from, state_to, an_instance)
hwp = HardwareProfile.find(an_instance.hardware_profile_id) - quota = Quota.find(:first, :conditions => { :pool_id => an_instance.pool_id }) + pool = Pool.find(an_instance.pool_id) + quota = Quota.find(pool.quota_id)
if(quota) if state_to == Instance::STATE_RUNNING @@ -43,7 +44,7 @@ class InstanceObserver < ActiveRecord::Observer quota.running_memory += hwp.memory elsif state_from == Instance::STATE_RUNNING #TODO update running cpus - quota.running_instances -- + quota.running_instances -= 1 quota.running_memory -= hwp.memory end
@@ -55,7 +56,7 @@ class InstanceObserver < ActiveRecord::Observer quota.total_instances -= 1 end
- quota.save + quota.save! end end end diff --git a/src/app/views/pool/new_quota.html.erb b/src/app/views/pool/new_quota.html.erb new file mode 100644 index 0000000..4290862 --- /dev/null +++ b/src/app/views/pool/new_quota.html.erb @@ -0,0 +1,18 @@ +<div class="dcloud_form"> + <%= error_messages_for 'pool' %> + + <h2>Create Quota for Pool: <%= @pool.name %></legend></h2><br /> + + <% form_tag :action => 'create_quota' do -%> + <%=hidden_field :pool, :id %> + <ul> + <li><label>Max Running Instances</label><%= text_field :quota, :maximum_running_instances %></li> + <li><label>Max Running Memory</label><%= text_field :quota, :maximum_running_memory %></li> + <li><label>Max Running CPUs</label><%= text_field :quota, :maximum_running_cpus %></li> + <li><label>Max Total Instances</label><%= text_field :quota, :maximum_total_instances %></li> + <li><label>Max Total Storage</label><%= text_field :quota, :maximum_total_storage %></li> + </ul> + </fieldset> + <%= submit_tag "Save", :class => "submit" %> + <% end %> +</div> \ No newline at end of file diff --git a/src/app/views/pool/quota.html.erb b/src/app/views/pool/quota.html.erb index edd4463..09b227e 100644 --- a/src/app/views/pool/quota.html.erb +++ b/src/app/views/pool/quota.html.erb @@ -1,5 +1,6 @@ -<% if @quota == nil %> +<% if !@quota %> <h1>There is no quota associated with this pool</h1> +<%= link_to "Create a Quota", {:controller => "pool", :action => "new_quota", :id => @pool}, :class=>"actionlink"%> <% else %> <table> <thead> diff --git a/src/app/views/pool/show.html.erb~ b/src/app/views/pool/show.html.erb~ new file mode 100644 index 0000000..2b46781 --- /dev/null +++ b/src/app/views/pool/show.html.erb~ @@ -0,0 +1,42 @@ +<script type="text/javascript"> + function clickRow(ev) { + var box = $('input[name="image_id[]"]', ev.target.parentNode); + if ($(ev.target).attr('name') != "image_id[]") + box.attr('checked', !box.attr('checked')); + } + + function checkAll(ev) { + $('input[name="image_id[]"]', dataTable_instances_table).attr('checked', $(ev.target).attr('checked')) + } +</script> + +<%= datatable( + Instance::COLUMNS.map {|c| c[:opts]}, + { + :table_dom_id => 'instances_table', + :per_page => Instance::per_page, + :sort_by => "[3, 'asc']", + :serverside => true, + :ajax_source => url_for(:controller => 'pool', :action => 'instances_paginate') + "/#{@pool.id}", + :append => ".fnSetFilteringDelay()", + :persist_state => false, + :click_callback => "function(ev) {clickRow(ev);}", + } +) %> + +<table class="datatable display" id="instances_table"> + <thead> + <tr> + <% Instance::COLUMNS.each do |c| %> + <%= "<th>#{c[:header]}</th>" %> + <% end %> + </tr> + </thead> + <tbody> + </tbody> +</table> +<%= link_to "Add a new instance", {:controller => "instance", :action => "new", "instance[pool_id]" => @pool}, :class=>"actionlink"%> +<%= link_to "User access", {:controller => "permissions", :action => "list", :pool_id => @pool.id}, :class=>"actionlink" if has_view_perms? %> +<%= link_to "Hardware Profiles", {:action => "hardware_profiles", :id => @pool.id}, :class=>"actionlink"%> +<%= link_to "Realms", {:action => "realms", :id => @pool.id}, :class=>"actionlink"%> +<%= link_to "Quota", {:action => "quota", :id => @pool}, :class=>"actionlink"%> diff --git a/src/config/environment.rb b/src/config/environment.rb index 1721126..9f0fa4f 100644 --- a/src/config/environment.rb +++ b/src/config/environment.rb @@ -43,7 +43,7 @@ Rails::Initializer.run do |config| config.gem "gettext", :lib => "gettext_rails" config.gem "gettext", :lib => "gettext_activerecord" config.gem "authlogic" - config.gem "deltacloud-client", :lib => "deltacloud" + config.gem "deltacloud-cliesrc/config/environment.rbnt", :lib => "deltacloud" config.gem "haml" config.gem "will_paginate"
diff --git a/src/db/migrate/20090802000000_create_quotas.rb b/src/db/migrate/20090802000000_create_quotas.rb index 29cc5bf..01c885d 100644 --- a/src/db/migrate/20090802000000_create_quotas.rb +++ b/src/db/migrate/20090802000000_create_quotas.rb @@ -20,12 +20,6 @@ class CreateQuotas < ActiveRecord::Migration def self.up create_table :quotas do |t| - t.integer :running_cpus - t.integer :running_memory - t.integer :running_instances - t.integer :total_storage - t.integer :total_instances - t.integer :pool_id t.integer :running_cpus, :default => 0 t.integer :running_memory, :default => 0 t.integer :running_instances, :default => 0 diff --git a/src/spec/models/instance_observer_spec.rb b/src/spec/models/instance_observer_spec.rb index 76514e5..7051955 100644 --- a/src/spec/models/instance_observer_spec.rb +++ b/src/spec/models/instance_observer_spec.rb @@ -4,7 +4,10 @@ describe InstanceObserver do
before(:each) do @timestamp = Time.now - @instance = Factory :new_instance + @hwp = Factory :mock_hwp1 + @pool = Factory :pool + @quota = Factory(:quota, :pool_id => @pool.id) + @instance = Factory(:new_instance, :pool => @pool, :hardware_profile => @hwp) end
it "should set started at timestamp when instance goes to state pending" do @@ -55,7 +58,7 @@ describe InstanceObserver do sleep(1)
@instance.state = Instance::STATE_SHUTTING_DOWN - @instance.save + @instance.save!
@instance.acc_running_time.should >= 1 @instance.acc_running_time.should <= 2 @@ -87,22 +90,51 @@ describe InstanceObserver do @instance.acc_stopped_time.should <= 2 end
- it "should update a pools quota when an associated instance state goes to running" do - hwp = Factory :mock_hwp1 - pool = Factory :pool - quota = Factory(:quota, :pool_id => pool.id) - instance = Factory(:new_instance, :pool => pool, :hardware_profile => hwp) + it "should update quota when a new pool instance is created" do + @quota = Quota.find(@quota) + + @quota.total_instances.should == 1 + @quota.total_storage.should == @hwp.storage + end + + it "should update quota when a pool instance goes into an inactive state" do + @quota = Quota.find(@quota) + + @instance.state = Instance::STATE_CREATE_FAILED + @quota.total_instances.should == 0 + @quota.total_storage.should == 0 + end + + it "should update a pools quota when an pool instance state goes to running" do + @quota = Quota.find(@quota) + + @instance.state = Instance::STATE_RUNNING + @instance.save!
- instance.state = Instance::STATE_RUNNING - instance.save + @quota = Quota.find(@quota.id) + @quota.running_instances.should == 1 + @quota.running_memory.should == @hwp.memory + #TODO test for cpus + + @quota.total_storage.should == @hwp.storage + @quota.total_instances.should == 1 + end + + it "should update a pools quota when an associated instance state goes from running another active state" do + @quota = Quota.find(@quota) + + @instance.state = Instance::STATE_RUNNING + @instance.save! + + @instance.state = Instance::STATE_SHUTTING_DOWN + @instance.save!
- quota = Quota.find_by_id(quota.id) - quota.running_instances.should == 1 - quota.running_memory.should == hwp.memory #TODO test for cpus + @quota.running_instances.should == 0 + @quota.running_memory.should == 0
- quota.total_storage.should == hwp.storage - quota.total_instances.should == 1 + @quota.total_storage.should == @hwp.storage + @quota.total_instances.should == 1 end
end \ No newline at end of file
From: martyntaylor mtaylor@redhat.com
--- src/app/controllers/pool_controller.rb | 39 +++++++++++++++++++++++++++++-- src/app/views/pool/quota.html.erb | 2 + src/config/environment.rb | 3 +- 3 files changed, 40 insertions(+), 4 deletions(-)
diff --git a/src/app/controllers/pool_controller.rb b/src/app/controllers/pool_controller.rb index 632d0cb..204eb80 100644 --- a/src/app/controllers/pool_controller.rb +++ b/src/app/controllers/pool_controller.rb @@ -151,13 +151,15 @@ class PoolController < ApplicationController
def new_quota @pool = Pool.find(params[:id]) - require_privilege(Privilege::POOL_MODIFY, @pool) + require_privilege(Privilege::QUOTA_MODIFY, @pool) end
def create_quota + #TODO Should be in Transaction + @pool = Pool.find(params[:pool][:id]) + if !Quota.exists?(@pool.quota_id) - @pool = Pool.find(params[:pool][:id]) - require_privilege(Privilege::POOL_MODIFY, @pool) + require_privilege(Privilege::QUOTA_MODIFY, @pool)
@quota = Quota.new(params[:quota]) @quota.save! @@ -165,10 +167,41 @@ class PoolController < ApplicationController @pool.quota_id = @quota @pool.save!
+ flash[:notice] = "Quota Created!" redirect_to :action => 'quota', :id => @pool else errors.add("quota_id", "There already exists a quota for this pool") + render :action => :new_quota + end + end + + def edit_quota + @pool = Pool.find(params[:id]) + @quota = Quota.find(@pool.quota_id) + require_privilege(Privilege::QUOTA_MODIFY, @pool) + end + + def update_quota + @pool = Pool.find(params[:pool][:id]) + require_privilege(Privilege::QUOTA_MODIFY, @pool) + + #TODO Should check to make sure that the new quota values greater than the current usage + @quota = Quota.find(@pool.quota_id) + if @quota.update_attributes(params[:quota]) + flash[:notice] = "Quota updated!" + redirect_to :controller => 'pool', :action => 'quota', :id => @pool + else + render :action => :edit_quota end end
+ def delete_quota + @pool = Pool.find(params[:id]) + @quota = Quota.find(@pool.quota_id) + require_privilege(Privilege::QUOTA_MODIFY, @pool) + if @quota.delete + flash[:notice] = "Quota Deleted!" + redirect_to :controller => 'pool', :action => 'quota', :id => @pool + end + end end \ No newline at end of file diff --git a/src/app/views/pool/quota.html.erb b/src/app/views/pool/quota.html.erb index 09b227e..8320fd1 100644 --- a/src/app/views/pool/quota.html.erb +++ b/src/app/views/pool/quota.html.erb @@ -53,4 +53,6 @@ </tr> </tbody> </table> + <%= link_to "Edit", {:controller => "pool", :action => "edit_quota", :id => @pool}, :class=>"actionlink"%> + <%= link_to "Delete", {:controller => "pool", :action => "delete_quota", :id => @pool}, :class=>"actionlink"%> <% end %> \ No newline at end of file diff --git a/src/config/environment.rb b/src/config/environment.rb index 9f0fa4f..eefd3a4 100644 --- a/src/config/environment.rb +++ b/src/config/environment.rb @@ -43,7 +43,7 @@ Rails::Initializer.run do |config| config.gem "gettext", :lib => "gettext_rails" config.gem "gettext", :lib => "gettext_activerecord" config.gem "authlogic" - config.gem "deltacloud-cliesrc/config/environment.rbnt", :lib => "deltacloud" + config.gem "deltacloud-client", :lib => "deltacloud" config.gem "haml" config.gem "will_paginate"
@@ -77,4 +77,5 @@ Rails::Initializer.run do |config| # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')] # config.i18n.default_locale = :de
+ end
From: martyntaylor mtaylor@redhat.com
--- src/app/controllers/pool_controller.rb | 1 - src/app/models/quota.rb | 32 +++++++++++++++++++++++++++++ src/spec/factories/quota.rb | 1 - src/spec/models/instance_observer_spec.rb | 16 +++++++------- src/spec/models/instance_spec.rb | 4 ++- src/spec/models/quota_spec.rb | 31 ++++++++++++++++++++++++++++ 6 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 src/spec/models/quota_spec.rb
diff --git a/src/app/controllers/pool_controller.rb b/src/app/controllers/pool_controller.rb index 204eb80..3bb43e2 100644 --- a/src/app/controllers/pool_controller.rb +++ b/src/app/controllers/pool_controller.rb @@ -185,7 +185,6 @@ class PoolController < ApplicationController @pool = Pool.find(params[:pool][:id]) require_privilege(Privilege::QUOTA_MODIFY, @pool)
- #TODO Should check to make sure that the new quota values greater than the current usage @quota = Quota.find(@pool.quota_id) if @quota.update_attributes(params[:quota]) flash[:notice] = "Quota updated!" diff --git a/src/app/models/quota.rb b/src/app/models/quota.rb index c772c0b..5e16065 100644 --- a/src/app/models/quota.rb +++ b/src/app/models/quota.rb @@ -30,4 +30,36 @@ class Quota < ActiveRecord::Base validates_presence_of :maximum_total_storage validates_presence_of :maximum_total_instances
+ validates_numericality_of :maximum_running_instances + validates_numericality_of :maximum_running_memory + validates_numericality_of :maximum_running_cpus + + validates_numericality_of :maximum_total_storage + validates_numericality_of :maximum_total_instances + + def can_create_instance?(instance) + hwp = HardwareProfile.find(instance.hardware_profile_id) + + if maximum_total_instances >= (total_instances + 1) && maximum_total_storage >= (total_storage + hwp.storage) + return true + end + return false + end + + def can_start_instance?(instance) + hwp = HardwareProfile.find(instance.hardware_profile_id) + # TODO Add check for CPUs + if maximum_running_instances >= (running_instances + 1) && maximum_running_memory >= (running_memory + hwp.memory) + return true + end + return false + end + + def validate + errors.add("maximum_running_instances", "cannot be less than the current running instances") if maximum_running_instances < running_instances + errors.add("maximum_running_memory", "cannot be less than the current running memory") if maximum_running_memory < running_memory + errors.add("maximum_running_cpus", "cannot be less than the current running CPUs") if maximum_running_cpus < running_cpus + errors.add("maximum_total_storage", "cannot be less than the current total storage") if maximum_total_storage < total_storage + errors.add("maximum_total_instances", "cannot be less than the current total instances") if maximum_total_instances < total_instances + end end diff --git a/src/spec/factories/quota.rb b/src/spec/factories/quota.rb index ce29b9f..a35f7ac 100644 --- a/src/spec/factories/quota.rb +++ b/src/spec/factories/quota.rb @@ -1,5 +1,4 @@ Factory.define :quota do |f| - f.association :pool f.maximum_running_instances 10 f.maximum_running_memory 10240 f.maximum_running_cpus 20 diff --git a/src/spec/models/instance_observer_spec.rb b/src/spec/models/instance_observer_spec.rb index 7051955..5b36377 100644 --- a/src/spec/models/instance_observer_spec.rb +++ b/src/spec/models/instance_observer_spec.rb @@ -5,8 +5,8 @@ describe InstanceObserver do before(:each) do @timestamp = Time.now @hwp = Factory :mock_hwp1 - @pool = Factory :pool - @quota = Factory(:quota, :pool_id => @pool.id) + @quota = Factory :quota + @pool = Factory(:pool, :quota_id => @quota.id) @instance = Factory(:new_instance, :pool => @pool, :hardware_profile => @hwp) end
@@ -98,16 +98,16 @@ describe InstanceObserver do end
it "should update quota when a pool instance goes into an inactive state" do + @instance.state = Instance::STATE_CREATE_FAILED + @instance.save! + @quota = Quota.find(@quota)
- @instance.state = Instance::STATE_CREATE_FAILED @quota.total_instances.should == 0 @quota.total_storage.should == 0 end
it "should update a pools quota when an pool instance state goes to running" do - @quota = Quota.find(@quota) - @instance.state = Instance::STATE_RUNNING @instance.save!
@@ -120,15 +120,15 @@ describe InstanceObserver do @quota.total_instances.should == 1 end
- it "should update a pools quota when an associated instance state goes from running another active state" do - @quota = Quota.find(@quota) - + it "should update a pools quota when an associated instance state goes from running to another active state" do @instance.state = Instance::STATE_RUNNING @instance.save!
@instance.state = Instance::STATE_SHUTTING_DOWN @instance.save!
+ @quota = Quota.find(@quota.id) + #TODO test for cpus @quota.running_instances.should == 0 @quota.running_memory.should == 0 diff --git a/src/spec/models/instance_spec.rb b/src/spec/models/instance_spec.rb index 0eaf837..15355a2 100644 --- a/src/spec/models/instance_spec.rb +++ b/src/spec/models/instance_spec.rb @@ -2,7 +2,9 @@ require 'spec_helper'
describe Instance do before(:each) do - @instance = Factory.build(:instance) + @quota = Factory :quota + @pool = Factory(:pool, :quota_id => @quota.id) + @instance = Factory.build(:instance, :pool_id => @pool.id) @actions = ['start', 'stop'] end
diff --git a/src/spec/models/quota_spec.rb b/src/spec/models/quota_spec.rb new file mode 100644 index 0000000..1596582 --- /dev/null +++ b/src/spec/models/quota_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Quota do + before(:each) do + @quota = Factory :quota + end + + it "should not allow quota limits to be changed to lower than current usage" do + @quota.running_instances = @quota.maximum_running_instances + @quota.maximum_running_instances -= 1 + + @quota.running_memory = @quota.maximum_running_memory + @quota.maximum_running_memory -= 1 + + @quota.running_cpus = @quota.maximum_running_cpus + @quota.maximum_running_cpus -= 1 + + @quota.total_instances = @quota.maximum_total_instances + @quota.maximum_total_instances -= 1 + + @quota.total_storage = @quota.total_storage + @quota.maximum_total_storage -= 1 + + @quota.save.should be_false + + @quota.errors.empty?.should be_false + @quota.errors.find_all {|attr,msg| ["maximum_running_instances", "maximum_running_memory", "maximum_running_cpus", "maximum_total_storage", + "maximum_total_instances"].include?(attr).should be_true } + end + +end \ No newline at end of file
From: martyntaylor mtaylor@redhat.com
--- src/app/controllers/pool_controller.rb | 2 +- src/app/models/task.rb | 4 +++- src/app/util/taskomatic.rb | 28 ++++++++++++++++++++++++++-- src/app/views/pool/new_quota.html.erb | 1 + src/spec/factories/quota.rb | 8 ++++++++ 5 files changed, 39 insertions(+), 4 deletions(-)
diff --git a/src/app/controllers/pool_controller.rb b/src/app/controllers/pool_controller.rb index 3bb43e2..7ff39b7 100644 --- a/src/app/controllers/pool_controller.rb +++ b/src/app/controllers/pool_controller.rb @@ -164,7 +164,7 @@ class PoolController < ApplicationController @quota = Quota.new(params[:quota]) @quota.save!
- @pool.quota_id = @quota + @pool.quota_id = @quota.id @pool.save!
flash[:notice] = "Quota Created!" diff --git a/src/app/models/task.rb b/src/app/models/task.rb index f3aaa63..ebbde40 100644 --- a/src/app/models/task.rb +++ b/src/app/models/task.rb @@ -43,7 +43,9 @@ class Task < ActiveRecord::Base FAILURE_PROVIDER_CONTACT_FAILED = "provider_contact_failed" FAILURE_PROVIDER_RETURNED_FAILED = "provider_returned_failed"
- FAILURE_CODES = [FAILURE_PROVIDER_NOT_FOUND, FAILURE_PROVIDER_CONTACT_FAILED, FAILURE_PROVIDER_RETURNED_FAILED] + FAILURE_OVER_POOL_QUOTA = "exceeded_pool_quota" + + FAILURE_CODES = [FAILURE_PROVIDER_NOT_FOUND, FAILURE_PROVIDER_CONTACT_FAILED, FAILURE_PROVIDER_RETURNED_FAILED, FAILURE_OVER_POOL_QUOTA]
validates_inclusion_of :failure_code, :in => FAILURE_CODES + [nil] diff --git a/src/app/util/taskomatic.rb b/src/app/util/taskomatic.rb index 74e069c..ccef5c7 100644 --- a/src/app/util/taskomatic.rb +++ b/src/app/util/taskomatic.rb @@ -45,6 +45,20 @@ class Taskomatic
def instance_create
+ pool = Pool.find(@task.instance.pool_id) + if Quota.exists?(pool.quota_id) + if !Quota.find(pool.quota_id).can_create_instance?(@task.instance) + @task.state = Task::STATE_FAILED + @task.failure_code = Task::FAILURE_OVER_POOL_QUOTA + @task.instance.state = Instance::STATE_CREATE_FAILED + + @task.save! + @task.instance.save! + + return nil + end + end + begin client = @task.instance.cloud_account.connect realm = @task.instance.realm.external_key rescue nil @@ -112,7 +126,16 @@ class Taskomatic end
def instance_start - do_action(:start!) + pool = Pool.find(@task.instance.pool_id) + if Quota.exists?(pool.quota_id) + if !Quota.find(pool.quota_id).can_create_instance?(@task.instance) + do_action(:start!) + return nil + end + end + + @task.state = Task::STATE_FAILED + @task.failure_code = Task::FAILURE_OVER_POOL_QUOTA end
def instance_stop @@ -163,4 +186,5 @@ class Taskomatic end end end -end \ No newline at end of file + +end diff --git a/src/app/views/pool/new_quota.html.erb b/src/app/views/pool/new_quota.html.erb index 4290862..87dc02d 100644 --- a/src/app/views/pool/new_quota.html.erb +++ b/src/app/views/pool/new_quota.html.erb @@ -1,5 +1,6 @@ <div class="dcloud_form"> <%= error_messages_for 'pool' %> + <%= error_messages_for 'quota' %>
<h2>Create Quota for Pool: <%= @pool.name %></legend></h2><br />
diff --git a/src/spec/factories/quota.rb b/src/spec/factories/quota.rb index a35f7ac..4a7ead8 100644 --- a/src/spec/factories/quota.rb +++ b/src/spec/factories/quota.rb @@ -4,4 +4,12 @@ Factory.define :quota do |f| f.maximum_running_cpus 20 f.maximum_total_instances 15 f.maximum_total_storage 8500 +end + +Factory.define :full_quota, :parent => :quota do |f| + f.running_instances 10 + f.running_memory 10240 + f.running_cpus 20 + f.total_instances 15 + f.total_storage 8500 end \ No newline at end of file
From: martyntaylor mtaylor@redhat.com
--- src/app/controllers/pool_controller.rb | 30 +++++++++++++++++++++++------- src/app/models/instance_observer.rb | 4 ++-- src/app/models/quota.rb | 1 + src/app/models/task.rb | 3 ++- src/spec/models/task_spec.rb | 5 ----- 5 files changed, 28 insertions(+), 15 deletions(-)
diff --git a/src/app/controllers/pool_controller.rb b/src/app/controllers/pool_controller.rb index 7ff39b7..75e2bcd 100644 --- a/src/app/controllers/pool_controller.rb +++ b/src/app/controllers/pool_controller.rb @@ -160,15 +160,31 @@ class PoolController < ApplicationController
if !Quota.exists?(@pool.quota_id) require_privilege(Privilege::QUOTA_MODIFY, @pool) - @quota = Quota.new(params[:quota]) - @quota.save! - - @pool.quota_id = @quota.id - @pool.save!
- flash[:notice] = "Quota Created!" - redirect_to :action => 'quota', :id => @pool + # Populate Current Pool totals to Quota + @pool.instances.each do |instance| + hwp = HardwareProfile.find(instance.hardware_profile_id) + if instance.state == Instance::STATE_RUNNING + @quota.running_instances += 1 + @quota.running_memory += hwp.memory + #TODO Add CPUs + end + + if InstanceObserver::ACTIVE_STATES.include?(instance.state) + @quota.total_instances += 1 + @quota.total_storage += hwp.storage + end + end + + if @quota.save + flash[:notice] = "Quota Created!" + @pool.quota_id = @quota.id + @pool.save! + redirect_to :action => 'quota', :id => @pool + else + render :action => :new_quota + end else errors.add("quota_id", "There already exists a quota for this pool") render :action => :new_quota diff --git a/src/app/models/instance_observer.rb b/src/app/models/instance_observer.rb index 9b0461a..725a3a7 100644 --- a/src/app/models/instance_observer.rb +++ b/src/app/models/instance_observer.rb @@ -35,9 +35,9 @@ class InstanceObserver < ActiveRecord::Observer
hwp = HardwareProfile.find(an_instance.hardware_profile_id) pool = Pool.find(an_instance.pool_id) - quota = Quota.find(pool.quota_id)
- if(quota) + if(Quota.exists?(pool.quota_id)) + quota = Quota.find(pool.quota_id) if state_to == Instance::STATE_RUNNING #TODO update running cpus quota.running_instances += 1 diff --git a/src/app/models/quota.rb b/src/app/models/quota.rb index 5e16065..57662a7 100644 --- a/src/app/models/quota.rb +++ b/src/app/models/quota.rb @@ -37,6 +37,7 @@ class Quota < ActiveRecord::Base validates_numericality_of :maximum_total_storage validates_numericality_of :maximum_total_instances
+ def can_create_instance?(instance) hwp = HardwareProfile.find(instance.hardware_profile_id)
diff --git a/src/app/models/task.rb b/src/app/models/task.rb index ebbde40..feec26a 100644 --- a/src/app/models/task.rb +++ b/src/app/models/task.rb @@ -116,7 +116,8 @@ class Task < ActiveRecord::Base
def validate errors.add("created_at", "Task started but does not have the creation time set") if time_started and created_at.nil? - errors.add("time_started", "Task ends but does not have the start time set") if time_ended and time_started.nil? + # Removed check on time_started exisiting. if time_ended does. This can now occur, when the task fails before is starts. e.g. When Over Qutoa + #errors.add("time_started", "Task ends but does not have the start time set") if time_ended and time_started.nil? errors.add("time_ended", "Tasks ends before it's started") unless time_ended.nil? or time_started.nil? or time_ended > time_started errors.add("time_started", "Tasks starts before it's created") unless time_started.nil? or created_at.nil? or time_started > created_at end diff --git a/src/spec/models/task_spec.rb b/src/spec/models/task_spec.rb index d392cb2..c5e7188 100644 --- a/src/spec/models/task_spec.rb +++ b/src/spec/models/task_spec.rb @@ -43,11 +43,6 @@ describe Task do @task.should_not be_valid end
- it "should have 'started' time set if it ended" do - @task.attributes = @valid_attributes.except :time_started - @task.should_not be_valid - end - it "should not be valid if it started before it was created" do @task.attributes = @valid_attributes @task.time_started = @task.created_at - 1.minute
deltacloud-devel@lists.fedorahosted.org