crowbar_framework/lib/schema_migration.rb
#
# Copyright 2011-2013, Dell
# Copyright 2013-2014, SUSE LINUX Products GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
module SchemaMigration
require "chef"
def self.run
BarclampCatalog.barclamps.each do |bc_name, details|
run_for_bc bc_name
end
end
def self.run_for_bc bc_name
return unless File.exist?("/var/lib/crowbar/install/crowbar-installed-ok")
db_host = Rails.configuration.database_configuration[Rails.env]["host"]
if ["localhost", "127.0.0.1"].include?(db_host) && !system("systemctl is-active postgresql")
raise "Cannot run schema migrations for #{bc_name}. Database server is not running"
end
template = get_template_for_barclamp bc_name
return if template.nil?
all_scripts = find_scripts_for_bc(bc_name)
return if all_scripts.empty?
begin
validator = CrowbarValidator.new(data_bags_path.join("template-#{bc_name}.schema"))
rescue StandardError => e
raise "Failed to load databag schema for #{bc_name}: #{e.message}"
end
props = Proposal.where(barclamp: bc_name)
props.each do |prop|
migrate_proposal(bc_name, validator, template, all_scripts, prop)
# Attempt to do migration for the matching committed proposal
# Note: we don't want to commit the proposal we just migrated, because it
# might have other uncommitted changes that are not wanted.
role_name = prop["id"].gsub("#{bc_name}-", "#{bc_name}-config-")
role = RoleObject.find_role_by_name(role_name)
unless role.nil?
migrate_role(bc_name, template, all_scripts, role)
end
service = ServiceObject.get_service(bc_name).new
service.post_schema_migration_callback(prop, role)
end
end
# set the schema revision number to revision
# This is useful to rerun migration scripts
def self.set_proposal_schema_revision(bc_name, revision)
proposal = Proposal.where(barclamp: bc_name).limit(1)[0]
proposal.properties["deployment"][bc_name]["schema-revision"] = revision.to_i
proposal.save
end
def self.migrate_proposal_from_json(bc_name, json)
template = get_template_for_barclamp bc_name
return if template.nil?
all_scripts = find_scripts_for_bc(bc_name)
return if all_scripts.empty?
attributes = json["attributes"][bc_name]
deployment = json["deployment"][bc_name]
# return migrated attributes and deployment
migrate_object(bc_name, template, all_scripts, attributes, deployment)
end
def self.get_barclamp_current_deployment_revison(bc_name)
template = get_template_for_barclamp bc_name
return [nil, nil] if template.nil?
proposals = Proposal.where(barclamp: bc_name)
latest_schema_revision = template["deployment"][bc_name]["schema-revision"]
latest_proposals_revision = proposals.collect do |prop|
{ name: prop.name, revision: prop["deployment"][bc_name]["schema-revision"] }
end
[latest_schema_revision, latest_proposals_revision]
end
private
def self.data_bags_path
Rails.root.join("..", "chef", "data_bags", "crowbar").expand_path
end
def self.get_migrate_dir(bc_name)
data_bags_path.join("migrate", bc_name)
end
# get the template for a given barclamp. the template is content of the
# json file from /opt/dell/chef/data_bags/crowbar/template-*bc-name*.json
def self.get_template_for_barclamp(bc_name)
begin
template = Proposal.new(barclamp: bc_name)
rescue Proposal::TemplateMissing
return nil
end
return nil if template.nil?
return nil if template["deployment"].nil?
return nil if template["deployment"][bc_name].nil?
template
end
private_class_method :get_template_for_barclamp
def self.find_scripts_for_bc(bc_name)
all_scripts = []
migrate_dir = get_migrate_dir(bc_name)
return all_scripts unless File.directory?(migrate_dir)
Dir.entries(migrate_dir).sort.each do |file|
next if [".", ".."].include?(file)
next if File.directory?(File.join(migrate_dir, file))
all_scripts << file
end
return all_scripts
end
def self.find_scripts_for_migration(bc_name, all_scripts, old_revision, new_revision)
scripts = []
migrate_dir = get_migrate_dir(bc_name)
return scripts if old_revision == new_revision
is_upgrade = old_revision < new_revision
if is_upgrade
start_revision = old_revision + 1
end_revision = new_revision
else
# downgrade function to reach revision X is in migration script X+1
start_revision = new_revision + 1
end_revision = old_revision
end
Range.new(start_revision, end_revision).each do |rev|
all_scripts.each do |script|
next unless script =~ /^0*#{rev}_.*\.rb$/
scripts << File.join(migrate_dir, script)
end
end
scripts.reverse! unless is_upgrade
return scripts
end
def self.run_script(script, is_upgrade, template_attributes, template_deployment, attributes, deployment)
# redefine upgrade/downgrade to no-op before each load
def upgrade ta, td, a, d
return a, d
end
def downgrade ta, td, a, d
return a, d
end
load script
if is_upgrade
attributes, deployment = upgrade(template_attributes, template_deployment, attributes, deployment)
else
attributes, deployment = downgrade(template_attributes, template_deployment, attributes, deployment)
end
end
def self.migrate_object(bc_name, template, all_scripts, attributes, deployment)
attributes ||= Mash.new
deployment ||= Mash.new
current_schema_revision = deployment["schema-revision"]
if current_schema_revision.nil?
current_schema_revision = 0
end
schema_revision = template["deployment"][bc_name]["schema-revision"]
if schema_revision.nil?
schema_revision = 0
end
return [nil, nil] if current_schema_revision == schema_revision
is_upgrade = current_schema_revision < schema_revision
scripts = self.find_scripts_for_migration(bc_name, all_scripts, current_schema_revision, schema_revision)
return [nil, nil] if scripts.empty?
scripts.each do |script|
# we only pass attributes and deployment to not encourage direct access
# to the proposal
begin
attributes, deployment = run_script(script, is_upgrade, template["attributes"][bc_name], template["deployment"][bc_name], attributes, deployment)
rescue StandardError => e
raise "error while executing migration script #{script}:\n#{e.message}"
end
end
deployment["schema-revision"] = schema_revision
return attributes, deployment
end
def self.migrate_proposal(bc_name, validator, template, all_scripts, proposal)
attributes = proposal["attributes"][bc_name]
deployment = proposal["deployment"][bc_name]
begin
(attributes, deployment) = migrate_object(bc_name, template, all_scripts, attributes, deployment)
rescue StandardError => e
raise "Failed to migrate proposal #{proposal.name} for #{bc_name}: #{e.message}"
end
return if attributes.nil? || deployment.nil?
proposal["attributes"][bc_name] = attributes
proposal["deployment"][bc_name] = deployment
errors = validator.validate(proposal.raw_data)
unless errors.empty?
error_lines = errors.map { |e| e.message }
error_lines.unshift "Failed to validate migrated proposal #{proposal.name} for #{bc_name}:"
raise error_lines.join("\n")
end
proposal.save
end
def self.migrate_role(bc_name, template, all_scripts, role)
attributes = role.default_attributes[bc_name]
deployment = role.override_attributes[bc_name]
begin
(attributes, deployment) = migrate_object(bc_name, template, all_scripts, attributes, deployment)
rescue StandardError => e
raise "Failed to migrate role #{role.name} for #{bc_name}: #{e.message}"
end
return if attributes.nil? || deployment.nil?
role.default_attributes[bc_name] = attributes
role.override_attributes[bc_name] = deployment
role.save
end
end