Skip to content

Commit 1238c6a

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 1238c6a

File tree

2 files changed

+316
-0
lines changed

2 files changed

+316
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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
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
33+
end
34+
end
35+
36+
@@os_map = [ # rubocop:disable Style/ClassVars
37+
["windows_generic", %w(winnetenterprise w2k3 win2k3 server2003 winnetstandard servernt)],
38+
["windows_generic", %w(winxppro winxp)],
39+
["windows_generic", %w(vista longhorn)],
40+
["windows_generic", %w(win2k win2000)],
41+
["windows_generic", %w(microsoft windows winnt)],
42+
["linux_ubuntu", %w(ubuntu)],
43+
["linux_chrome", %w(chromeos)],
44+
["linux_chromium", %w(chromiumos)],
45+
["linux_suse", %w(suse sles)],
46+
["linux_redhat", %w(redhat rhel)],
47+
["linux_fedora", %w(fedora)],
48+
["linux_gentoo", %w(gentoo)],
49+
["linux_centos", %w(centos)],
50+
["linux_debian", %w(debian)],
51+
["linux_coreos", %w(coreos)],
52+
["linux_esx", %w(vmnixx86 vmwareesxserver esxserver vmwareesxi)],
53+
["linux_solaris", %w(solaris)],
54+
["linux_generic", %w(linux)]
55+
]
56+
57+
# rubocop:disable Naming/VariableName
58+
def self.normalize_os_name(osName)
59+
findStr = osName.downcase.gsub(/[^a-z0-9]/, "")
60+
@@os_map.each do |a|
61+
a[1].each do |n|
62+
return a[0] unless findStr.index(n).nil?
63+
end
64+
end
65+
"unknown"
66+
end
67+
# rubocop:enable Naming/VariableName
68+
69+
# rubocop:disable Naming/VariableName
70+
def self.image_name(obj)
71+
osName = nil
72+
73+
# Select most accurate name field
74+
os = obj.operating_system
75+
if os
76+
# check the given field names for possible matching value
77+
osName = [:distribution, :product_type, :product_name].each do |field| # rubocop:disable Style/SymbolArray
78+
os_field = os.send(field)
79+
break(os_field) if os_field && OperatingSystem.normalize_os_name(os_field) != "unknown"
80+
end
81+
82+
# If the normalized name comes back as unknown, nil out the value so we can get it from another field
83+
if osName.kind_of?(String)
84+
osName = nil if OperatingSystem.normalize_os_name(osName) == "unknown"
85+
else
86+
osName = nil
87+
end
88+
end
89+
90+
# If the OS Name is still blank check the 'user_assigned_os'
91+
if osName.nil? && obj.respond_to?(:user_assigned_os) && obj.user_assigned_os
92+
osName = obj.user_assigned_os
93+
end
94+
95+
# If the OS Name is still blank check the hardware table
96+
if osName.nil? && obj.hardware && !obj.hardware.guest_os.nil?
97+
osName = obj.hardware.guest_os
98+
# if we get generic linux or unknown back see if the vm name is better
99+
norm_os = OperatingSystem.normalize_os_name(osName)
100+
if norm_os == "linux_generic" || norm_os == "unknown" # rubocop:disable Style/MultipleComparison
101+
vm_name = OperatingSystem.normalize_os_name(obj.name)
102+
return vm_name unless vm_name == "unknown"
103+
end
104+
end
105+
106+
# If the OS Name is still blank use the name field from the object given
107+
osName = obj.name if osName.nil? && obj.respond_to?(:name)
108+
109+
# Normalize name to match existing icons
110+
OperatingSystem.normalize_os_name(osName || "")
111+
end
112+
# rubocop:enable Naming/VariableName
113+
end
114+
115+
class VmOrTemplate < ActiveRecord::Base
116+
self.inheritance_column = :_type_disabled
117+
self.table_name = 'vms'
118+
119+
has_one :operating_system
120+
has_one :hardware
121+
end
122+
123+
class Host < ActiveRecord::Base
124+
self.inheritance_column = :_type_disabled
125+
126+
has_one :operating_system
127+
has_one :hardware
128+
end
129+
130+
class ComputerSystem < ActiveRecord::Base
131+
has_one :operating_system
132+
has_one :hardware
133+
end
134+
135+
class Hardware < ActiveRecord::Base
136+
end
137+
138+
def up
139+
add_column :operating_systems, :platform, :string
140+
add_column :operating_systems, :image_name, :string
141+
142+
say_with_time("Updating platform and image_name in OperatingSystems") do
143+
base_relation = OperatingSystem.all
144+
say_batch_started(base_relation.size)
145+
146+
base_relation.find_in_batches do |group|
147+
group.each(&:save)
148+
say_batch_processed(group.count)
149+
end
150+
end
151+
end
152+
153+
def down
154+
remove_column :operating_systems, :platform, :string
155+
remove_column :operating_systems, :image_name, :string
156+
end
157+
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)