src/app/models/instance.rb
#
# Copyright 2011 Red Hat, Inc.
#
# 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.
#
# == Schema Information
#
# Table name: instances
#
# id :integer not null, primary key
# external_key :string(255)
# name :string(1024) not null
# hardware_profile_id :integer not null
# frontend_realm_id :integer
# owner_id :integer
# pool_id :integer not null
# provider_account_id :integer
# instance_hwp_id :integer
# public_addresses :string(255)
# private_addresses :string(255)
# state :string(255)
# last_error :text
# lock_version :integer default(0)
# acc_pending_time :integer default(0)
# acc_running_time :integer default(0)
# acc_shutting_down_time :integer default(0)
# acc_stopped_time :integer default(0)
# time_last_pending :datetime
# time_last_running :datetime
# time_last_shutting_down :datetime
# time_last_stopped :datetime
# created_at :datetime
# updated_at :datetime
# deployment_id :integer
# assembly_xml :text
# instance_config_xml :text
# image_uuid :string(255)
# image_build_uuid :string(255)
# provider_image_uuid :string(255)
# provider_instance_id :string(255)
# user_data :string(255)
# uuid :string(255)
# secret :string(255)
#
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.
require 'util/deployable_xml'
require 'util/instance_config_xml'
class Instance < ActiveRecord::Base
acts_as_paranoid
class << self
include CommonFilterMethods
end
include Alberich::PermissionedObject
before_destroy :destroyable?
belongs_to :pool
belongs_to :pool_family
belongs_to :provider_account
belongs_to :deployment
belongs_to :hardware_profile
belongs_to :frontend_realm
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
belongs_to :instance_hwp
has_one :instance_key, :dependent => :destroy
has_many :events, :as => :source, :dependent => :destroy,
:order => 'events.id ASC'
has_many :instance_parameters, :dependent => :destroy
has_many :instance_matches, :dependent => :destroy
has_many :tasks, :as =>:task_target, :dependent => :destroy
after_create "assign_owner_roles(owner)"
validates_presence_of :pool_id
validates_presence_of :hardware_profile_id
#validates_presence_of :external_key
# TODO: can we do uniqueness validation on indirect association
# -- pool.account.provider
#validates_uniqueness_of :external_key, :scope => :provider_id
validates_presence_of :name
validates_uniqueness_of :name, :scope => [:pool_id, :deleted_at]
validates_length_of :name, :maximum => 1024
before_create :generate_uuid
before_create :set_pool_family
STATE_NEW = "new"
STATE_PENDING = "pending"
STATE_RUNNING = "running"
STATE_SHUTTING_DOWN = "shutting_down"
STATE_STOPPED = "stopped"
STATE_STOPPING = "stopping"
STATE_CREATE_FAILED = "create_failed"
STATE_ERROR = "error"
STATE_VANISHED = "vanished"
N_('new')
N_('pending')
N_('running')
N_('shutting_down')
N_('stopped')
N_('stopping')
N_('create_failed')
N_('error')
N_('vanished')
STATES = [STATE_NEW, STATE_PENDING, STATE_RUNNING,
STATE_SHUTTING_DOWN, STATE_STOPPED, STATE_CREATE_FAILED,
STATE_ERROR, STATE_VANISHED]
STOPPABLE_INACCESSIBLE_STATES = [STATE_NEW, STATE_PENDING, STATE_RUNNING, STATE_SHUTTING_DOWN]
# States that indicate some sort of failure/problem with an instance:
FAILED_STATES = [STATE_CREATE_FAILED, STATE_ERROR, STATE_VANISHED]
ACTIVE_FAILED_STATES = [STATE_ERROR, STATE_VANISHED]
scope :deployed, :conditions => { :state => [STATE_RUNNING, STATE_SHUTTING_DOWN] }
# FIXME: "pending" is misleading as it doesn't just cover STATE_PENDING
scope :pending, :conditions => { :state => [STATE_NEW, STATE_PENDING] }
scope :running, :conditions => { :state => [STATE_RUNNING] }
scope :in_new_state, :conditions => { :state => [STATE_NEW] }
scope :pending_or_deployed, :conditions => { :state => [STATE_NEW, STATE_PENDING, STATE_RUNNING, STATE_SHUTTING_DOWN] }
# FIXME: "failed" is misleading too...
scope :failed, :conditions => { :state => FAILED_STATES }
scope :stopped, :conditions => {:state => STATE_STOPPED}
scope :not_stopped, :conditions => "state <> 'stopped'"
scope :stoppable, :conditions => { :state => [STATE_PENDING, STATE_RUNNING] }
scope :stoppable_inaccessible, :conditions => { :state => STOPPABLE_INACCESSIBLE_STATES }
SEARCHABLE_COLUMNS = %w(name state)
validates_inclusion_of :state,
:in => STATES
validate :pool_and_account_enabled_validation, :on => :create
before_destroy :destroy_on_provider
# A user should only be able to update certain attributes, but the API may permit other attributes to be
# changed if called from another Aeolus component, so attr_protected isn't quite what we want:
USER_MUTABLE_ATTRS = ['name']
def perm_ancestors
ancestors = super
ancestors << deployment unless deployment.nil?
ancestors += [pool, pool_family]
end
def get_action_list(user=nil)
# return empty list rather than nil
# FIXME: not handling pending state now -- only current state
# FIXME: filter actions based on quota
# FIXME: not doing quota filtering now
InstanceTask.valid_actions_for_instance_state(state, self, user) || []
end
def pool_and_account_enabled_validation
errors.add(:pool, _('must be enabled')) unless pool and pool.enabled?
errors.add(:pool, _('has all associated Providers disabled')) if pool and pool.pool_family.all_providers_disabled?
end
def assembly_xml
@assembly_xml ||= AssemblyXML.new(self[:assembly_xml].to_s)
end
def instance_config_xml
if not self[:instance_config_xml].nil?
@instance_config_xml ||= InstanceConfigXML.new(self[:instance_config_xml].to_s)
end
end
# Provide method to check if requested action exists, so caller can decide
# if they want to throw an error of some sort before continuing
# (ie in service api)
def valid_action?(action)
get_action_list.include?(action)
end
def queue_action(user, action, data = nil)
return false unless get_action_list.include?(action)
task = InstanceTask.create!({ :user => user,
:task_target => self,
:action => action,
:args => data})
event = Event.create!(:source => self, :event_time => Time.now,
:summary => "#{action} action queued",
:status_code => "#{action}_queued")
task
end
# Returns the total time that this instance has been in the state
def total_state_time(state)
if !STATES.include?(state)
return _('Error, could not calculate state time: invalid state')
end
case state
when STATE_PENDING
if self.state == STATE_PENDING
return acc_pending_time + (Time.now - time_last_pending)
else
return acc_pending_time
end
when STATE_RUNNING
if self.state == STATE_RUNNING
return acc_running_time + (Time.now - time_last_running)
else
return acc_running_time
end
when STATE_SHUTTING_DOWN
if self.state == STATE_SHUTTING_DOWN
return acc_shutting_down_time + (Time.now - time_last_shutting_down)
else
return acc_shutting_down_time
end
when STATE_STOPPED
if self.state == STATE_STOPPED
return acc_stopped_time + (Time.now - time_last_stopped)
else
return acc_stopped_time
end
else
return _('Error, could not calculate state time: state is not monitored')
end
end
def create_auth_key
raise "instance provider_account is not set" unless self.provider_account
client = self.provider_account.connect
return nil unless client && client.feature?(:instances, :authentication_key)
if key = client.create_key(:name => key_name)
self.instance_key = InstanceKey.create!(:pem => key.pem, :name => key.id, :instance => self)
self.save!
end
end
def self.get_user_instances_stats(session, user)
stats = {
:running_instances => 0,
:stopped_instances => 0,
}
instances = []
pools = Pool.list_for_user(session, user, Alberich::Privilege::VIEW, Instance)
pools.each{|pool| pool.instances.each {|i| instances << i}}
instances.each do |i|
if i.state == Instance::STATE_RUNNING
stats[:running_instances] += 1
elsif i.state == Instance::STATE_STOPPED
stats[:stopped_instances] += 1
end
end
stats[:total_instances] = instances.size
return stats
end
USER_DATA_VERSION = "1"
OAUTH_SECRET_SEED = [('a'..'z'),('A'..'Z'),(0..9)].map{|i| i.to_a}.flatten
def self.generate_oauth_secret
# generates a string of between 40 and 50 characters consisting of a
# random selection of alphanumeric (upper and lower case) characters
(0..(rand(10) + 40)).map { OAUTH_SECRET_SEED[rand(OAUTH_SECRET_SEED.length)] }.join
end
def generate_user_data(config_server)
["#{USER_DATA_VERSION}|#{config_server.endpoint}|#{uuid}|#{secret}"].pack("m0").delete("\n")
end
def add_instance_config!(config_server, config)
self.user_data = generate_user_data(config_server)
self.instance_config_xml = config.to_s
save!
begin
config_server.send_config(config)
rescue Errno::ECONNREFUSED
raise _('Cannot connect to the Config Server')
end
end
def restartable?
# TODO: we don't support stateful instances yet, so it's `false` for the time being.
# In the meantime, we can use this method to write validation code for cases
# where does matter whether an instance is stateful or stateless.
false
end
def destroyable?
(state == STATE_CREATE_FAILED) || (state == STATE_STOPPED && ! restartable?) || (state == STATE_VANISHED)
end
def failed?
FAILED_STATES.include?(state)
end
def inactive?
(FAILED_STATES + [STATE_STOPPED]).include?(state)
end
def failed_or_running?
(FAILED_STATES + [STATE_RUNNING]).include?(state)
end
# represents states from which instance doesn't automatically transits
# into any other state, also checks that there is no queued 'start' action
# for stopped instance (rhevm, vpshere)
def finished?
return false if state == Instance::STATE_STOPPED && pending_or_successful_start?
inactive? || state == Instance::STATE_NEW
end
def self.list(order_field, order_dir)
#Instance.all(:include => [ :owner ],
# :order => (order_field || 'name') +' '+ (order_dir || 'asc'))
includes(:owner).order((order_field || 'name') +' '+ (order_dir || 'asc'))
end
def image_arch
# try to get architecture of the image associated with this instance
# for imported images template is empty -> architecture is not set,
# in this case we omit this check
image.template.os.arch
rescue => e
logger.warn "failed to get image architecture for instance '#{name}', skipping architecture check: #{e}"
nil
end
def includes_instance_match?(match)
instance_matches.any?{|m| m.equals?(match)}
end
def launch!(match, user, config_server, config)
# create a taskomatic task
task = InstanceTask.create!({:user => user,
:task_target => self,
:action => InstanceTask::ACTION_CREATE})
Taskomatic.create_instance!(task, match, config_server, config)
end
def self.csv_export(instances)
csvm = get_csv_class
csv_string = csvm.generate(:col_sep => ";", :row_sep => "\r\n") do |csv|
event_attributes = Event.new.attributes.keys.reject {|key| key if key == "created_at" || key == "updated_at"}
csv << event_attributes.map {|event| event.capitalize }
events = instances.map{|i| i.events}.flatten!
unless events.nil?
events.each do |event|
csv << event_attributes.map {|event_attribute| event[event_attribute] }
end
end
end
csv_string
end
scope :with_hardware_profile, lambda {
{:include => :hardware_profile}
}
def first_running?
not deployment.instances.deployed.any? {|i| i != self}
end
def stop(user)
do_operation(user, 'stop')
end
def start(user)
do_operation(user, 'start')
end
def stop_with_event(user)
stop(user)
true
rescue
self.events << Event.create(
:source => self,
:event_time => DateTime.now,
:status_code => 'instance_stop_failed',
:summary => "Failed to stop instance #{self.name}",
:description => $!.message
)
log_backtrace($!)
false
end
def reboot(user)
if tasks.where("action = :action AND time_submitted > :time_ago",
{:action => "reboot", :time_ago => 2.minutes.ago}).present?
raise _('reboot is already scheduled.')
else
do_operation(user, 'reboot')
end
end
def forced_stop(user)
self.state = STATE_STOPPED
save!
event = Event.create!(:source => self, :event_time => Time.now,
:summary => "Instance is not accessible, state changed to stopped",
:status_code => "forced_stop")
end
def deployed?
[STATE_RUNNING, STATE_SHUTTING_DOWN].include?(state)
end
def stopped?
[STATE_STOPPED].include?(state)
end
def pending?
[STATE_NEW, STATE_PENDING].include?(state)
end
def uptime
deployed? ? (Time.now - time_last_running) : 0
end
def stopped_after_creation?
last_task = tasks.last
state == Instance::STATE_STOPPED &&
# TODO: to keep backward compatibility with dc-core 0.5
# time_last_pending can't be used, because pending
# state was used instead of shutting_down in older dc-api version.
# https://bugzilla.redhat.com/show_bug.cgi?id=857542
#time_last_pending.to_i > time_last_running.to_i &&
last_task &&
[InstanceTask::ACTION_CREATE, InstanceTask::ACTION_START].include?(last_task.action) &&
last_task.created_at.to_i > time_last_running.to_i &&
# also make sure that the 'create' task was created after
# last deployment launch request - instance can be stopped
# since previous rollback+retry request
last_task.created_at.to_f > last_launch_time.to_f &&
provider_account &&
provider_account.provider.provider_type.goes_to_stop_after_creation?
end
def in_startable_state?
# returns true if this instance is part of a deployment and this deployment
# is in any of rollback modes
return true if deployment.nil?
Deployment::INSTANCE_STARTABLE_STATES.include?(deployment.state)
end
def requires_explicit_start?
# this is for RHEVM/VSPHERE instances where instance goes to 'stopped' state
# after creation - we check if it wasn't running before this stopped state
# and if we already did send start request to it
in_startable_state? && stopped_after_creation? &&
!pending_or_successful_start?
end
def stuck_in_stopping?
state == Instance::STATE_SHUTTING_DOWN &&
Time.now - time_last_shutting_down > 120 &&
provider_account &&
provider_account.provider.provider_type.goes_to_stop_after_creation?
end
PRESET_FILTERS_OPTIONS = [
{:title => "instances.preset_filters.other_than_stopped", :id => "other_than_stopped", :query => where("instances.state != ?", "stopped")},
{:title => "instances.preset_filters.new", :id => "new", :query => where("instances.state" => "new")},
{:title => "instances.preset_filters.pending", :id => "pending", :query => where("instances.state" => "pending")},
{:title => "instances.preset_filters.running", :id => "running", :query => where("instances.state" => "running")},
{:title => "instances.preset_filters.shutting_down", :id => "shutting_down", :query => where("instances.state" => "shutting_down")},
{:title => "instances.preset_filters.stopped", :id => "stopped", :query => where("instances.state" => "stopped")},
{:title => "instances.preset_filters.create_failed", :id => "create_failed", :query => where("instances.state" => "create_failed")},
{:title => "instances.preset_filters.error", :id => "error", :query => where("instances.state" => "error")},
{:title => "instances.preset_filters.vanished", :id => "vanished", :query => where("instances.state" => "vanished")}
]
def destroy_on_provider
if provider_account and provider_account.provider.provider_type.destroy_supported? and ![STATE_CREATE_FAILED, STATE_VANISHED].include?(state)
task = self.queue_action(self.owner, 'destroy')
raise _('Destroy cannot be performed on this instance.') unless task
Taskomatic.destroy_instance(task)
end
end
def self.stoppable_inaccessible_instances(instances)
failed_accounts = {}
instances.select do |i|
next unless STOPPABLE_INACCESSIBLE_STATES.include?(i.state)
next unless i.provider_account
unless failed_accounts.has_key?(i.provider_account.id)
failed_accounts[i.provider_account.id] = i.provider_account.connect.nil?
end
failed_accounts[i.provider_account.id]
end
end
def reset_attrs
# TODO: is it OK to upload params to config server multiple times?
# do we now keep instance config on config server by default?
update_attributes(:state => Instance::STATE_NEW,
:provider_account => nil,
:public_addresses => nil,
:private_addresses => nil)
instance_key.destroy if instance_key
end
def stop_request_queued?
task = tasks.last
task && task.action == InstanceTask::ACTION_STOP &&
task.state == Task::STATE_FINISHED
end
def disappears_after_stop_request?
provider_account &&
provider_account.provider.provider_type.stopped_instances_disappear?
end
private
def self.apply_search_filter(search)
return scoped unless search
where("lower(instances.name) LIKE :search OR lower(instances.state) LIKE :search", :search => "%#{search.downcase}%")
end
def key_name
"#{self.name}_#{Time.now.to_i}_key_#{self.object_id}".gsub(/[^a-zA-Z0-9\.\-]/, '_')
end
def generate_uuid
self[:uuid] = UUIDTools::UUID.timestamp_create.to_s
end
def set_pool_family
self[:pool_family_id] = pool.pool_family_id
end
def do_operation(user, operation)
task = self.queue_action(user, operation)
unless task
raise operation == 'stop' ? _('Stop is an invalid action.') : _('Reboot is an invalid action.')
end
Taskomatic.send("#{operation}_instance", task)
end
def pending_or_successful_start?
task = tasks.last
return false if task.nil? || task.action != 'start'
return true if task.state == Task::STATE_FINISHED
# it's possible that start request takes more than 30 secs on rhevm,
# but dbomatic kills child process after 30sec by default, so
# task may stay in 'pending' state. If task is in pending state for
# more than 2 mins, consider previous start request as failed.
task.state == Task::STATE_PENDING && Time.now - task.created_at < 120
end
def last_launch_time
return nil if deployment.nil?
event = deployment.events.find_last_by_status_code(:pending)
event.nil? ? nil : event.created_at
end
include CostEngine::Mixins::Instance
end