Skip to content

Commit a84a87a

Browse files
committed
Add cache columns to operating_systems table
Both platform and image_name are relatively stable values that get re-calculated every time they are accessed, and in the worst case, causes it to traverse through many different sources to try and find a proper `platform`/`image_name`. This is compounded when you are trying to fetch thousands of records at once. This adds the columns to be cache store for those values, and should be triggered on every update of those records, and related records (not addressed in this patch).
1 parent f3ea75f commit a84a87a

File tree

2 files changed

+318
-0
lines changed

2 files changed

+318
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
class AddImageNameToOperatingSystems < ActiveRecord::Migration[5.0]
2+
include MigrationHelper
3+
4+
StubOperatingSystemObj = Struct.new(:operating_system) do
5+
attr_reader :hardware # always nil
6+
def name
7+
""
8+
end
9+
end
10+
11+
class OperatingSystem < ActiveRecord::Base
12+
belongs_to :host
13+
belongs_to :vm_or_template
14+
belongs_to :computer_system
15+
16+
# Using a `before_save` here since this is the mechanism that will be used
17+
# in the app. Causes a bit of issues in the specs, but proves that this
18+
# would work moving forward.
19+
before_save :update_platform_and_image_name
20+
21+
def update_platform_and_image_name
22+
obj = case # rubocop:disable Style/EmptyCaseCondition
23+
when host_id then host
24+
when vm_or_template_id then vm_or_template
25+
when computer_system_id then computer_system
26+
else
27+
StubOperatingSystemObj.new(self)
28+
end
29+
30+
if obj
31+
self.image_name = self.class.image_name(obj)
32+
self.platform = self.image_name.split("_").first # rubocop:disable Style/RedundantSelf
33+
end
34+
end
35+
36+
# rubocop:disable Style/PercentLiteralDelimiters
37+
@@os_map = [ # rubocop:disable Style/ClassVars
38+
["windows_generic", %w(winnetenterprise w2k3 win2k3 server2003 winnetstandard servernt)],
39+
["windows_generic", %w(winxppro winxp)],
40+
["windows_generic", %w(vista longhorn)],
41+
["windows_generic", %w(win2k win2000)],
42+
["windows_generic", %w(microsoft windows winnt)],
43+
["linux_ubuntu", %w(ubuntu)],
44+
["linux_chrome", %w(chromeos)],
45+
["linux_chromium", %w(chromiumos)],
46+
["linux_suse", %w(suse sles)],
47+
["linux_redhat", %w(redhat rhel)],
48+
["linux_fedora", %w(fedora)],
49+
["linux_gentoo", %w(gentoo)],
50+
["linux_centos", %w(centos)],
51+
["linux_debian", %w(debian)],
52+
["linux_coreos", %w(coreos)],
53+
["linux_esx", %w(vmnixx86 vmwareesxserver esxserver vmwareesxi)],
54+
["linux_solaris", %w(solaris)],
55+
["linux_generic", %w(linux)]
56+
]
57+
# rubocop:enable Style/PercentLiteralDelimiters
58+
59+
# rubocop:disable Naming/VariableName
60+
def self.normalize_os_name(osName)
61+
findStr = osName.downcase.gsub(/[^a-z0-9]/, "")
62+
@@os_map.each do |a|
63+
a[1].each do |n|
64+
return a[0] unless findStr.index(n).nil?
65+
end
66+
end
67+
"unknown"
68+
end
69+
# rubocop:enable Naming/VariableName
70+
71+
# rubocop:disable Naming/VariableName
72+
def self.image_name(obj)
73+
osName = nil
74+
75+
# Select most accurate name field
76+
os = obj.operating_system
77+
if os
78+
# check the given field names for possible matching value
79+
osName = [:distribution, :product_type, :product_name].each do |field|
80+
os_field = os.send(field)
81+
break(os_field) if os_field && OperatingSystem.normalize_os_name(os_field) != "unknown"
82+
end
83+
84+
# If the normalized name comes back as unknown, nil out the value so we can get it from another field
85+
if osName.kind_of?(String)
86+
osName = nil if OperatingSystem.normalize_os_name(osName) == "unknown"
87+
else
88+
osName = nil
89+
end
90+
end
91+
92+
# If the OS Name is still blank check the 'user_assigned_os'
93+
if osName.nil? && obj.respond_to?(:user_assigned_os) && obj.user_assigned_os
94+
osName = obj.user_assigned_os
95+
end
96+
97+
# If the OS Name is still blank check the hardware table
98+
if osName.nil? && obj.hardware && !obj.hardware.guest_os.nil?
99+
osName = obj.hardware.guest_os
100+
# if we get generic linux or unknown back see if the vm name is better
101+
norm_os = OperatingSystem.normalize_os_name(osName)
102+
if norm_os == "linux_generic" || norm_os == "unknown" # rubocop:disable Style/MultipleComparison
103+
vm_name = OperatingSystem.normalize_os_name(obj.name)
104+
return vm_name unless vm_name == "unknown"
105+
end
106+
end
107+
108+
# If the OS Name is still blank use the name field from the object given
109+
osName = obj.name if osName.nil? && obj.respond_to?(:name)
110+
111+
# Normalize name to match existing icons
112+
OperatingSystem.normalize_os_name(osName || "")
113+
end
114+
# rubocop:enable Naming/VariableName
115+
end
116+
117+
class VmOrTemplate < ActiveRecord::Base
118+
self.inheritance_column = :_type_disabled
119+
self.table_name = 'vms'
120+
121+
has_one :operating_system
122+
has_one :hardware
123+
end
124+
125+
class Host < ActiveRecord::Base
126+
self.inheritance_column = :_type_disabled
127+
128+
has_one :operating_system
129+
has_one :hardware
130+
end
131+
132+
class ComputerSystem < ActiveRecord::Base
133+
has_one :operating_system
134+
has_one :hardware
135+
end
136+
137+
class Hardware < ActiveRecord::Base
138+
end
139+
140+
def up
141+
add_column :operating_systems, :platform, :string
142+
add_column :operating_systems, :image_name, :string
143+
144+
say_with_time("Updating platform and image_name in OperatingSystems") do
145+
base_relation = OperatingSystem.all
146+
say_batch_started(base_relation.size)
147+
148+
base_relation.find_in_batches do |group|
149+
group.each(&:save)
150+
say_batch_processed(group.count)
151+
end
152+
end
153+
end
154+
155+
def down
156+
remove_column :operating_systems, :platform, :string
157+
remove_column :operating_systems, :image_name, :string
158+
end
159+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
require_migration
2+
3+
describe AddImageNameToOperatingSystems do
4+
let(:host_stub) { migration_stub(:Host) }
5+
let(:hardware_stub) { migration_stub(:Hardware) }
6+
let(:vm_or_template_stub) { migration_stub(:VmOrTemplate) }
7+
let(:computer_system_stub) { migration_stub(:ComputerSystem) }
8+
let(:operating_system_stub) { migration_stub(:OperatingSystem) }
9+
10+
# rubocop:disable Layout/SpaceInsideArrayPercentLiteral
11+
let(:test_os_values) do
12+
[
13+
%w[an_amazing_undiscovered_os unknown],
14+
%w[centos-7 linux_centos],
15+
%w[debian-8 linux_debian],
16+
%w[opensuse-13 linux_suse],
17+
%w[sles-12 linux_suse],
18+
%w[rhel-7 linux_redhat],
19+
%w[ubuntu-15-10 linux_ubuntu],
20+
%w[windows-2012-r2 windows_generic],
21+
%w[vmnix-x86 linux_esx],
22+
%w[vista windows_generic],
23+
%w[coreos-cloud linux_coreos]
24+
]
25+
end
26+
# rubocop:enable Layout/SpaceInsideArrayPercentLiteral
27+
28+
def record_with_os(klass, os_attributes = nil, record_attributes = {:name => ""}, hardware_attributes = nil)
29+
os_record = operating_system_stub.new(os_attributes) if os_attributes
30+
31+
if klass == operating_system_stub
32+
os_record.save!
33+
os_record
34+
else
35+
record = klass.new
36+
record_attributes.each do |attr, val|
37+
record.send("#{attr}=", val) if record.respond_to?(attr)
38+
end
39+
40+
record.operating_system = os_record
41+
record.hardware = hardware_stub.new(hardware_attributes) if hardware_attributes
42+
record.save!
43+
record
44+
end
45+
end
46+
47+
# Runs tests for class type to confirm they
48+
def test_for_klass(klass)
49+
begin
50+
# This callback is necessary after the migration, but fails when the
51+
# column doesn't eixst (prior to the migration). Removing it and
52+
# re-enabling it after the migration.
53+
operating_system_stub.skip_callback(:save, :before, :update_platform_and_image_name)
54+
55+
distribution_based = []
56+
product_type_based = []
57+
product_name_based = []
58+
fallback_records = []
59+
60+
test_os_values.each do |(value, _)|
61+
distribution_based << record_with_os(klass, :distribution => value)
62+
product_type_based << record_with_os(klass, :product_type => value)
63+
product_name_based << record_with_os(klass, :product_name => value)
64+
end
65+
66+
# favor distribution over product_type
67+
fallback_records << record_with_os(klass, :distribution => "rhel-7", :product_type => "centos-7")
68+
# falls back to os.product_type if invalid os.distribution
69+
fallback_records << record_with_os(klass, :distribution => "undiscovered-7", :product_type => "rhel-7")
70+
# falls back to os.product_name
71+
fallback_records << record_with_os(klass, :distribution => "undiscovered-7", :product_name => "rhel-7")
72+
# falls back to hardware.guest_os
73+
fallback_records << record_with_os(klass, {:distribution => "undiscovered-7"}, {}, {:guest_os => "rhel-7"})
74+
# falls back to Host#user_assigned_os
75+
fallback_records << record_with_os(klass, {:distribution => "undiscovered-7"}, {:user_assigned_os => "rhel-7"})
76+
ensure
77+
# If the any of the above fails, make sure we re-enable callbacks so
78+
# subsequent specs don't fail trying to skip this callback when it
79+
# doesn't exist.
80+
operating_system_stub.set_callback(:save, :before, :update_platform_and_image_name)
81+
end
82+
83+
migrate
84+
85+
test_os_values.each.with_index do |(_, image_name), index|
86+
[distribution_based, product_type_based, product_name_based].each do |record_list|
87+
os_record = record_list[index]
88+
os_record.reload
89+
os_record = os_record.operating_system if os_record.respond_to?(:operating_system)
90+
91+
expect(os_record.image_name).to eq(image_name)
92+
expect(os_record.platform).to eq(image_name.split("_").first)
93+
end
94+
end
95+
96+
fallback_records.each(&:reload)
97+
98+
platform, image_name = %w[linux linux_redhat]
99+
fallback_records.each.with_index do |record, index|
100+
os_record = record
101+
os_record = os_record.operating_system if os_record.respond_to?(:operating_system)
102+
103+
# OperatingSystem records don't have a hardware relation, so this will be
104+
# a "unknown" OS
105+
platform, image_name = %w[unknown unknown] if index == 3 && klass == operating_system_stub
106+
107+
# Both ComputerSystem and VmOrTemplate don't have :user_assigned_os, so
108+
# these will return "unknown" instead of what we (tried to) set.
109+
platform, image_name = %w[unknown unknown] if index == 4 && klass != host_stub
110+
111+
expect(os_record.image_name).to eq(image_name)
112+
expect(os_record.platform).to eq(platform)
113+
end
114+
end
115+
116+
migration_context :up do
117+
it "adds the columns" do
118+
before_columns = operating_system_stub.columns.map(&:name)
119+
expect(before_columns).to_not include("platform")
120+
expect(before_columns).to_not include("image_name")
121+
122+
migrate
123+
124+
after_columns = operating_system_stub.columns.map(&:name)
125+
expect(after_columns).to include("platform")
126+
expect(after_columns).to include("image_name")
127+
end
128+
129+
it "updates OperatingSystem for Host records" do
130+
test_for_klass host_stub
131+
end
132+
133+
it "updates OperatingSystem for VmOrTemplate records" do
134+
test_for_klass vm_or_template_stub
135+
end
136+
137+
it "updates OperatingSystem for ComputerSystem records" do
138+
test_for_klass computer_system_stub
139+
end
140+
141+
it "updates orphaned OperatingSystem records" do
142+
test_for_klass operating_system_stub
143+
end
144+
end
145+
146+
migration_context :down do
147+
it "adds the columns" do
148+
before_columns = operating_system_stub.columns.map(&:name)
149+
expect(before_columns).to include("platform")
150+
expect(before_columns).to include("image_name")
151+
152+
migrate
153+
154+
after_columns = operating_system_stub.columns.map(&:name)
155+
expect(after_columns).to_not include("platform")
156+
expect(after_columns).to_not include("image_name")
157+
end
158+
end
159+
end

0 commit comments

Comments
 (0)