From: martyntaylor mtaylor@redhat.com
--- src/app/models/task.rb | 4 +- src/app/services/data_service.rb | 145 -------- src/app/services/data_service_active_record.rb | 269 ++++++++++++++ src/spec/factories/pool.rb | 2 +- src/spec/factories/user.rb | 5 + .../services/data_service_active_record_spec.rb | 370 ++++++++++++++++++++ src/spec/services/data_service_spec.rb | 182 ---------- 7 files changed, 647 insertions(+), 330 deletions(-) delete mode 100644 src/app/services/data_service.rb create mode 100644 src/app/services/data_service_active_record.rb create mode 100644 src/spec/services/data_service_active_record_spec.rb delete mode 100644 src/spec/services/data_service_spec.rb
diff --git a/src/app/models/task.rb b/src/app/models/task.rb index feec26a..8058089 100644 --- a/src/app/models/task.rb +++ b/src/app/models/task.rb @@ -118,7 +118,7 @@ class Task < ActiveRecord::Base errors.add("created_at", "Task started but does not have the creation time set") if time_started and created_at.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 + 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 end diff --git a/src/app/services/data_service.rb b/src/app/services/data_service.rb deleted file mode 100644 index 9343fb1..0000000 --- a/src/app/services/data_service.rb +++ /dev/null @@ -1,145 +0,0 @@ -class DataService - - QoSDataPoint = Struct.new(:time, :average, :max, :min) - QuotaUsagePoint = Struct.new(:used, :max) - TotalQuotaUsagePoint = Struct.new(:name, :no_instances) - - # This will return array of data points between start and end, if there is a data point where the interval start + interval end - # is greater than the end time, it will be ignored - # Example: - # start = 12.30, end = 12.32, interval = 45secs - # Intervals: 12.30.00 - 12.30.45, 12.30.45 - 12.31.30 will be returned. Interval 12.31.30 - 12.32.15 will not - def self.qos_task_submission_stats(start_time, end_time, interval_length, parent, action) - - instances = [] - - if parent.class == Provider - cloud_accounts = CloudAccount.find(:all, :conditions => {:provider_id => parent.id}) - cloud_accounts.each do |cloud_account| - instances.concat(instances) - end - elsif parent.class == Pool || parent.class == CloudAccount - instances = parent.instances - else - return nil - end - - return calculate_qos_task_submission_stats(start_time, end_time, interval_length, instances, action) - end - - def self.tasks_submissions_mean_max_min(time, tasks) - - first_pass = true - - total_time = nil - maximum_time = nil - minimum_time = nil - - tasks.each do |task| - - if(first_pass == true) - total_time = task.submission_time - maximum_time = task.submission_time - minimum_time = task.submission_time - first_pass = false - else - total_time += task.submission_time - - if task.submission_time > maximum_time - maximum_time = task.submission_time - end - - if task.submission_time< minimum_time - minimum_time = task.submission_time - end - end - - end - average_time = total_time / tasks.length - - return QoSDataPoint.new(time, average_time, maximum_time, minimum_time) - end - - # Returns the Used and Maximum Resource Usage - def self.quota_utilisation(parent, resource_name) - quota = parent.quota - - case resource_name - when Quota::RESOURCE_RUNNING_INSTANCES - return QuotaUsagePoint.new(quota.running_instances, quota.maximum_running_instances) - when Quota::RESOURCE_RUNNING_MEMORY - return QuotaUsagePoint.new(quota.running_memory.to_f, quota.maximum_running_memory.to_f) - when Quota::RESOURCE_RUNNING_CPUS - return QuotaUsagePoint.new(quota.running_cpus.to_f, quota.maximum_running_cpus.to_f) - when Quota::RESOURCE_TOTAL_INSTANCES - return QuotaUsagePoint.new(quota.total_instances, quota.maximum_total_instances) - when Quota::RESOURCE_TOTAL_STORAGE - return QuotaUsagePoint.new(quota.total_storage.to_f, quota.maximum_total_storage.to_f) - when Quota::RESOURCE_OVERALL - return self.overall_usage(parent) - else - return nil - end - end - - def self.overall_usage(parent) - usage_points = [] - Quota::RESOURCE_NAMES.each do |resource_name| - usage_points << quota_utilisation(parent, resource_name) - end - - worst_case = nil - usage_points.each do |usage_point| - if worst_case - if ((100 / worst_case.max) * worst_case.used) < ((100 / usage_point.max) * usage_point.used) - worst_case = usage_point - end - else - worst_case = usage_point - end - end - return worst_case - end - - def self.total_quota_utilisation(provider) - data_points = [] - free_instances = 0 - - cloud_accounts = CloudAccount.find(:all, :conditions => {:provider_id => provider.id}) - cloud_accounts.each do |cloud_account| - quota = cloud_account.quota - if quota - data_points << TotalQuotaUsagePoint.new(cloud_account.username, quota.total_instances) - free_instances += (quota.maximum_total_instances - quota.total_instances) - end - end - data_points << TotalQuotaUsagePoint.new("free", free_instances) - return data_points - end - - private - def self.calculate_qos_task_submission_stats(start_time, end_time, interval_length, instances, action) - - data = [] - until start_time > (end_time - interval_length) do - interval_time = start_time + interval_length - - tasks = Task.find(:all, :conditions => { :time_submitted => start_time..interval_time, - :time_started => start_time..Time.now, - :failure_code => nil, - :action => action, - :task_target_id => instances - }) - if tasks.length > 0 - data << tasks_submissions_mean_max_min(start_time, tasks) - else - data << QoSDataPoint.new(start_time, 0, 0, 0) - end - - start_time = interval_time - end - - return data - end - -end diff --git a/src/app/services/data_service_active_record.rb b/src/app/services/data_service_active_record.rb new file mode 100644 index 0000000..86511a1 --- /dev/null +++ b/src/app/services/data_service_active_record.rb @@ -0,0 +1,269 @@ +class DataServiceActiveRecord + + # Structures for holding graph data + QoSDataPoint = Struct.new(:time, :average, :max, :min) + + QuotaUsagePoint = Struct.new(:used, :max) + + TotalQuotaUsagePoint = Struct.new(:name, :no_instances) + + QoSFailureRatePoint = Struct.new(:time, :failure_rate) + + def self.qos_task_submission_stats(parent, start_time, end_time, interval_length, action) + return qos_time_stats(parent, start_time, end_time, interval_length, {:action => action}, TASK_SUBMISSION_TIMES) + end + + def self.qos_task_completion_stats(parent, start_time, end_time, interval_length, action) + return qos_time_stats(parent, start_time, end_time, interval_length, {:action => action}, TASK_COMPLETION_TIMES) + end + + def self.qos_task_submission_mean_max_min(parent, start_time, end_time, action) + return qos_times_mean_max_min(parent, start_time, end_time, action, TASK_SUBMISSION_TIMES) + end + + def self.qos_task_completion_mean_max_min(parent, start_time, end_time, action) + return qos_times_mean_max_min(parent, start_time, end_time, action, TASK_COMPLETION_TIMES) + end + + def self.qos_instance_runtime_stats(parent, start_time, end_time, interval_length) + return qos_time_stats(parent, start_time, end_time, interval_length, nil, INSTANCE_RUN_TIMES) + end + + def self.qos_instance_runtime_mean_max_min(parent, start_time, end_time) + return qos_times_mean_max_min(parent, start_time, end_time, nil, INSTANCE_RUN_TIMES) + end + + def self.qos_failure_rate(parent, start_time, end_time, failure_code) + return failure_rate(parent, start_time, end_time, failure_code) + end + + def self.qos_failure_rate_stats(parent, start_time, end_time, interval_length, failure_code) + qos_time_stats(parent, start_time, end_time, interval_length, {:failure_code => failure_code}, FAILURE_RATE) + end + + # Returns the Used and Maximum Resource Usage + def self.quota_usage(parent, resource_name) + if parent + quota = parent.quota + if quota + case resource_name + when Quota::RESOURCE_RUNNING_INSTANCES + return QuotaUsagePoint.new(quota.running_instances, quota.maximum_running_instances) + when Quota::RESOURCE_RUNNING_MEMORY + return QuotaUsagePoint.new(quota.running_memory.to_f, quota.maximum_running_memory.to_f) + when Quota::RESOURCE_RUNNING_CPUS + return QuotaUsagePoint.new(quota.running_cpus.to_f, quota.maximum_running_cpus.to_f) + when Quota::RESOURCE_TOTAL_INSTANCES + return QuotaUsagePoint.new(quota.total_instances, quota.maximum_total_instances) + when Quota::RESOURCE_TOTAL_STORAGE + return QuotaUsagePoint.new(quota.total_storage.to_f, quota.maximum_total_storage.to_f) + when Quota::RESOURCE_OVERALL + return self.overall_usage(parent) + else + return nil + end + end + end + return nil + end + + def self.provider_quota_usage(provider) + data_points = [] + free_instances = 0 + + cloud_accounts = CloudAccount.find(:all, :conditions => {:provider_id => provider.id}) + cloud_accounts.each do |cloud_account| + quota = cloud_account.quota + if quota + data_points << TotalQuotaUsagePoint.new(cloud_account.username, quota.total_instances) + free_instances += (quota.maximum_total_instances - quota.total_instances) + end + end + data_points << TotalQuotaUsagePoint.new("free", free_instances) + return data_points + end + + ##################### + ## PRIVATE METHODS ## + ##################### + private + + TASK_SUBMISSION_TIMES = "TASK_SUBMISSION_TIMES" + + TASK_COMPLETION_TIMES = "TASK_COMPLETION_TIMES" + + INSTANCE_RUN_TIMES = "INSTANCE_RUN_TIMES" + + FAILURE_RATE = "FAILURE_RATE" + + def self.qos_time_stats(parent, start_time, end_time, interval_length, params, compare_field) + data = [] + until start_time > (end_time - interval_length) do + interval_time = start_time + interval_length + + case compare_field + when FAILURE_RATE + data << failure_rate(parent, start_time, interval_time, params[:failure_code]) + when INSTANCE_RUN_TIMES + data << qos_times_mean_max_min(parent, start_time, interval_time, nil, compare_field) + when TASK_COMPLETION_TIMES + data << qos_times_mean_max_min(parent, start_time, interval_time, params[:action], compare_field) + when TASK_SUBMISSION_TIMES + data << qos_times_mean_max_min(parent, start_time, interval_time, params[:action], compare_field) + end + + start_time = interval_time + end + return data + end + + # Calculates the mean, max and min times, for the tasks state time, e.g. submission, completion, etc... + def self.qos_times_mean_max_min(parent, start_time, end_time, action, compare_field) + first_pass = true + + total_time = nil + maximum_time = nil + minimum_time = nil + + case compare_field + when TASK_SUBMISSION_TIMES + list = get_compare_tasks(parent, compare_field, start_time, end_time, action) + when TASK_COMPLETION_TIMES + list = get_compare_tasks(parent, compare_field, start_time, end_time, action) + when INSTANCE_RUN_TIMES + list = get_compare_instances(parent, compare_field, start_time, end_time) + else + return nil + end + + list.each do |l| + case compare_field + when TASK_SUBMISSION_TIMES + compare_time = l.submission_time + when TASK_COMPLETION_TIMES + compare_time = l.runtime + puts compare_time + when INSTANCE_RUN_TIMES + compare_time = l.total_state_time(Instance::STATE_RUNNING) + else + return nil + end + + if(first_pass == true) + total_time = compare_time + maximum_time = compare_time + minimum_time = compare_time + first_pass = false + else + total_time += compare_time + + if compare_time > maximum_time + maximum_time = compare_time + end + + if compare_time < minimum_time + minimum_time = compare_time + end + end + end + + if total_time == nil + average_time = nil + elsif total_time == 0 + average_time = 0 + else + average_time = total_time / list.length + end + + return QoSDataPoint.new(start_time, average_time, maximum_time, minimum_time) + end + + def self.get_parent_instances(parent) + instances = [] + + if parent.class == Provider + cloud_accounts = CloudAccount.find(:all, :conditions => {:provider_id => parent.id}) + cloud_accounts.each do |cloud_account| + instances.concat(cloud_account.instances) + end + elsif parent.class == Pool || parent.class == CloudAccount + instances = parent.instances + else + return nil + end + + return instances + end + + def self.get_compare_tasks(parent, compare_field, start_time, end_time, action) + instances = get_parent_instances(parent) + case compare_field + when TASK_SUBMISSION_TIMES + return Task.find(:all, :conditions => {:time_submitted => start_time...end_time, + :time_started => start_time..Time.now, + :failure_code => nil, + :action => action, + :task_target_id => instances }) + when TASK_COMPLETION_TIMES + return Task.find(:all, :conditions => {:time_started => start_time...end_time, + :time_ended => start_time..Time.now, + :failure_code => nil, + :action => action, + :task_target_id => instances, + :state => Task::STATE_FINISHED }) + else + return nil + end + end + + # returns the failure rate of instance starts for instances associated with the parent, (pool/cloudaccount) given the failure code + def self.failure_rate(parent, start_time, end_time, failure_code) + instances = get_parent_instances(parent) + tasks = Task.find(:all, :conditions => {:created_at => start_time...end_time, + :task_target_id => instances }) + + failure_rate = 0 + if tasks.length > 0 + failed_tasks = tasks.find_all{ |task| task.failure_code == failure_code} + if failed_tasks.length > 0 + failure_rate = (100.to_f / tasks.length.to_f) * failed_tasks.length.to_f + end + end + return QoSFailureRatePoint.new(start_time, failure_rate) + end + + def self.overall_usage(parent) + usage_points = [] + Quota::RESOURCE_NAMES.each do |resource_name| + usage_points << quota_usage(parent, resource_name) + end + + worst_case = nil + usage_points.each do |usage_point| + if worst_case + if worst_case.max == Quota::NO_LIMIT + worst_case = usage_point + elsif usage_point.max == Quota::NO_LIMIT + # DO Nothing + elsif ((100 / worst_case.max) * worst_case.used) < ((100 / usage_point.max) * usage_point.used) + worst_case = usage_point + end + else + worst_case = usage_point + end + end + return worst_case + end + + def self.get_compare_instances(parent, compare_field, start_time, end_time) + instances = get_parent_instances(parent) + case compare_field + when INSTANCE_RUN_TIMES + return instances.find(:all, :conditions => {:time_last_pending => start_time...end_time, + :time_last_running => start_time..Time.now}) + else + return nil + end + end + +end diff --git a/src/spec/factories/pool.rb b/src/spec/factories/pool.rb index 4ce7c89..d85b6a7 100644 --- a/src/spec/factories/pool.rb +++ b/src/spec/factories/pool.rb @@ -1,6 +1,6 @@ Factory.define :pool do |p| p.name 'mypool' - p.owner { |owner| owner.association(:user, :login => 'pool_owner', :email => 'pool_owner@example.com') } + p.association :owner, :factory => :pool_user end
Factory.define :tpool, :parent => :pool do |p| diff --git a/src/spec/factories/user.rb b/src/spec/factories/user.rb index 0cc33c7..778aa30 100644 --- a/src/spec/factories/user.rb +++ b/src/spec/factories/user.rb @@ -15,3 +15,8 @@ end
Factory.define :provider_admin_user, :parent => :user do |u| end + +Factory.define :pool_user, :parent => :user do |u| + u.sequence(:login) { |n| "pool_user#{n}" } + u.email { |e| "#{e.login}@example.com" } +end \ No newline at end of file diff --git a/src/spec/services/data_service_active_record_spec.rb b/src/spec/services/data_service_active_record_spec.rb new file mode 100644 index 0000000..7dd6b50 --- /dev/null +++ b/src/spec/services/data_service_active_record_spec.rb @@ -0,0 +1,370 @@ +require 'spec_helper' + +describe DataServiceActiveRecord do + + it "should calculate the total instance quota usage for a provider with a number of cloud accounts" do + client = mock('DeltaCloud', :null_object => true) + provider = Factory.build(:mock_provider) + provider.stub!(:connect).and_return(client) + provider.save! + + data = [[25, 10], [40, 20], [20, 20]] + free = 0 + for i in 0..2 + cloud_account = Factory.build(:cloud_account, :provider => provider, :username => "username" + i.to_s) + cloud_account.stub!(:valid_credentials?).and_return(true) + cloud_account.save! + + quota = Factory(:quota, :maximum_total_instances => data[i][0], :total_instances => data[i][1]) + cloud_account.quota_id = quota.id + cloud_account.save! + + free += (data[i][0] - data[i][1]) + end + + data_points = DataServiceActiveRecord.provider_quota_usage(provider) + data_points[0].should == DataServiceActiveRecord::TotalQuotaUsagePoint.new("username0", data[0][1]) + data_points[1].should == DataServiceActiveRecord::TotalQuotaUsagePoint.new("username1", data[1][1]) + data_points[2].should == DataServiceActiveRecord::TotalQuotaUsagePoint.new("username2", data[2][1]) + data_points[3].should == DataServiceActiveRecord::TotalQuotaUsagePoint.new("free", free) + + end + + it "should calculate the total number of instances and maximum number of instances of a cloud account" do + client = mock('DeltaCloud', :null_object => true) + provider = Factory.build(:mock_provider) + provider.stub!(:connect).and_return(client) + provider.save! + + cloud_account = Factory.build(:cloud_account, :provider => provider) + cloud_account.stub!(:valid_credentials?).and_return(true) + cloud_account.save! + + quota = Factory(:quota, + :maximum_running_instances => 40, + :maximum_running_memory => 10240, + :maximum_running_cpus => 10, + :maximum_total_instances => 50, + :maximum_total_storage => 500, + :running_instances => 20, + :running_memory => 4096, + :running_cpus => 7, + :total_instances => 20, + :total_storage => 499) + cloud_account.quota_id = quota.id + + data_point = DataServiceActiveRecord.quota_usage(cloud_account, Quota::RESOURCE_RUNNING_INSTANCES) + data_point.should == DataServiceActiveRecord::QuotaUsagePoint.new(20, 40) + + data_point = DataServiceActiveRecord.quota_usage(cloud_account, Quota::RESOURCE_RUNNING_MEMORY) + data_point.should == DataServiceActiveRecord::QuotaUsagePoint.new(4096, 10240) + + data_point = DataServiceActiveRecord.quota_usage(cloud_account, Quota::RESOURCE_RUNNING_CPUS) + data_point.should == DataServiceActiveRecord::QuotaUsagePoint.new(7, 10) + + data_point = DataServiceActiveRecord.quota_usage(cloud_account, Quota::RESOURCE_TOTAL_INSTANCES) + data_point.should == DataServiceActiveRecord::QuotaUsagePoint.new(20, 50) + + data_point = DataServiceActiveRecord.quota_usage(cloud_account, Quota::RESOURCE_TOTAL_STORAGE) + data_point.should == DataServiceActiveRecord::QuotaUsagePoint.new(499, 500) + + data_point = DataServiceActiveRecord.quota_usage(cloud_account, Quota::RESOURCE_OVERALL) + data_point.should == DataServiceActiveRecord::QuotaUsagePoint.new(499, 500) + end + + it "should calculate the average, max and min task submission times" do + user = Factory :user + pool = Factory(:pool, :owner => user) + instance = Factory(:instance, :pool_id => pool.id) + + start_time = Time.utc(2010,"jan",1,20,15,1) + for i in 1..10 do + task = InstanceTask.new(:instance => instance, + :state => Task::STATE_PENDING, + :failure_code => nil, + :task_target_id => instance.id, + :type => "InstanceTask", + :action => InstanceTask::ACTION_CREATE) + task.save! + + task.created_at = start_time + task.time_submitted = start_time + task.time_started = start_time + i + task.save! + end + + data_point = DataServiceActiveRecord.qos_task_submission_mean_max_min(pool, start_time, Time.now, InstanceTask::ACTION_CREATE) + + data_point.average.should == 5.5 + data_point.min.should == 1 + data_point.max.should == 10 + end + + it "should create data points for the average, max and min task submission times between two times at given intervals" do + user = Factory :user + pool = Factory(:pool, :owner => user) + instance = Factory(:instance, :pool_id => pool.id) + + expected_averages = [ 20, 40, 60, 80, 100] + no_intervals = expected_averages.length + interval_length = 30 + + end_time = Time.utc(2010,"jan",1,20,15,1) + start_time = end_time - (interval_length * no_intervals) + + generate_tasks(start_time, interval_length, instance, expected_averages) + data_points = DataServiceActiveRecord.qos_task_submission_stats(pool, start_time, end_time, interval_length, InstanceTask::ACTION_CREATE) + + for i in 0...data_points.length + average_time = expected_averages[i] + dp = data_points[i] + + dp.average.should == average_time + # The multiplications could be set as static numbers but are left as calculations for easier understanding of code + dp.max.should == (average_time / 10) * 2 * 9 + dp.min.should == (average_time / 10) * 2 + end + end + + it "should create data points for mean, max and min task submission times at given intervals for a provider with multiple accounts" do + pool = Factory :pool + + expected_averages = [] + expected_averages[0] = [ 20, 40, 60, 80, 100] + expected_averages[1] = [ 40, 60, 80, 100, 120] + expected_averages[2] = [ 60, 80, 100, 120, 140] + + no_intervals = expected_averages.length + interval_length = 30 + end_time = Time.utc(2010,"jan",1,20,15,1) + start_time = end_time - (interval_length * no_intervals) + + client = mock('DeltaCloud', :null_object => true) + provider = Factory.build(:mock_provider) + provider.stub!(:connect).and_return(client) + provider.save! + + cloud_accounts = [] + expected_averages.each do |expected_average| + cloud_account = Factory.build(:cloud_account, :provider => provider, :username => "username" + expected_average[0].to_s) + cloud_account.stub!(:valid_credentials?).and_return(true) + cloud_account.save! + + instance = Factory(:instance, :cloud_account_id => cloud_account.id, :pool_id => pool.id) + generate_tasks(start_time, interval_length, instance, expected_average) + end + + data_points = DataServiceActiveRecord.qos_task_submission_stats(pool, start_time, end_time, interval_length, InstanceTask::ACTION_CREATE) + + for i in 0...data_points.length + dp = data_points[i] + dp.average.should == expected_averages[1][i] + dp.max.should == (expected_averages[2][i] / 10) * 2 * 9 + dp.min.should == (expected_averages[0][i] / 10) * 2 + end + end + + it "should create data points for mean, max, min instance runtimes" do + interval_length = 1000 + + start_time = Time.utc(2010,"jan",1,20,15,1) + start_time1 = start_time + interval_length + start_time2 = start_time1 + interval_length + start_times = [start_time, start_time1, start_time2] + + end_time = start_time2 + interval_length + + runtime1 = [5, 10, 15, 20, 25] + runtime2 = [10, 20, 30, 40, 50] + runtime3 = [100, 200, 300, 400, 500] + runtimes = [runtime1, runtime2, runtime3] + + user = Factory :pool_user + pool = Factory(:pool, :owner => user) + cloud_account = Factory :mock_cloud_account + + for i in 0..2 do + runtimes[i].each do |runtime| + instance = Factory(:instance, :pool => pool, :cloud_account => cloud_account, :state => Instance::STATE_STOPPED) + instance.save! + + instance.time_last_pending = start_times[i] + (interval_length / 2) + instance.time_last_running = start_times[i] + (interval_length / 2) + instance.acc_running_time = runtime + instance.save! + end + end + + stats = DataServiceActiveRecord.qos_instance_runtime_stats(cloud_account, start_time, end_time, interval_length) + stats[0].should == DataServiceActiveRecord::QoSDataPoint.new(start_times[0], 15, 25, 5) + stats[1].should == DataServiceActiveRecord::QoSDataPoint.new(start_times[1], 30, 50, 10) + stats[2].should == DataServiceActiveRecord::QoSDataPoint.new(start_times[2], 300, 500, 100) + + end + + it "should generate the mean max and min instance runtimes of instances for a given cloud account or pool" do + user = Factory :pool_user + pool = Factory(:pool, :owner => user) + + cloud_account = Factory :mock_cloud_account + + start_time = Time.utc(2010,"jan",1,20,15,1) + [50, 100, 150, 200, 250].each do |runtime| + instance = Factory(:new_instance, :pool => pool, :cloud_account => cloud_account) + instance.time_last_pending = start_time + instance.time_last_running = start_time + instance.acc_running_time = runtime + instance.save! + end + + expected_results = DataServiceActiveRecord::QoSDataPoint.new(start_time, 150, 250, 50) + results = DataServiceActiveRecord.qos_instance_runtime_mean_max_min(pool, start_time, Time.now) + results.should == expected_results + end + + it "should calculate the average time it takes a provider to complete a task between two times" do + user = Factory :pool_user + pool = Factory(:pool, :owner => user) + cloud_account = Factory(:mock_cloud_account) + instance = Factory(:instance, :pool => pool, :cloud_account => cloud_account) + + start_time = Time.utc(2010,"jan",1,20,15,1) + task_completion_times = [10, 20, 30, 40, 50] + total_time = 0 + + task_completion_times.each do |time| + task = InstanceTask.new(:instance => instance, + :type => "InstanceTask", + :state => Task::STATE_FINISHED, + :failure_code => nil, + :action => InstanceTask::ACTION_CREATE, + :task_target_id => instance.id) + task.save! + + task.created_at = start_time + task.time_started = start_time + task.time_ended = start_time + time + task.save! + + total_time += time + end + + expected_average_time = total_time / task_completion_times.length + average_time = DataServiceActiveRecord.qos_task_completion_mean_max_min(cloud_account.provider, start_time, Time.now, InstanceTask::ACTION_CREATE) + + average_time[:average].should == expected_average_time + average_time[:min].should == 10 + average_time[:max].should == 50 + end + + it "should calculate the correct failure rate of instances starts for a particular pool or cloud account" do + start_time = Time.utc(2010,"jan",1,20,15,1) + create_time = start_time + 1 + end_time = create_time + 1 + user = Factory :pool_user + pool = Factory(:pool, :owner => user) + cloud_account = Factory :mock_cloud_account + instance = Factory(:instance, :pool => pool, :cloud_account => cloud_account) + + failures = 5 + non_failures = 20 + + for i in 1..failures + task = InstanceTask.new(:instance => instance, + :type => "InstanceTask", + :state => Task::STATE_FAILED, + :failure_code => Task::FAILURE_OVER_POOL_QUOTA, + :action => InstanceTask::ACTION_CREATE, + :task_target_id => instance.id) + task.created_at = create_time + task.save! + end + + for i in 1..non_failures + task = InstanceTask.new(:instance => instance, + :type => "InstanceTask", + :state => Task::STATE_FINISHED, + :failure_code => nil, + :action => InstanceTask::ACTION_CREATE, + :task_target_id => instance.id) + task.created_at = create_time + task.save! + end + + date = DataServiceActiveRecord.failure_rate(pool, start_time, end_time, Task::FAILURE_OVER_POOL_QUOTA) + date.failure_rate.should == (100 / (non_failures + failures)) * failures + end + + it "should create data points for failure rates of instances between two times at given intervals" do + interval_length = 1000 + + start_time = Time.utc(2010,"jan",1,20,15,1) + start_time1 = start_time + interval_length + start_time2 = start_time1 + interval_length + start_times = [start_time, start_time1, start_time2] + + end_time = start_time2 + interval_length + + failures = [5, 10, 15] + number_of_instances = 20 + + user = Factory :pool_user + pool = Factory(:pool, :owner => user) + cloud_account = Factory :mock_cloud_account + instance = Factory(:instance, :pool => pool, :cloud_account => cloud_account) + + for i in 0..2 + for j in 1..failures[i] + task = InstanceTask.new(:instance => instance, + :type => "InstanceTask", + :state => Task::STATE_FAILED, + :failure_code => Task::FAILURE_OVER_POOL_QUOTA, + :action => InstanceTask::ACTION_CREATE, + :task_target_id => instance.id) + task.created_at = start_times[i] + task.time_submitted = start_times[i] + task.save! + end + + non_failures = number_of_instances - failures[i] + for j in 1..non_failures + task = InstanceTask.new(:instance => instance, + :type => "InstanceTask", + :state => Task::STATE_FINISHED, + :failure_code => nil, + :action => InstanceTask::ACTION_CREATE, + :task_target_id => instance.id) + task.created_at = start_times[i] + task.time_submitted = start_times[i] + task.save! + end + end + + data = DataServiceActiveRecord.qos_failure_rate_stats(pool, start_time, end_time, interval_length, Task::FAILURE_OVER_POOL_QUOTA) + data[0].should == DataServiceActiveRecord::QoSFailureRatePoint.new(start_time, 25) + data[1].should == DataServiceActiveRecord::QoSFailureRatePoint.new(start_time1, 50) + data[2].should == DataServiceActiveRecord::QoSFailureRatePoint.new(start_time2, 75) + end + + def generate_tasks(start_time, interval_length, instance, expected_averages) + interval_time = start_time + expected_averages.each do |avg| + submission_time = interval_time + (interval_length / 2) + for i in 1..9 do + started_time = submission_time + ((avg / 10) * 2) * i + + task = InstanceTask.new(:instance => instance, + :type => "InstanceTask", + :state => Task::STATE_QUEUED, + :failure_code => nil, + :action => InstanceTask::ACTION_CREATE, + :task_target_id => instance.id) + task.created_at = submission_time + task.time_submitted = submission_time + task.time_started = started_time + task.save! + end + interval_time += interval_length + end + end +end \ No newline at end of file diff --git a/src/spec/services/data_service_spec.rb b/src/spec/services/data_service_spec.rb deleted file mode 100644 index 56bbc41..0000000 --- a/src/spec/services/data_service_spec.rb +++ /dev/null @@ -1,182 +0,0 @@ -require 'spec_helper' - -describe DataService do - - it "should calculate the total instance quota usage for a provider with a numbner of cloud accounts" do - client = mock('DeltaCloud', :null_object => true) - provider = Factory.build(:mock_provider) - provider.stub!(:connect).and_return(client) - provider.save! - - data = [[25, 10], [40, 20], [20, 20]] - free = 0 - for i in 0..2 - cloud_account = Factory.build(:cloud_account, :provider => provider, :username => "username" + i.to_s) - cloud_account.stub!(:valid_credentials?).and_return(true) - cloud_account.save! - - quota = Factory(:quota, :maximum_total_instances => data[i][0], :total_instances => data[i][1]) - cloud_account.quota_id = quota.id - cloud_account.save! - - free += (data[i][0] - data[i][1]) - end - - data_points = DataService.total_quota_utilisation(provider) - data_points[0].should == DataService::TotalQuotaUsagePoint.new("username0", data[0][1]) - data_points[1].should == DataService::TotalQuotaUsagePoint.new("username1", data[1][1]) - data_points[2].should == DataService::TotalQuotaUsagePoint.new("username2", data[2][1]) - data_points[3].should == DataService::TotalQuotaUsagePoint.new("free", free) - - end - - it "should calculate the total number of instances and maximum number of instances of a cloud account" do - client = mock('DeltaCloud', :null_object => true) - provider = Factory.build(:mock_provider) - provider.stub!(:connect).and_return(client) - provider.save! - - cloud_account = Factory.build(:cloud_account, :provider => provider) - cloud_account.stub!(:valid_credentials?).and_return(true) - cloud_account.save! - - quota = Factory(:quota, - :maximum_running_instances => 40, - :maximum_running_memory => 10240, - :maximum_running_cpus => 10, - :maximum_total_instances => 50, - :maximum_total_storage => 500, - :running_instances => 20, - :running_memory => 4096, - :running_cpus => 7, - :total_instances => 20, - :total_storage => 499) - cloud_account.quota_id = quota.id - - data_point = DataService.quota_utilisation(cloud_account, Quota::RESOURCE_RUNNING_INSTANCES) - data_point.should == DataService::QuotaUsagePoint.new(20, 40) - - data_point = DataService.quota_utilisation(cloud_account, Quota::RESOURCE_RUNNING_MEMORY) - data_point.should == DataService::QuotaUsagePoint.new(4096, 10240) - - data_point = DataService.quota_utilisation(cloud_account, Quota::RESOURCE_RUNNING_CPUS) - data_point.should == DataService::QuotaUsagePoint.new(7, 10) - - data_point = DataService.quota_utilisation(cloud_account, Quota::RESOURCE_TOTAL_INSTANCES) - data_point.should == DataService::QuotaUsagePoint.new(20, 50) - - data_point = DataService.quota_utilisation(cloud_account, Quota::RESOURCE_TOTAL_STORAGE) - data_point.should == DataService::QuotaUsagePoint.new(499, 500) - - data_point = DataService.quota_utilisation(cloud_account, Quota::RESOURCE_OVERALL) - data_point.should == DataService::QuotaUsagePoint.new(499, 500) - end - - it "should calculate the average, max and min task submission times" do - tasks = [] - instance = Factory :instance - - for i in 1..10 do - time = Time.utc(2010,"jan",1,20,15,1) - task = Task.new(:instance => instance, :type => "InstanceTask", :state => Task::STATE_PENDING, :failure_code => nil) - task.time_submitted = time - time += i - task.time_started = time - task.save - tasks << task - end - - data_point = DataService.tasks_submissions_mean_max_min(Time.now, tasks) - - data_point.average.should == 5.5 - data_point.min.should == 1 - data_point.max.should == 10 - end - - it "should create data points for the average, max and min task submission times between two times at given intervals" do - pool = Factory :pool - instance = Factory(:instance, :pool_id => pool.id) - - expected_averages = [ 20, 40, 60, 80, 100] - no_intervals = expected_averages.length - interval_length = 30 - - end_time = Time.utc(2010,"jan",1,20,15,1) - start_time = end_time - (interval_length * no_intervals) - - generate_tasks(start_time, interval_length, instance, expected_averages) - - data_points = DataService.qos_task_submission_stats(start_time, end_time, interval_length, pool, InstanceTask::ACTION_CREATE) - - for i in 0...data_points.length - average_time = expected_averages[i] - dp = data_points[i] - - dp.average.should == average_time - # The multiplications could be set as static numbers but are left as calculations for easier understanding of code - dp.max.should == (average_time / 10) * 2 * 9 - dp.min.should == (average_time / 10) * 2 - end - end - - it "should create data points for mean, max and min task submission times at given intervals for a provider with multiple accounts" do - pool = Factory :pool - - expected_averages = [] - expected_averages[0] = [ 20, 40, 60, 80, 100] - expected_averages[1] = [ 40, 60, 80, 100, 120] - expected_averages[2] = [ 60, 80, 100, 120, 140] - - no_intervals = expected_averages.length - interval_length = 30 - end_time = Time.utc(2010,"jan",1,20,15,1) - start_time = end_time - (interval_length * no_intervals) - - client = mock('DeltaCloud', :null_object => true) - provider = Factory.build(:mock_provider) - provider.stub!(:connect).and_return(client) - provider.save! - - cloud_accounts = [] - expected_averages.each do |expected_average| - cloud_account = Factory.build(:cloud_account, :provider => provider, :username => "username" + expected_average[0].to_s) - cloud_account.stub!(:valid_credentials?).and_return(true) - cloud_account.save! - - instance = Factory(:instance, :cloud_account_id => cloud_account.id, :pool_id => pool.id) - generate_tasks(start_time, interval_length, instance, expected_average) - end - - data_points = DataService.qos_task_submission_stats(start_time, end_time, interval_length, pool, InstanceTask::ACTION_CREATE) - - for i in 0...data_points.length - dp = data_points[i] - dp.average.should == expected_averages[1][i] - dp.max.should == (expected_averages[2][i] / 10) * 2 * 9 - dp.min.should == (expected_averages[0][i] / 10) * 2 - end - end - - def generate_tasks(start_time, interval_length, instance, expected_averages) - interval_time = start_time - expected_averages.each do |avg| - submission_time = interval_time + (interval_length / 2) - for i in 1..9 do - started_time = submission_time + ((avg / 10) * 2) * i - - task = InstanceTask.new(:instance => instance, - :type => "InstanceTask", - :state => Task::STATE_QUEUED, - :failure_code => nil, - :action => InstanceTask::ACTION_CREATE, - :task_target_id => instance.id) - task.created_at = submission_time - task.time_submitted = submission_time - task.time_started = started_time - task.save! - end - interval_time += interval_length - end - end - -end
From: martyntaylor mtaylor@redhat.com
--- src/lib/tasks/demo_data.rake | 20 ++++++++++++-------- 1 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/src/lib/tasks/demo_data.rake b/src/lib/tasks/demo_data.rake index ef6e8d2..f38ebbd 100644 --- a/src/lib/tasks/demo_data.rake +++ b/src/lib/tasks/demo_data.rake @@ -35,7 +35,7 @@ namespace :db do cloud_account = CloudAccount.find(args[:cloud_account_id]) user = User.find(args[:user_id])
- instance = create_instance() + instance = create_instance(cloud_account)
started_at = Time.now create_tasks(instance, user) @@ -44,12 +44,13 @@ namespace :db do print_stats(started_at, ended_at) end
- def create_instance() - instance = Instance.new({:name => "instance", + def create_instance(cloud_account) + instance = Instance.new({:name => "instance" + Time.now.to_s, :hardware_profile_id => HardwareProfile.find(:first), :image_id => Image.find(:first), :state => Instance::STATE_NEW, - :pool_id => 1 + :pool_id => 1, + :cloud_account_id => cloud_account.id }) instance.save! return instance @@ -81,17 +82,20 @@ namespace :db do
random = 1 + rand(100) if random <= PROB_FAILURE + time_created = END_TIME - rand(1 + (@time_scale.to_f)) + task.created_at = time_created task.state = Task::STATE_FAILED task.failure_code = Task::FAILURE_CODES[rand(Task::FAILURE_CODES.length)] else task.state = Task::STATE_FINISHED - time_submitted = END_TIME - rand(1 + (@time_scale.to_f)) + task.created_at = time_submitted task.time_submitted = time_submitted
- task.time_started = calculate_time_started(time_submitted, random) - - time_ended = task.time_started + rand(20) + 1 + time_started = calculate_time_started(time_submitted, random) + task.time_started = time_started + task.save! + time_ended = time_started + rand(20) + 1 task.time_ended = time_ended end task.save!
From: martyntaylor mtaylor@redhat.com
--- src/app/controllers/dashboard_controller.rb | 18 +- src/app/models/graph.rb | 28 +++ src/app/services/graph_service.rb | 320 +++++++++++++++++---------- src/app/views/dashboard/summary.haml | 8 +- 4 files changed, 255 insertions(+), 119 deletions(-)
diff --git a/src/app/controllers/dashboard_controller.rb b/src/app/controllers/dashboard_controller.rb index bcd2073..757598d 100644 --- a/src/app/controllers/dashboard_controller.rb +++ b/src/app/controllers/dashboard_controller.rb @@ -31,17 +31,25 @@ class DashboardController < ApplicationController return params[:ajax] == "true" end
- def provider_qos_graph + def provider_qos_avg_time_to_submit_graph params[:provider] = Provider.find(params[:id]) - graph = GraphService.dashboard_qos(current_user, params)[params[:provider]][Graph::QOS_AVG_TIME_TO_SUBMIT] + graph = GraphService.dashboard_qos_avg_time_to_submit_graph(current_user, params)[params[:provider]][Graph::QOS_AVG_TIME_TO_SUBMIT] respond_to do |format| format.svg { render :xml => graph.svg} end end
- def account_quota_graph - params[:account] = CloudAccount.find(params[:id]) - graph = GraphService.dashboard_quota(current_user, params)[params[:account]][Graph::QUOTA_INSTANCES_IN_USE] + def quota_usage_graph + if params[:cloud_account_id] + params[:parent] = CloudAccount.find(params[:cloud_account_id]) + elsif params[:pool_id] + params[:parent] = Pool.find(params[:pool_id]) + else + return nil + end + + graphs = GraphService.dashboard_quota_usage(current_user, params) + graph = graphs[params[:parent]][Graph.get_quota_usage_graph_name(params[:resource_name])] respond_to do |format| format.svg { render :xml => graph.svg} end diff --git a/src/app/models/graph.rb b/src/app/models/graph.rb index 1c20dc3..cf1b498 100644 --- a/src/app/models/graph.rb +++ b/src/app/models/graph.rb @@ -4,7 +4,35 @@ class Graph QOS_AVG_TIME_TO_SUBMIT = "qos_avg_time_to_submit" QUOTA_INSTANCES_IN_USE = "quota_instances_in_use" INSTANCES_BY_PROVIDER_PIE = "instances_by_provider_pie" + + # Quota Usage Graphs + QUOTA_USAGE_RUNNING_INSTANCES = "quota_utilization_running_instances" + QUOTA_USAGE_RUNNING_MEMORY = "quota_utilization_running_memory" + QUOTA_USAGE_RUNNING_CPUS = "quota_utilization_running_cpus" + QUOTA_USAGE_TOTAL_INSTANCES = "quota_utilization_total_instances" + QUOTA_USAGE_TOTAL_STORAGE = "quota_utilization_total_storage" + QUOTA_USAGE_OVERALL = "quota_utilization_overall" + def initialize @svg = "" end + + def self.get_quota_usage_graph_name(resource_name) + case resource_name + when Quota::RESOURCE_RUNNING_INSTANCES + return QUOTA_USAGE_RUNNING_INSTANCES + when Quota::RESOURCE_RUNNING_MEMORY + return QUOTA_USAGE_RUNNING_MEMORY + when Quota::RESOURCE_RUNNING_CPUS + return QUOTA_USAGE_RUNNING_CPUS + when Quota::RESOURCE_TOTAL_INSTANCES + return QUOTA_USAGE_TOTAL_INSTANCES + when Quota::RESOURCE_TOTAL_STORAGE + return QUOTA_USAGE_TOTAL_STORAGE + when Quota::RESOURCE_OVERALL + return QUOTA_USAGE_OVERALL + else + return nil + end + end end diff --git a/src/app/services/graph_service.rb b/src/app/services/graph_service.rb index 11da01c..6c17743 100644 --- a/src/app/services/graph_service.rb +++ b/src/app/services/graph_service.rb @@ -3,6 +3,8 @@ class GraphService require 'nokogiri' require 'scruffy'
+ DATA_SERVICE = DataServiceActiveRecord + def self.dashboard_quota (user,opts = {}) #FIXME add permission checks to filter what graphs user can get graphs = Hash.new @@ -12,7 +14,7 @@ class GraphService if opts[:cloud_account] cloud_account = opts[:cloud_account] cloud_account_graphs = Hash.new - cloud_account_graphs[Graph::QUOTA_INSTANCES_IN_USE] = quota_instances_in_use_graph(cloud_account,opts) + cloud_account_graphs[Graph::QUOTA_INSTANCES_IN_USE] = qos_failure_rate_graph(parent, opts = {}) graphs[cloud_account] = cloud_account_graphs else CloudAccount.all.each do |cloud_account| @@ -24,7 +26,16 @@ class GraphService graphs end
- def self.dashboard_qos (user,opts = {}) + def self.dashboard_quota_usage(user, opts = {}) + parent = opts[:parent] + + graphs = Hash.new + graphs[parent] = quota_usage_graph(parent, opts) + + return graphs + end + + def self.dashboard_qos_avg_time_to_submit_graph(user, opts = {}) #FIXME add permission checks to filter what graphs user can get graphs = Hash.new
@@ -58,7 +69,198 @@ class GraphService output_stream = IO::popen( cmd, "r+") end
- def self.quota_instances_in_use_graph (cloud_account, opts = {}) + def self.quota_usage_graph (parent, opts = {}) + x = [1,2] + + #we'll just have zero values for the unexpected case where cloud_account has no quota + y = x.collect { |v| 0 } + if parent.quota + quota = parent.quota + data_point = DataServiceActiveRecord.quota_usage(parent, opts[:resource_name]) + #Handle No Limit case + if data_point.max == Quota::NO_LIMIT + y = [data_point.used, nil] + else + y = [data_point.used, data_point.max] + end + end + + chart_opts = {:x => x, :y => y} + + graphs = Hash.new + graphs[Graph.get_quota_usage_graph_name(opts[:resource_name])] = draw_bar_chart(opts, chart_opts) + return graphs + end + + def self.qos_avg_time_to_submit_graph(parent, opts = {}) + start_time = Time.parse(opts[:start_time]) + end_time = Time.parse(opts[:end_time]) + interval_length = opts[:interval_length].to_f + action = opts[:task_action] + + stats = DATA_SERVICE.qos_task_submission_stats(parent, start_time, end_time, interval_length, action) + data = get_data_from_stats(stats, "average") + draw_line_graph(opts, data) + end + + def self.qos_failure_rate_graph(parent, opts = {}) + start_time = Time.parse(opts[:start_time]) + end_time = Time.parse(opts[:end_time]) + interval_length = opts[:interval_length].to_f + failure_code = opts[:failure_code] + + stats = DATA_SERVICE.qos_failure_rate_stats(parent, start_time, end_time, interval_length, failure_code) + data = get_data_from_stats(stats, "failure_rate") + data[:y_range] = "[0:100]" + draw_line_graph(opts, data) + end + + def self.qos_avg_time_to_complete_life_cycle_event(parent, opts = {}) + start_time = Time.parse(opts[:start_time]) + end_time = Time.parse(opts[:end_time]) + interval_length = opts[:interval_length].to_f + action = opts[:task_action] + + stats = DATA_SERVICE.qos_task_completion_stats(parent, start_time, end_time, interval_length, action) + data = get_data_from_stats(stats, "average") + draw_line_graph(opts, data) + end + + def self.instances_by_provider_pie (opts = {}) + pie_opts = {} + providers = Provider.all + providers.each do |provider| + running_instances = 0 + provider.cloud_accounts.each do |account| + running_instances = running_instances + account.quota.running_instances if account.quota + end + if running_instances > 0 + pie_opts[:"#{provider.name}"] = running_instances + end + end + + return draw_pie_chart(opts, pie_opts) + end + + def self.get_data_from_stats(stats, type) + x = [] + y = [] + y_max = 0 + for i in 0...stats.length do + x << i + y_value = stats[i][type] + if y_value + y << y_value + if y_value > y_max + y_max = y_value + end + else + y << 0 + end + end + + if y_max == 0 + y_max = 1 + else + y_max = y_max * 1.1 + end + + y_range = "[0:" + y_max.to_s + "]" + return { :x => x, :y => y, :y_range => y_range } + end + + def self.draw_pie_chart(opts, pie_opts) + #things we're checking for in opts: :height, :width + height = 200 unless opts[:height].nil? ? nil : height = opts[:height].to_i + width = 300 unless opts[:width].nil? ? nil : width = opts[:width].to_i + + graph = Graph.new + + mytheme = Scruffy::Themes::Keynote.new + mytheme.background = :white + mytheme.marker = :black #sets the label text color + mytheme.colors = %w(#00689a #00b0e0) + + scruffy_graph = Scruffy::Graph.new({:theme => mytheme}) + scruffy_graph.renderer = Scruffy::Renderers::Pie.new + scruffy_graph.add :pie, '', pie_opts + + raw_svg = scruffy_graph.render :width => width, :height => height + + xml = Nokogiri::XML(raw_svg) + svg = xml.css 'svg' + svg.each do |node| + node.set_attribute 'viewBox',"0 0 #{width} #{height}" + end + + xml.root.traverse do |node| + if node.name == 'text' + if node.has_attribute? 'font-family' + node.set_attribute 'font-family','sans-serif' + end + if (node.has_attribute? 'font-size') && node.get_attribute('font-size').length > 0 + size = node.get_attribute('font-size').to_f + size = size * 1.5 + node.set_attribute 'font-size',size.to_s + end + end + end + + graph.svg = xml.to_s + graph + end + + def self.draw_line_graph(opts, data) + #things we're checking for in opts: :height, :width + + height = 60 unless opts[:height].nil? ? nil : height = opts[:height].to_i + width = 100 unless opts[:width].nil? ? nil : width = opts[:width].to_i + + graph = Graph.new + gp = gnuplot_open + + Gnuplot::Plot.new( gp ) do |plot| + plot.terminal "svg size #{width},#{height}" + plot.arbitrary_lines << "unset xtics" + plot.arbitrary_lines << "unset x2tics" + plot.arbitrary_lines << "unset ytics" + plot.arbitrary_lines << "unset y2tics" + plot.set "bmargin","0" + plot.set "lmargin","1" + plot.set "rmargin","0" + plot.set "tmargin","0" + + #FIXME: get data from DataService for the provider. + #For demo, plot a random walk for demo of graph display until we hook into DataService + #First build two equal-length arrays + #x = (0..500).collect { |v| v.to_f } + + #walk = 0 + #y = x.collect { |v| rand > 0.5 ? walk = walk + 1 : walk = walk - 1 } + + x = data[:x] + y = data[:y] + y_range = data[:y_range] + + #plot.set "yrange [-50:50]" + plot.set "yrange " + y_range + + #This type of plot takes two equal length arrays of numbers as input. + plot.data << Gnuplot::DataSet.new( [x, y] ) do |ds| + ds.using = "1:2" + ds.with = "lines" + ds.notitle + end + end + gp.flush + gp.close_write + gp.read(nil,graph.svg) + gp.close_read + graph + end + + def self.draw_bar_chart(opts, chart_opts) + #things we're checking for in opts: :max_value, :height, :width
unless max_value = opts[:max_value] @@ -67,7 +269,6 @@ class GraphService height = 80 unless opts[:height].nil? ? nil : height = opts[:height].to_i width = 150 unless opts[:width].nil? ? nil : width = opts[:width].to_i
- raw_svg = "" gp = gnuplot_open Gnuplot::Plot.new( gp ) do |plot| @@ -87,14 +288,8 @@ class GraphService plot.set "xrange [.25:2.75]" plot.set "yrange [0:#{max_value * 1.5}]" #we want to scale maxvalue 50% larger to leave room for label
- x = [1,2] - #we'll just have zero values for the unexpected case where cloud_account has no quota - y = x.collect { |v| 0 } - if cloud_account.quota - quota = cloud_account.quota - y = [quota.running_instances,quota.maximum_running_instances] - end - + x = chart_opts[:x] + y = chart_opts[:y]
#The two arrays above are three columns of data for gnuplot. plot.data << Gnuplot::DataSet.new( [[x[0]], [y[0]]] ) do |ds| @@ -155,105 +350,6 @@ class GraphService graph = Graph.new graph.svg = modified_svg graph - - end - - def self.qos_avg_time_to_submit_graph (provider, opts = {}) - #things we're checking for in opts: :height, :width - - height = 60 unless opts[:height].nil? ? nil : height = opts[:height].to_i - width = 100 unless opts[:width].nil? ? nil : width = opts[:width].to_i - - graph = Graph.new - gp = gnuplot_open - - Gnuplot::Plot.new( gp ) do |plot| - plot.terminal "svg size #{width},#{height}" - plot.arbitrary_lines << "unset xtics" - plot.arbitrary_lines << "unset x2tics" - plot.arbitrary_lines << "unset ytics" - plot.arbitrary_lines << "unset y2tics" - plot.set "bmargin","0" - plot.set "lmargin","1" - plot.set "rmargin","0" - plot.set "tmargin","0" - - #FIXME: get data from DataService for the provider. - #For demo, plot a random walk for demo of graph display until we hook into DataService - #First build two equal-length arrays - x = (0..500).collect { |v| v.to_f } - - walk = 0 - y = x.collect { |v| rand > 0.5 ? walk = walk + 1 : walk = walk - 1 } - plot.set "yrange [-50:50]" - - #This type of plot takes two equal length arrays of numbers as input. - plot.data << Gnuplot::DataSet.new( [x, y] ) do |ds| - ds.using = "1:2" - ds.with = "lines" - ds.notitle - end - end - gp.flush - gp.close_write - gp.read(nil,graph.svg) - gp.close_read - graph - end - - def self.instances_by_provider_pie (opts = {}) - #things we're checking for in opts: :height, :width - - height = 200 unless opts[:height].nil? ? nil : height = opts[:height].to_i - width = 300 unless opts[:width].nil? ? nil : width = opts[:width].to_i - - graph = Graph.new - - mytheme = Scruffy::Themes::Keynote.new - mytheme.background = :white - mytheme.marker = :black #sets the label text color - mytheme.colors = %w(#00689a #00b0e0) - - scruffy_graph = Scruffy::Graph.new({:theme => mytheme}) - scruffy_graph.renderer = Scruffy::Renderers::Pie.new - - pie_opts = {} - providers = Provider.all - providers.each do |provider| - running_instances = 0 - provider.cloud_accounts.each do |account| - running_instances = running_instances + account.quota.running_instances if account.quota - end - if running_instances > 0 - pie_opts[:"#{provider.name}"] = running_instances - end - end - - scruffy_graph.add :pie, '', pie_opts - - raw_svg = scruffy_graph.render :width => width, :height => height - - xml = Nokogiri::XML(raw_svg) - svg = xml.css 'svg' - svg.each do |node| - node.set_attribute 'viewBox',"0 0 #{width} #{height}" - end - - xml.root.traverse do |node| - if node.name == 'text' - if node.has_attribute? 'font-family' - node.set_attribute 'font-family','sans-serif' - end - if (node.has_attribute? 'font-size') && node.get_attribute('font-size').length > 0 - size = node.get_attribute('font-size').to_f - size = size * 1.5 - node.set_attribute 'font-size',size.to_s - end - end - end - - graph.svg = xml.to_s - graph end
-end +end \ No newline at end of file diff --git a/src/app/views/dashboard/summary.haml b/src/app/views/dashboard/summary.haml index 66aedde..f3a21d3 100644 --- a/src/app/views/dashboard/summary.haml +++ b/src/app/views/dashboard/summary.haml @@ -60,7 +60,11 @@ %div{ :style => "clear:both"} - @providers.each do |provider| .provider_service_quality_graph - = "<object data='" + url_for(:action => :provider_qos_graph, :id => provider.id, :width => 100, :height => 50) + "' type='image/svg+xml' />" + -end_time = Time.now + -start_time = end_time - (24 * 60 * 60) + -interval_length = 3600 + -task_action = "create" + = "<object data='" + url_for(:action => :provider_qos_avg_time_to_submit_graph, :id => provider.id, :start_time => start_time, :end_time => end_time, :interval_length => interval_length, :task_action => task_action, :width => 100, :height => 50) + "' type='image/svg+xml' />" .provider_service_quality_graph_summary = provider.name <!-- FIXME 'good/poor/average service... --> @@ -109,7 +113,7 @@ .account_quota_usage_graph_summary = account.provider.name + ": " + account.name .account_quota_usage_current_graph - %object{ :data => url_for(:action => :account_quota_graph, :id => account.id, :width => 100, :height => 50), :type => 'image/svg+xml'} + %object{ :data => url_for(:action => :quota_usage_graph, :cloud_account_id => account.id, :resource_name => Quota::RESOURCE_RUNNING_INSTANCES, :width => 100, :height => 50), :type => 'image/svg+xml'} <div style="clear: both;" />
:javascript
On Tue, Jul 20, 2010 at 12:49:04PM -0400, mtaylor@redhat.com wrote:
From: martyntaylor mtaylor@redhat.com
src/app/controllers/dashboard_controller.rb | 18 +- src/app/models/graph.rb | 28 +++ src/app/services/graph_service.rb | 320 +++++++++++++++++---------- src/app/views/dashboard/summary.haml | 8 +- 4 files changed, 255 insertions(+), 119 deletions(-)
diff --git a/src/app/controllers/dashboard_controller.rb b/src/app/controllers/dashboard_controller.rb index bcd2073..757598d 100644 --- a/src/app/controllers/dashboard_controller.rb +++ b/src/app/controllers/dashboard_controller.rb @@ -31,17 +31,25 @@ class DashboardController < ApplicationController return params[:ajax] == "true" end
- def provider_qos_graph
- def provider_qos_avg_time_to_submit_graph params[:provider] = Provider.find(params[:id])
- graph = GraphService.dashboard_qos(current_user, params)[params[:provider]][Graph::QOS_AVG_TIME_TO_SUBMIT]
- graph = GraphService.dashboard_qos_avg_time_to_submit_graph(current_user, params)[params[:provider]][Graph::QOS_AVG_TIME_TO_SUBMIT] respond_to do |format| format.svg { render :xml => graph.svg} end end
- def account_quota_graph
- params[:account] = CloudAccount.find(params[:id])
- graph = GraphService.dashboard_quota(current_user, params)[params[:account]][Graph::QUOTA_INSTANCES_IN_USE]
- def quota_usage_graph
- if params[:cloud_account_id]
params[:parent] = CloudAccount.find(params[:cloud_account_id])
- elsif params[:pool_id]
params[:parent] = Pool.find(params[:pool_id])
- else
return nil
- end
- graphs = GraphService.dashboard_quota_usage(current_user, params)
- graph = graphs[params[:parent]][Graph.get_quota_usage_graph_name(params[:resource_name])] respond_to do |format| format.svg { render :xml => graph.svg} end
diff --git a/src/app/models/graph.rb b/src/app/models/graph.rb index 1c20dc3..cf1b498 100644 --- a/src/app/models/graph.rb +++ b/src/app/models/graph.rb @@ -4,7 +4,35 @@ class Graph QOS_AVG_TIME_TO_SUBMIT = "qos_avg_time_to_submit" QUOTA_INSTANCES_IN_USE = "quota_instances_in_use" INSTANCES_BY_PROVIDER_PIE = "instances_by_provider_pie"
- # Quota Usage Graphs
- QUOTA_USAGE_RUNNING_INSTANCES = "quota_utilization_running_instances"
- QUOTA_USAGE_RUNNING_MEMORY = "quota_utilization_running_memory"
- QUOTA_USAGE_RUNNING_CPUS = "quota_utilization_running_cpus"
- QUOTA_USAGE_TOTAL_INSTANCES = "quota_utilization_total_instances"
- QUOTA_USAGE_TOTAL_STORAGE = "quota_utilization_total_storage"
- QUOTA_USAGE_OVERALL = "quota_utilization_overall"
- def initialize @svg = "" end
- def self.get_quota_usage_graph_name(resource_name)
- case resource_name
when Quota::RESOURCE_RUNNING_INSTANCES
return QUOTA_USAGE_RUNNING_INSTANCES
when Quota::RESOURCE_RUNNING_MEMORY
return QUOTA_USAGE_RUNNING_MEMORY
when Quota::RESOURCE_RUNNING_CPUS
return QUOTA_USAGE_RUNNING_CPUS
when Quota::RESOURCE_TOTAL_INSTANCES
return QUOTA_USAGE_TOTAL_INSTANCES
when Quota::RESOURCE_TOTAL_STORAGE
return QUOTA_USAGE_TOTAL_STORAGE
when Quota::RESOURCE_OVERALL
return QUOTA_USAGE_OVERALL
else
return nil
- end
- end
end diff --git a/src/app/services/graph_service.rb b/src/app/services/graph_service.rb index 11da01c..6c17743 100644 --- a/src/app/services/graph_service.rb +++ b/src/app/services/graph_service.rb @@ -3,6 +3,8 @@ class GraphService require 'nokogiri' require 'scruffy'
- DATA_SERVICE = DataServiceActiveRecord
- def self.dashboard_quota (user,opts = {}) #FIXME add permission checks to filter what graphs user can get graphs = Hash.new
@@ -12,7 +14,7 @@ class GraphService if opts[:cloud_account] cloud_account = opts[:cloud_account] cloud_account_graphs = Hash.new
cloud_account_graphs[Graph::QUOTA_INSTANCES_IN_USE] = quota_instances_in_use_graph(cloud_account,opts)
else CloudAccount.all.each do |cloud_account|cloud_account_graphs[Graph::QUOTA_INSTANCES_IN_USE] = qos_failure_rate_graph(parent, opts = {}) graphs[cloud_account] = cloud_account_graphs
@@ -24,7 +26,16 @@ class GraphService graphs end
- def self.dashboard_qos (user,opts = {})
- def self.dashboard_quota_usage(user, opts = {})
- parent = opts[:parent]
- graphs = Hash.new
- graphs[parent] = quota_usage_graph(parent, opts)
- return graphs
- end
- def self.dashboard_qos_avg_time_to_submit_graph(user, opts = {}) #FIXME add permission checks to filter what graphs user can get graphs = Hash.new
@@ -58,7 +69,198 @@ class GraphService output_stream = IO::popen( cmd, "r+") end
- def self.quota_instances_in_use_graph (cloud_account, opts = {})
def self.quota_usage_graph (parent, opts = {})
x = [1,2]
#we'll just have zero values for the unexpected case where cloud_account has no quota
y = x.collect { |v| 0 }
if parent.quota
quota = parent.quota
data_point = DataServiceActiveRecord.quota_usage(parent, opts[:resource_name])
#Handle No Limit case
if data_point.max == Quota::NO_LIMIT
y = [data_point.used, nil]
else
y = [data_point.used, data_point.max]
end
end
chart_opts = {:x => x, :y => y}
graphs = Hash.new
graphs[Graph.get_quota_usage_graph_name(opts[:resource_name])] = draw_bar_chart(opts, chart_opts)
return graphs
end
def self.qos_avg_time_to_submit_graph(parent, opts = {})
start_time = Time.parse(opts[:start_time])
end_time = Time.parse(opts[:end_time])
interval_length = opts[:interval_length].to_f
action = opts[:task_action]
stats = DATA_SERVICE.qos_task_submission_stats(parent, start_time, end_time, interval_length, action)
data = get_data_from_stats(stats, "average")
draw_line_graph(opts, data)
end
def self.qos_failure_rate_graph(parent, opts = {})
start_time = Time.parse(opts[:start_time])
end_time = Time.parse(opts[:end_time])
interval_length = opts[:interval_length].to_f
failure_code = opts[:failure_code]
stats = DATA_SERVICE.qos_failure_rate_stats(parent, start_time, end_time, interval_length, failure_code)
data = get_data_from_stats(stats, "failure_rate")
data[:y_range] = "[0:100]"
draw_line_graph(opts, data)
end
def self.qos_avg_time_to_complete_life_cycle_event(parent, opts = {})
start_time = Time.parse(opts[:start_time])
end_time = Time.parse(opts[:end_time])
interval_length = opts[:interval_length].to_f
action = opts[:task_action]
stats = DATA_SERVICE.qos_task_completion_stats(parent, start_time, end_time, interval_length, action)
data = get_data_from_stats(stats, "average")
draw_line_graph(opts, data)
end
def self.instances_by_provider_pie (opts = {})
pie_opts = {}
providers = Provider.all
providers.each do |provider|
running_instances = 0
provider.cloud_accounts.each do |account|
running_instances = running_instances + account.quota.running_instances if account.quota
end
if running_instances > 0
pie_opts[:"#{provider.name}"] = running_instances
end
end
return draw_pie_chart(opts, pie_opts)
end
def self.get_data_from_stats(stats, type)
x = []
y = []
y_max = 0
for i in 0...stats.length do
x << i
y_value = stats[i][type]
if y_value
y << y_value
if y_value > y_max
y_max = y_value
end
else
y << 0
end
end
if y_max == 0
y_max = 1
else
y_max = y_max * 1.1
end
y_range = "[0:" + y_max.to_s + "]"
return { :x => x, :y => y, :y_range => y_range }
end
def self.draw_pie_chart(opts, pie_opts)
#things we're checking for in opts: :height, :width
height = 200 unless opts[:height].nil? ? nil : height = opts[:height].to_i
width = 300 unless opts[:width].nil? ? nil : width = opts[:width].to_i
graph = Graph.new
mytheme = Scruffy::Themes::Keynote.new
mytheme.background = :white
mytheme.marker = :black #sets the label text color
mytheme.colors = %w(#00689a #00b0e0)
scruffy_graph = Scruffy::Graph.new({:theme => mytheme})
scruffy_graph.renderer = Scruffy::Renderers::Pie.new
scruffy_graph.add :pie, '', pie_opts
raw_svg = scruffy_graph.render :width => width, :height => height
xml = Nokogiri::XML(raw_svg)
svg = xml.css 'svg'
svg.each do |node|
node.set_attribute 'viewBox',"0 0 #{width} #{height}"
end
xml.root.traverse do |node|
if node.name == 'text'
if node.has_attribute? 'font-family'
node.set_attribute 'font-family','sans-serif'
end
if (node.has_attribute? 'font-size') && node.get_attribute('font-size').length > 0
size = node.get_attribute('font-size').to_f
size = size * 1.5
node.set_attribute 'font-size',size.to_s
end
end
end
graph.svg = xml.to_s
graph
end
def self.draw_line_graph(opts, data)
#things we're checking for in opts: :height, :width
height = 60 unless opts[:height].nil? ? nil : height = opts[:height].to_i
width = 100 unless opts[:width].nil? ? nil : width = opts[:width].to_i
graph = Graph.new
gp = gnuplot_open
Gnuplot::Plot.new( gp ) do |plot|
plot.terminal "svg size #{width},#{height}"
plot.arbitrary_lines << "unset xtics"
plot.arbitrary_lines << "unset x2tics"
plot.arbitrary_lines << "unset ytics"
plot.arbitrary_lines << "unset y2tics"
plot.set "bmargin","0"
plot.set "lmargin","1"
plot.set "rmargin","0"
plot.set "tmargin","0"
#FIXME: get data from DataService for the provider.
#For demo, plot a random walk for demo of graph display until we hook into DataService
#First build two equal-length arrays
#x = (0..500).collect { |v| v.to_f }
#walk = 0
#y = x.collect { |v| rand > 0.5 ? walk = walk + 1 : walk = walk - 1 }
x = data[:x]
y = data[:y]
y_range = data[:y_range]
#plot.set "yrange [-50:50]"
plot.set "yrange " + y_range
#This type of plot takes two equal length arrays of numbers as input.
plot.data << Gnuplot::DataSet.new( [x, y] ) do |ds|
ds.using = "1:2"
ds.with = "lines"
ds.notitle
end
end
gp.flush
gp.close_write
gp.read(nil,graph.svg)
gp.close_read
graph
end
def self.draw_bar_chart(opts, chart_opts)
#things we're checking for in opts: :max_value, :height, :width
unless max_value = opts[:max_value]
@@ -67,7 +269,6 @@ class GraphService height = 80 unless opts[:height].nil? ? nil : height = opts[:height].to_i width = 150 unless opts[:width].nil? ? nil : width = opts[:width].to_i
- raw_svg = "" gp = gnuplot_open Gnuplot::Plot.new( gp ) do |plot|
@@ -87,14 +288,8 @@ class GraphService plot.set "xrange [.25:2.75]" plot.set "yrange [0:#{max_value * 1.5}]" #we want to scale maxvalue 50% larger to leave room for label
x = [1,2]
#we'll just have zero values for the unexpected case where cloud_account has no quota
y = x.collect { |v| 0 }
if cloud_account.quota
quota = cloud_account.quota
y = [quota.running_instances,quota.maximum_running_instances]
end
x = chart_opts[:x]
y = chart_opts[:y] #The two arrays above are three columns of data for gnuplot. plot.data << Gnuplot::DataSet.new( [[x[0]], [y[0]]] ) do |ds|
@@ -155,105 +350,6 @@ class GraphService graph = Graph.new graph.svg = modified_svg graph
- end
- def self.qos_avg_time_to_submit_graph (provider, opts = {})
- #things we're checking for in opts: :height, :width
- height = 60 unless opts[:height].nil? ? nil : height = opts[:height].to_i
- width = 100 unless opts[:width].nil? ? nil : width = opts[:width].to_i
- graph = Graph.new
- gp = gnuplot_open
- Gnuplot::Plot.new( gp ) do |plot|
plot.terminal "svg size #{width},#{height}"
plot.arbitrary_lines << "unset xtics"
plot.arbitrary_lines << "unset x2tics"
plot.arbitrary_lines << "unset ytics"
plot.arbitrary_lines << "unset y2tics"
plot.set "bmargin","0"
plot.set "lmargin","1"
plot.set "rmargin","0"
plot.set "tmargin","0"
#FIXME: get data from DataService for the provider.
#For demo, plot a random walk for demo of graph display until we hook into DataService
#First build two equal-length arrays
x = (0..500).collect { |v| v.to_f }
walk = 0
y = x.collect { |v| rand > 0.5 ? walk = walk + 1 : walk = walk - 1 }
plot.set "yrange [-50:50]"
#This type of plot takes two equal length arrays of numbers as input.
plot.data << Gnuplot::DataSet.new( [x, y] ) do |ds|
ds.using = "1:2"
ds.with = "lines"
ds.notitle
end
- end
- gp.flush
- gp.close_write
- gp.read(nil,graph.svg)
- gp.close_read
- graph
- end
- def self.instances_by_provider_pie (opts = {})
- #things we're checking for in opts: :height, :width
- height = 200 unless opts[:height].nil? ? nil : height = opts[:height].to_i
- width = 300 unless opts[:width].nil? ? nil : width = opts[:width].to_i
- graph = Graph.new
- mytheme = Scruffy::Themes::Keynote.new
- mytheme.background = :white
- mytheme.marker = :black #sets the label text color
- mytheme.colors = %w(#00689a #00b0e0)
- scruffy_graph = Scruffy::Graph.new({:theme => mytheme})
- scruffy_graph.renderer = Scruffy::Renderers::Pie.new
- pie_opts = {}
- providers = Provider.all
- providers.each do |provider|
running_instances = 0
provider.cloud_accounts.each do |account|
running_instances = running_instances + account.quota.running_instances if account.quota
end
if running_instances > 0
pie_opts[:"#{provider.name}"] = running_instances
end
- end
- scruffy_graph.add :pie, '', pie_opts
- raw_svg = scruffy_graph.render :width => width, :height => height
- xml = Nokogiri::XML(raw_svg)
- svg = xml.css 'svg'
- svg.each do |node|
node.set_attribute 'viewBox',"0 0 #{width} #{height}"
- end
- xml.root.traverse do |node|
if node.name == 'text'
if node.has_attribute? 'font-family'
node.set_attribute 'font-family','sans-serif'
end
if (node.has_attribute? 'font-size') && node.get_attribute('font-size').length > 0
size = node.get_attribute('font-size').to_f
size = size * 1.5
node.set_attribute 'font-size',size.to_s
end
end
- end
- graph.svg = xml.to_s
- graph end
-end +end \ No newline at end of file diff --git a/src/app/views/dashboard/summary.haml b/src/app/views/dashboard/summary.haml index 66aedde..f3a21d3 100644 --- a/src/app/views/dashboard/summary.haml +++ b/src/app/views/dashboard/summary.haml @@ -60,7 +60,11 @@ %div{ :style => "clear:both"} - @providers.each do |provider| .provider_service_quality_graph
= "<object data='" + url_for(:action => :provider_qos_graph, :id => provider.id, :width => 100, :height => 50) + "' type='image/svg+xml' />"
-end_time = Time.now
-start_time = end_time - (24 * 60 * 60)
-interval_length = 3600
-task_action = "create"
= "<object data='" + url_for(:action => :provider_qos_avg_time_to_submit_graph, :id => provider.id, :start_time => start_time, :end_time => end_time, :interval_length => interval_length, :task_action => task_action, :width => 100, :height => 50) + "' type='image/svg+xml' />" .provider_service_quality_graph_summary = provider.name <!-- FIXME 'good/poor/average service... -->
@@ -109,7 +113,7 @@ .account_quota_usage_graph_summary = account.provider.name + ": " + account.name .account_quota_usage_current_graph
%object{ :data => url_for(:action => :account_quota_graph, :id => account.id, :width => 100, :height => 50), :type => 'image/svg+xml'}
%object{ :data => url_for(:action => :quota_usage_graph, :cloud_account_id => account.id, :resource_name => Quota::RESOURCE_RUNNING_INSTANCES, :width => 100, :height => 50), :type => 'image/svg+xml'} <div style="clear: both;" />
:javascript
1.7.1.1
deltacloud-devel mailing list deltacloud-devel@lists.fedorahosted.org https://fedorahosted.org/mailman/listinfo/deltacloud-devel
ACK, sir!
deltacloud-devel@lists.fedorahosted.org