right_infrastructure_agent 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,117 @@
1
+ #--
2
+ # Copyright (c) 2011-2013 RightScale, Inc, All Rights Reserved Worldwide.
3
+ #
4
+ # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
5
+ # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
6
+ # reproduction, modification, or disclosure of this program is
7
+ # strictly prohibited. Any use of this program by an authorized
8
+ # licensee is strictly subject to the terms and conditions,
9
+ # including confidentiality obligations, set forth in the applicable
10
+ # License Agreement between RightScale.com, Inc. and the licensee.
11
+ #++
12
+
13
+ module RightScale
14
+
15
+ # This module is for use as a mixin in an HTTP controller that handles requests from
16
+ # global object replication sink to check whether replication is required for a global
17
+ # object owned by this server.
18
+ #
19
+ # This module expects the following to be defined:
20
+ # - logger - variable pointing to the standard logger in use
21
+ # - global_object_replicator_source - method returning type of replication source, e.g., 'library'
22
+ module GlobalObjectReplicatorSource
23
+
24
+ include RightScale::InfrastructureHelpers
25
+ include RightScale::ModelsHelper
26
+
27
+ # Compares checksum parameter with local state of a replicated table and sends backs a
28
+ # replicator/synchronize_replica_range with records that may need to be updated in the core DB
29
+ #
30
+ # @param :sink [String] Type of replication sink making request, e.g., "core" or "library"
31
+ # @param :class_name [String] Name of a class that acts_as_global_object
32
+ # @param :schema_version [Integer] Requested schema version for any synchronization records
33
+ # @param :checksum_type [String] Type of checksum: only 'global_object_version_sum'
34
+ # @param :max_id_at_start [Integer] State passed back to the core in the resulting
35
+ # synchronize_replica_range request
36
+ # @param :begin_id [Integer, NilClass] First row ID in the checksum range,
37
+ # or nil for all rows
38
+ # @param :end_id [Integer, NilClass] Last row ID in the checksum range,
39
+ # or nil for all rows
40
+ # @param :send_records_on_checksum_mismatch [Boolean] Whether any records should be
41
+ # returned in the resulting synchronize_replica_range request
42
+ # @param :checksum_value [Integer, NilClass] Checksum value calculated using the
43
+ # specific :checksum_type
44
+ # @param :shard_id [Integer] Shard ID of the sender
45
+ #
46
+ # @return [NilClass] nil
47
+ #
48
+ # @raise [ArgumentError] Unknown replication sink
49
+ # @raise [RightScale::Exceptions::QueryFailure] Failed to complete synchronization query
50
+ # @raise [RightScale::Exceptions::RetryableError] Query failed but may be retried
51
+ def verify_replica_range
52
+ global_object_class = constantize(params[:class_name])
53
+ schema_version = to_int_or_nil(params[:schema_version])
54
+ checksum_type = params[:checksum_type]
55
+ max_id_at_start = to_int_or_nil(params[:max_id_at_start])
56
+ begin_id = to_int_or_nil(params[:begin_id])
57
+ end_id = to_int_or_nil(params[:end_id])
58
+ send_records_on_checksum_mismatch = params[:send_records_on_checksum_mismatch]
59
+ checksum_value = params[:checksum_value]
60
+ shard_id = to_int_or_nil(params[:shard_id])
61
+
62
+ success = query("verify_replica_range for #{global_object_class.name} rows #{begin_id} " +
63
+ " - #{end_id}", :email_errors => true) do
64
+ records_to_synchronize = []
65
+
66
+ if (global_object_class.calculate_global_object_checksum(checksum_type, schema_version, begin_id, end_id) == checksum_value)
67
+ checksum_matched = true
68
+ logger.debug("GlobalObjectReplica: Verified #{global_object_class.name} global_object_version_sum " +
69
+ "for rows #{begin_id} - #{end_id}")
70
+ else
71
+ checksum_matched = false
72
+ logger.debug("GlobalObjectReplica: Verification of #{global_object_class.name} global_object_version_sum " +
73
+ "rows #{begin_id} - #{end_id} failed. Sending synchronization records.")
74
+ records_to_synchronize = if send_records_on_checksum_mismatch
75
+ global_object_class.get_initialization_hashes(schema_version, begin_id, end_id)
76
+ else
77
+ []
78
+ end
79
+ end
80
+
81
+ replicator = case params[:sink] # TODO Replace this with "#{params[:sink]}_replicator" once all replicator actors gone
82
+ when "core" then (global_object_replicator_source == "library") ? "replicator" : "wasabi_replicator_sink"
83
+ when "library" then "wasabi_replicator_sink"
84
+ else raise ArgumentError, "Unknown replication sink: #{params[:sink].inspect}"
85
+ end
86
+ payload = {
87
+ :source => global_object_replicator_source,
88
+ :class_name => global_object_class.name,
89
+ :checksum_type => checksum_type,
90
+ :max_id_at_start => max_id_at_start,
91
+ :begin_id => begin_id,
92
+ :end_id => end_id,
93
+ :checksum_matched => checksum_matched,
94
+ :records_to_synchronize => records_to_synchronize,
95
+ :has_more => global_object_class.max_id > end_id }
96
+
97
+ EM.next_tick do
98
+ # Execute this request on next_tick since, depending on the configuration, the router could route
99
+ # this request directly via HTTP and there would be no break in the chain of replication requests
100
+ begin
101
+ RightScale::RightHttpClient.push("/#{replicator}/synchronize_replica_range", payload, :scope => {:shard => shard_id})
102
+ rescue Exception => e
103
+ logger.error(format_error("Failed to synchronize_replica_range for class #{global_object_class.name}", e))
104
+ end
105
+ end
106
+
107
+ true
108
+ end
109
+
110
+ raise RightScale::Exceptions::QueryFailure.new(@last_error) unless success
111
+
112
+ render_nothing
113
+ end
114
+
115
+ end # GlobalObjectReplicatorSource
116
+
117
+ end # RightScale
@@ -0,0 +1,88 @@
1
+ #--
2
+ ## Copyright (c) 2014 RightScale, Inc, All Rights Reserved Worldwide.
3
+ #
4
+ # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
5
+ # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
6
+ # reproduction, modification, or disclosure of this program is
7
+ # strictly prohibited. Any use of this program by an authorized
8
+ # licensee is strictly subject to the terms and conditions,
9
+ # including confidentiality obligations, set forth in the applicable
10
+ # License Agreement between RightScale.com, Inc. and
11
+ # the licensee.
12
+ #++
13
+
14
+ require 'global_session'
15
+
16
+ module RightScale
17
+
18
+ # Authorization client for infrastructure agents
19
+ class InfrastructureAuthClient < AuthClient
20
+
21
+ include RightScale::InfrastructureHelpers
22
+
23
+ # Initialized authorization client
24
+ #
25
+ # @param [String] client_name of application using this client
26
+ # @param [String] router_url including base path for accessing RightNet router
27
+ # @param [String] config_dir path to global session configuration information
28
+ #
29
+ # @options option [String] :agent_id identifying this agent in AgentIdentity format
30
+ def initialize(client_name, router_url, config_dir, options = {})
31
+ @client_name = client_name
32
+ @router_url = router_url
33
+ @agent_id = options[:agent_id]
34
+
35
+ config = GlobalSession::Configuration.new(File.join(config_dir, "global_session.yml"), ENV["RAILS_ENV"])
36
+ @global_session_dir = constantize(config["directory"]).new(config, File.join(config_dir, "authorities"))
37
+ @global_session_timeout = (config["timeout"] * 8) / 10
38
+
39
+ @state = :authorized
40
+ reset_stats
41
+ end
42
+
43
+ # Headers to be added to HTTP request
44
+ # Include authorization header by default
45
+ #
46
+ # @return [Hash] headers to be added to request header
47
+ #
48
+ # @raise [Exceptions::Unauthorized] not authorized
49
+ # @raise [Exceptions::RetryableError] authorization expired, but retry may succeed
50
+ def headers
51
+ check_authorized
52
+ auth_header
53
+ end
54
+
55
+ # Headers to be added to HTTP request
56
+ #
57
+ # @return [Hash] headers to be added to request header
58
+ #
59
+ # @raise [Exceptions::Unauthorized] not authorized
60
+ # @raise [Exceptions::RetryableError] authorization expired, but retry may succeed
61
+ def auth_header
62
+ check_authorized
63
+ {"Authorization" => "Bearer #{infrastructure_session}"}
64
+ end
65
+
66
+ protected
67
+
68
+ # Retrieve or create global session for infrastructure agent
69
+ # Cache session and only recreate when times out
70
+ #
71
+ # @return [String] infrastructure session
72
+ def infrastructure_session
73
+ now = Time.now
74
+ if @cached_infrastructure_session && (now - @cached_infrastructure_session[:created_at]) < @global_session_timeout
75
+ @cached_infrastructure_session[:session]
76
+ else
77
+ global_session = GlobalSession::Session.new(@global_session_dir)
78
+ global_session["infrastructure"] = @client_name
79
+ global_session["agent"] = @agent_id if @agent_id
80
+ session = global_session.to_s
81
+ @cached_infrastructure_session = {:session => session, :created_at => now}
82
+ session
83
+ end
84
+ end
85
+
86
+ end # InfrastructureAuthClient
87
+
88
+ end # RightScale
@@ -0,0 +1,85 @@
1
+ #--
2
+ # Copyright (c) 2011-2013 RightScale, Inc, All Rights Reserved Worldwide.
3
+ #
4
+ # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
5
+ # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
6
+ # reproduction, modification, or disclosure of this program is
7
+ # strictly prohibited. Any use of this program by an authorized
8
+ # licensee is strictly subject to the terms and conditions,
9
+ # including confidentiality obligations, set forth in the applicable
10
+ # License Agreement between RightScale.com, Inc. and the licensee.
11
+ #++
12
+
13
+ module RightScale
14
+
15
+ module InfrastructureHelpers
16
+
17
+ # Render result as nothing
18
+ #
19
+ # @return [TrueClass] Always return true
20
+ def render_nothing
21
+ render(:nothing => true, :status => :no_content)
22
+ true
23
+ end
24
+
25
+ # Format log message for an error
26
+ #
27
+ # @param description [String] Error description that is placed first in output
28
+ # @param exception [Exception, String] Exception or exception message
29
+ # @param backtrace [Symbol] Exception backtrace extent: :no_trace, :caller, or :trace,
30
+ # defaults to :caller
31
+ #
32
+ # @return [String] Formatted error
33
+ def format_error(description, exception, backtrace = :caller)
34
+ RightScale::Log.format(description, exception, backtrace)
35
+ end
36
+
37
+ # Convert string parameter to integer if not nil or empty
38
+ #
39
+ # @param param [String, NilClass] Parameter value
40
+ #
41
+ # @return [Integer, NilClass] Integer form of parameter, or nil if not a string
42
+ def to_int_or_nil(param)
43
+ (param.nil? || (param.respond_to?(:empty?) && param.empty?)) ? nil : param.to_i
44
+ end
45
+
46
+ # Convert string parameter to boolean
47
+ #
48
+ # @param param [String, TrueClass, FalseClass, NilClass] Parameter value
49
+ #
50
+ # @return [TrueClass, FalseClass] Boolean form of parameter
51
+ def to_bool(param)
52
+ ![nil, false, "false"].include?(param)
53
+ end
54
+
55
+ # Tries to find a constant with the name specified in the argument string:
56
+ #
57
+ # "Module".constantize # => Module
58
+ # "Test::Unit".constantize # => Test::Unit
59
+ #
60
+ # The name is assumed to be the one of a top-level constant, no matter whether
61
+ # it starts with "::" or not. No lexical context is taken into account:
62
+ #
63
+ # C = 'outside'
64
+ # module M
65
+ # C = 'inside'
66
+ # C # => 'inside'
67
+ # "C".constantize # => 'outside', same as ::C
68
+ # end
69
+ #
70
+ # NameError is raised when the name is not in CamelCase or the constant is
71
+ # unknown.
72
+ def constantize(camel_cased_word)
73
+ names = camel_cased_word.split('::')
74
+ names.shift if names.empty? || names.first.empty?
75
+
76
+ constant = Object
77
+ names.each do |name|
78
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
79
+ end
80
+ constant
81
+ end
82
+
83
+ end # InfrastructureHelpers
84
+
85
+ end # RightScale
@@ -0,0 +1,137 @@
1
+ # Copyright (c) 2009-2014 RightScale, Inc, All Rights Reserved Worldwide.
2
+ #
3
+ # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
4
+ # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
5
+ # reproduction, modification, or disclosure of this program is
6
+ # strictly prohibited. Any use of this program by an authorized
7
+ # licensee is strictly subject to the terms and conditions,
8
+ # including confidentiality obligations, set forth in the applicable
9
+ # License Agreement between RightScale.com, Inc. and the licensee.
10
+
11
+ module RightScale
12
+
13
+ class LoginPolicyFactory
14
+ # Regexp used to substitute sequences of non-username-friendly characters with a humble underscore.
15
+ # Technically we are more strict than the POSIX standard allows (a username could have any character
16
+ # in his name that is a valid filename character), but we want to have sane *and* legal usernames.
17
+ INVALID_USERNAME_CHARS = /[^a-z0-9_]+/
18
+
19
+ # Number of seconds before a public key is considered old
20
+ OLD_PUBLIC_KEY_BOUNDARY_AGE = 24 * 60 * 60
21
+
22
+ # Create LoginPolicy for given account, including all users that are authorized
23
+ # to login, their public keys, etc. The policy is based on Permissions, UserCredentials
24
+ # and Settings of the account and is uniform across all instances of the account.
25
+ # Optionally omit public keys that have not been updated recently with the expectation
26
+ # that the receiver of this policy instead uses the fingerprint to determine
27
+ # whether it needs to request the update.
28
+ #
29
+ # === Parameters
30
+ # account(Account):: Account that login policy should be built for
31
+ # terse(Boolean):: Whether to omit a public key from the policy if it was created
32
+ # more than OLD_PUBLIC_KEY_BOUNDARY_AGE seconds ago
33
+ #
34
+ # === Return
35
+ # policy(RightScale::LoginPolicy) Policy specifying which users may login & some related metadata
36
+ def self.policy_for_account(account, terse = false)
37
+ # Find all users who are allowed and able to do managed login. Simultaneously build some indices
38
+ # of timestamps authorizing perms, which will be used presently to create the policy object.
39
+
40
+ # Build the policy and its list of users
41
+ old_cred_boundary = Time.now - OLD_PUBLIC_KEY_BOUNDARY_AGE
42
+ timestamps_max = Time.at(0)
43
+ policy = LoginPolicy.new
44
+ policy.exclusive = account.setting('managed_login_mandatory')
45
+
46
+ # Do this as a closure around the timestamp and cred boundaries instead
47
+ # of a method to avoid having to pass them back and forth constantly.
48
+ create_login_user = Proc.new do |u, is_superuser|
49
+ cred_updated_at = Time.parse(u.cred_updated_at)
50
+ timestamps_max = [timestamps_max, Time.parse(u.perm_updated_at), cred_updated_at].max
51
+
52
+ # Currently there is only one public key per user but in the future there may be more
53
+ # There should always be the exact number of fingerprints
54
+ public_keys = [u.cred_public_value]
55
+ processed_keys = []
56
+ public_key_fingerprints = [u.cred_public_fingerprint]
57
+ processed_fingerprints = []
58
+
59
+ if terse && cred_updated_at < old_cred_boundary
60
+ processed_keys = public_keys.map { |k| nil }
61
+ processed_fingerprints = public_key_fingerprints
62
+ else
63
+ # Post-process the public keys to make sure they are well-formed AND contain a comment
64
+ # at the end of the line. Technically the comment is optional, but a bug in RightImage
65
+ # 5.1.1 - 5.6 makes the comment mandatory else the instance has heinous problems
66
+ # when trying to update its managed login policy.
67
+ public_keys.zip(public_key_fingerprints).each do |(k, f)|
68
+ if (components = LoginPolicy.parse_public_key(k))
69
+ components[3] ||= u.email
70
+ processed_keys << components.compact.join(' ')
71
+ processed_fingerprints << f
72
+ else
73
+ # In case this is some malformed or other weird key, omit its value
74
+ end
75
+ end
76
+ end
77
+
78
+ uuid = u.id.to_s
79
+ username = u.email.split('@', 2).first.downcase.gsub(INVALID_USERNAME_CHARS,'_')
80
+ common_name = u.email
81
+ superuser = is_superuser
82
+ expires_at = u.perm_deleted_at.nil? ? nil : Time.parse(u.perm_deleted_at)
83
+
84
+ LoginUser.new(uuid, username, public_key = nil, common_name, superuser, expires_at, processed_keys,
85
+ profiled_data = nil, processed_fingerprints)
86
+ end
87
+
88
+ # Get list of all super users
89
+ superusers = users_with_role(account, 'server_superuser')
90
+
91
+ # Build all users conditionally on inclusion of superusers list
92
+ policy.users = users_with_role(account, 'server_login').map do |u|
93
+ create_login_user.call(u, (superusers && superusers.include?(u)) || false )
94
+ end
95
+
96
+ policy.created_at = timestamps_max
97
+
98
+ return policy
99
+ end
100
+
101
+ # Create LoginPolicy for given instance, including all users that are authorized
102
+ # to login, their public keys, etc. Currently this just delegates to #policy_for_account
103
+ # since all instances of a given account have the same policy at a given time. This may
104
+ # change in the future, however.
105
+ #
106
+ # === Parameters
107
+ # instance(Ec2Instance):: Instance that login policy should be built for
108
+ # audit_id(Integer):: Audit identifier
109
+ # terse(Boolean):: Whether to omit a public key from the policy if it was created
110
+ # more than OLD_PUBLIC_KEY_BOUNDARY_AGE seconds ago
111
+ #
112
+ # === Return
113
+ # policy(RightScale::LoginPolicy) Policy specifying which users may login & some related metadata
114
+ def self.policy_for_instance(instance, audit_id, terse = false)
115
+ policy = policy_for_account(instance.account, terse)
116
+ policy.audit_id = audit_id
117
+ return policy
118
+ end
119
+
120
+ def self.users_with_role(account, role)
121
+ User.all( :select => "`users`.*, `user_credentials`.public_value as cred_public_value, " +
122
+ "`user_credentials`.updated_at as cred_updated_at, " +
123
+ "`user_credentials`.public_value_fingerprint as cred_public_fingerprint, " +
124
+ "`permissions`.updated_at as perm_updated_at, " +
125
+ "`permissions`.deleted_at as perm_deleted_at",
126
+ :joins => [:user_credential, :permissions],
127
+ :conditions => ["permissions.account_id = ? " +
128
+ "AND permissions.role_id = ? " +
129
+ "AND (permissions.deleted_at IS NULL OR permissions.deleted_at > ?) " +
130
+ "AND user_credentials.public_value IS NOT NULL " +
131
+ "AND user_credentials.public_value <> ''",
132
+ account.id, Role[role], Time.now.utc ])
133
+ end
134
+
135
+ end # LoginPolicyFactory
136
+
137
+ end # RightScale
@@ -0,0 +1,483 @@
1
+ # Copyright (c) 2009-2011 RightScale, Inc, All Rights Reserved Worldwide.
2
+ #
3
+ # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
4
+ # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
5
+ # reproduction, modification, or disclosure of this program is
6
+ # strictly prohibited. Any use of this program by an authorized
7
+ # licensee is strictly subject to the terms and conditions,
8
+ # including confidentiality obligations, set forth in the applicable
9
+ # License Agreement between RightScale.com, Inc. and
10
+ # the licensee.
11
+
12
+ module RightScale
13
+
14
+ # Helper methods for accessing ActiveRecord models
15
+ # They are only usable when executing in a Rails environment
16
+ module ModelsHelper
17
+
18
+ # Pattern in exception message from trigger in core when account is not in current shard
19
+ WRONG_SHARD_ERROR = "ERROR_UPDATE_NOT_ALLOWED does not exist"
20
+
21
+ # Retry exception message for when get wrong shard exception
22
+ WRONG_SHARD_RETRY_MESSAGE = "Account temporarily unavailable"
23
+
24
+ # Retry exception message for when account disabled due to shard migration
25
+ DISABLED_SHARD_RETRY_MESSAGE = "Account temporarily unavailable"
26
+
27
+ # Default retry exception message
28
+ DEFAULT_RETRY_MESSAGE = "RightScale database temporarily unavailable"
29
+
30
+ # Mysql and ActiveRecord case-insensitive exception message content that if present causes the
31
+ # error to be propagated to the requester as retryable after max retries is exceeded internally
32
+ RETRYABLE_ERRORS = ["deadlock found", "lock wait timeout", "can't connect to", "has gone away", WRONG_SHARD_ERROR]
33
+
34
+ # Query database using retry on failure and reconnect handling
35
+ # Audit and/or log error if given block returns nil or raises, return block result otherwise
36
+ # Store any error message in @last_error
37
+ #
38
+ # === Parameters
39
+ # description(String):: Description of query action that is used in error messages
40
+ # options(Hash):: Query options:
41
+ # :audit(AuditEntry):: Audit entry used to append error message if any
42
+ # :include_backtrace_in_last_error(Boolean):: Whether to pass :trace to Log.format for exceptions
43
+ # :email_errors(Boolean):: Whether to send email for errors
44
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
45
+ #
46
+ # === Block
47
+ # Accesses MySQL and returns result, required
48
+ #
49
+ # === Return
50
+ # (Object|nil):: Value returned by block, or nil if failed
51
+ def query(description, options = {}, &blk)
52
+ begin
53
+ @last_error = nil
54
+ run_query(options, &blk)
55
+ rescue Exception => e
56
+ retryable = e.is_a?(RightScale::Exceptions::RetryableError)
57
+ description = "Failed to #{description}" + (retryable ? " but retryable" : "")
58
+ Log.error(description, e, :trace)
59
+ if options[:include_backtrace_in_last_error]
60
+ @last_error = Log.format(description, e, :trace)
61
+ else
62
+ @last_error = Log.format(description, e)
63
+ end
64
+ options[:audit].append(AuditFormatter.error(@last_error)) if options[:audit]
65
+
66
+ if(options[:email_errors])
67
+ ExceptionMailer.deliver_notification(description, e.message, e)
68
+ end
69
+
70
+ raise if retryable
71
+ nil
72
+ end
73
+ end
74
+
75
+ # Run database query block
76
+ # When catch retryable MySQL and ActiveRecord errors, rerun block, retry up to 3 times
77
+ #
78
+ # === Parameters
79
+ # options(Hash):: Query options:
80
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
81
+ #
82
+ # === Block
83
+ # Accesses MySQL and returns result, required
84
+ #
85
+ # === Return
86
+ # res(Object|nil):: Value returned by given block, or nil if desired data was not found
87
+ #
88
+ # === Raise
89
+ # RuntimeError:: Block is missing
90
+ # RetryableError: If exceed max retries and :retryable_error option enabled
91
+ # Also re-raises any query block exceptions
92
+ def run_query(options = {})
93
+ raise 'Missing block' unless block_given?
94
+ res = nil
95
+ disconnected = true
96
+ while disconnected do
97
+ retries = 0
98
+ begin
99
+ res = yield
100
+ disconnected = false
101
+ rescue ActiveRecord::RecordNotFound
102
+ res = nil
103
+ disconnected = false
104
+ rescue Exception => e
105
+ if is_retryable_error?(e, local = true)
106
+ if retries >= 3
107
+ Log.warning("Aborting query after 3 failed retries")
108
+ if options[:retryable_error] && is_retryable_error?(e)
109
+ if e.message =~ /#{WRONG_SHARD_ERROR}/i
110
+ raise RightScale::Exceptions::RetryableError.new(WRONG_SHARD_RETRY_MESSAGE, e)
111
+ else
112
+ raise RightScale::Exceptions::RetryableError.new(DEFAULT_RETRY_MESSAGE, e)
113
+ end
114
+ else
115
+ raise # re-raise the exception
116
+ end
117
+ else
118
+ retries += 1
119
+ Log.error("Failed running MySQL query", e, :trace)
120
+ Log.info("Retrying query...")
121
+ retry
122
+ end
123
+ else
124
+ raise # re-raise the exception
125
+ end
126
+ end
127
+ end
128
+ res
129
+ end
130
+
131
+ # Is given exception a MySQL exception worth retrying, e.g., a deadlock or timeout?
132
+ #
133
+ # === Parameter
134
+ # e(Exception):: Exception to be tested
135
+ # local(Boolean):: Whether making this decision for local or external consumption
136
+ #
137
+ # === Return
138
+ # (Boolean):: true if worth retrying, otherwise false or nil
139
+ def is_retryable_error?(e, local = false)
140
+ (e.is_a?(MysqlError) || e.is_a?(ActiveRecord::ActiveRecordError)) && (local ||
141
+ (RETRYABLE_ERRORS + ActiveRecord::ConnectionAdapters::MysqlAdapter::LOST_CONNECTION_ERROR_MESSAGES).find { |m| e.message =~ /#{m}/i })
142
+ end
143
+
144
+ # Retrieve existing audit or create new one
145
+ #
146
+ # === Parameters
147
+ # instance(Instance):: Instance used as auditee
148
+ # summary(String):: New audit summary, default to empty string
149
+ # detail(String):: New audit detail, default to empty string
150
+ # account(Account):: Account associated with audit, default to instance's account
151
+ # user(User):: User who caused the activity
152
+ # options(Hash):: Query options:
153
+ # :audit_id(Integer):: Audit entry id if audit is to be retrieved
154
+ # :agent_id(String):: Serialized agent identity if audit is to be created (:agent_identity
155
+ # is an alternative deprecated option name)
156
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
157
+ #
158
+ # === Return
159
+ # (AuditEntry):: Audit entry model
160
+ #
161
+ # === Raise
162
+ # ArgumentError:: If neither :audit_id nor :agent_id is specified
163
+ def retrieve_or_create_audit(instance, summary = '', detail = '', account = instance.account, user = nil, options = {})
164
+ if options[:audit_id] && options[:audit_id] != -1
165
+ retrieve("audit with id #{options[:audit_id]}") { audit_entry(options[:audit_id], options) }
166
+ elsif (agent_id = options[:agent_id] || options[:agent_identity])
167
+ query("create audit for instance agent #{agent_id}", options) do
168
+ AuditEntry.create!( {:auditee => instance, :summary => summary, :detail => detail,
169
+ :account => account, :user => user} )
170
+ end
171
+ else
172
+ raise ArgumentError, "Must specify audit ID or agent ID"
173
+ end
174
+ end
175
+
176
+ # Retrieve existing user or get default user stub
177
+ # Store any error message in @last_error
178
+ #
179
+ # === Parameters
180
+ # instance(Instance):: Instance for account
181
+ # options(Hash):: Query options:
182
+ # :user_id(Integer):: User id or 0 meaning use stub
183
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
184
+ #
185
+ # === Return
186
+ # current_user(User):: User retrieved or stubbed
187
+ def retrieve_or_default_user(instance, options = {})
188
+ user_id = (options[:user_id] || 0).to_i # ensure user id is non-nil integer
189
+ if user_id == 0
190
+ current_user = User.new(:email => 'alerter@rightscale.com')
191
+ current_user.id = 0
192
+ else
193
+ account = instance.account
194
+ current_user = query("User in account #{account} with id #{user_id}", options) do
195
+ account.users.detect { |u| u.id == user_id }
196
+ end
197
+ end
198
+ current_user
199
+ end
200
+
201
+ # Retrieve database object using the ModelsHelper functions below that themselves use #run_query,
202
+ # e.g., retrieve("recipe for instance", :audit => audit) { recipe(id, :retryable_error => true }
203
+ # Audit and/or log error if block returns nil or raises exception, otherwise return block result
204
+ # Store any error message in @last_error
205
+ #
206
+ # === Parameters
207
+ # description(String):: Description of object that is used in error messages
208
+ # options(Hash):: Query options:
209
+ # :audit(AuditEntry):: Audit entry used to append error message if any
210
+ # :log(Boolean):: Whether to log message when object does not exist
211
+ #
212
+ # === Block
213
+ # Performs query to retrieve object, required
214
+ # The block should retrieve using other ModelsHelper functions like #instance or #account
215
+ # so that #run_query is applied rather than accessing models directly in the block
216
+ #
217
+ # === Return
218
+ # item(Object):: Value returned by block, or nil if not found or failed
219
+ #
220
+ # === Raise
221
+ # RuntimeError:: Block is missing
222
+ def retrieve(description, options = {})
223
+ raise 'Missing block' unless block_given?
224
+ retryable = nil
225
+ begin
226
+ @last_error = nil
227
+ unless item = yield
228
+ @last_error = "Could not find #{description}"
229
+ Log.warning(@last_error) if options[:log]
230
+ end
231
+ rescue Exception => e
232
+ retryable = e if e.is_a?(RightScale::Exceptions::RetryableError)
233
+ description = "Failed to retrieve #{description}" + (retryable ? " but retryable" : "")
234
+ Log.error(description, e, e.is_a?(RightScale::Exceptions) ? :caller : :trace)
235
+ @last_error = Log.format(description, e)
236
+ item = nil
237
+ end
238
+ options[:audit].append(AuditFormatter.error(@last_error)) if options[:audit] && item.nil? && @last_error
239
+ raise retryable if retryable
240
+ item
241
+ end
242
+
243
+ # Retrieve InstanceApiToken model with given id
244
+ #
245
+ # === Parameters
246
+ # id(Integer):: Id of InstanceApiToken to be retrieved
247
+ # options(Hash):: Query options:
248
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
249
+ #
250
+ # === Return
251
+ # (InstanceApiToken|nil):: Corresponding API token, or nil if no token with this id exists
252
+ def instance_token(id, options = {})
253
+ run_query(options) { InstanceApiToken.find(id) }
254
+ end
255
+
256
+ # Retrieve instance model with given API token id
257
+ #
258
+ # === Parameters
259
+ # token_id(Integer):: Id of InstanceApiToken of instance to be retrieved
260
+ # options(Hash):: Query options:
261
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
262
+ #
263
+ # === Return
264
+ # instance(Ec2Instance|Instance|nil):: Corresponding instance, or nil if no token with
265
+ # this id exists
266
+ #
267
+ # === Raise
268
+ # RetryableError:: If account disabled for shard migration, or if exceed maximum retries
269
+ def instance(token_id, options = {})
270
+ token = instance_token(token_id, options)
271
+ token && (instance = token.instance)
272
+ if instance && instance.account.disabled_in_every_shard?
273
+ raise RightScale::Exceptions::RetryableError.new(DISABLED_SHARD_RETRY_MESSAGE)
274
+ end
275
+ instance
276
+ end
277
+
278
+ # Get instance from API token id
279
+ # Cache all tokens retrieved
280
+ #
281
+ # === Parameters
282
+ # token_id(Integer):: API token id
283
+ # options(Hash):: Query options:
284
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
285
+ #
286
+ # === Return
287
+ # instance(Instance):: Corresponding instance
288
+ #
289
+ # === Raise
290
+ # RetryableError:: If account disabled for shard migration, or if exceed maximum retries
291
+ # RightScale::Exceptions::Application:: If InstanceApiToken or Instance not found
292
+ def instance_from_token_id(token_id, options = {})
293
+ @tokens ||= {}
294
+ if instance = @tokens[token_id]
295
+ run_query(options) { instance.reload }
296
+ else
297
+ # Not in cache, look it up
298
+ instance_api_token = instance_token(token_id)
299
+ raise RightScale::Exceptions::Application, "Instance token with id '#{token_id}' not found" unless instance_api_token
300
+ instance = instance_api_token.instance
301
+ raise RightScale::Exceptions::Application, "Instance with token id '#{token_id}' not found" unless instance
302
+ @tokens[token_id] = instance
303
+ end
304
+ if instance && instance.account.disabled_in_every_shard?
305
+ raise RightScale::Exceptions::RetryableError.new(DISABLED_SHARD_RETRY_MESSAGE)
306
+ end
307
+ instance
308
+ end
309
+
310
+ # Get instance model corresponding to instance agent with given identity
311
+ #
312
+ # === Parameters
313
+ # identity(String):: Serialized instance agent identity
314
+ # options(Hash):: Query options:
315
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
316
+ #
317
+ # === Return
318
+ # (Instance):: Corresponding instance
319
+ #
320
+ # === Raise
321
+ # ArgumentError:: Invalid agent identity
322
+ def instance_from_agent_id(agent_id, options = {})
323
+ raise ArgumentError, "Invalid agent identity" unless AgentIdentity.valid?(agent_id)
324
+ instance_from_token_id(AgentIdentity.parse(agent_id).base_id, options)
325
+ end
326
+
327
+ # Retrieve account with given id
328
+ #
329
+ # === Parameters
330
+ # id(Integer):: Id of account to be retrieved
331
+ # options(Hash):: Query options:
332
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
333
+ #
334
+ # === Return
335
+ # (Account|nil):: Corresponding account, or nil if no account with this id exists
336
+ def account(id, options = {})
337
+ run_query(options) { Account.find(id) }
338
+ end
339
+
340
+ # Retrieve AuditEntry with given id
341
+ #
342
+ # === Parameters
343
+ # id(Integer):: Id of AuditEntry to be retrieved
344
+ # options(Hash):: Query options:
345
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
346
+ #
347
+ # === Return
348
+ # (AuditEntry|nil):: Corresponding setting, or nil if no setting with this id exists
349
+ def audit_entry(id, options = {})
350
+ run_query(options) { AuditEntry.find(id) }
351
+ end
352
+
353
+ # Retrieve RightScript with given id
354
+ #
355
+ # === Parameters
356
+ # id(Integer):: Id of RightScript to be retrieved
357
+ # options(Hash):: Query options:
358
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
359
+ #
360
+ # === Return
361
+ # (RightScript|nil):: Corresponding RightScript or nil if no RightScript with this id exists
362
+ def right_script(id, options = {})
363
+ run_query(options) { RightScript.find(id) }
364
+ end
365
+
366
+ # Retrieve RightScript with given name on given instance
367
+ #
368
+ # === Parameters
369
+ # name(String):: Name of RightScript that should be retrieved
370
+ # instance(Instance):: Instance on which RightScript is defined
371
+ # options(Hash):: Query options:
372
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
373
+ #
374
+ # === Return
375
+ # script(RightScript):: Corresponding RightScript
376
+ def right_script_from_name(name, instance, options = {})
377
+ script = nil
378
+ template = run_query(options) { instance.server_template }
379
+ if template
380
+ script = run_query(options) { template.right_scripts.find_by_name(name) }
381
+ end
382
+ script
383
+ end
384
+
385
+ # Retrieve Chef recipe with given id
386
+ #
387
+ # === Parameters
388
+ # id(Integer):: Id of ServerTemplateChefRecipe to be retrieved
389
+ # options(Hash):: Query options:
390
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
391
+ #
392
+ # === Return
393
+ # (ServerTemplateChefRecipe|nil):: Corresponding recipe, or nil if no recipe with this id exists
394
+ def recipe(id, options = {})
395
+ run_query(options) { ServerTemplateChefRecipe.find(id) }
396
+ end
397
+
398
+ # Retrieve recipe with given name on given instance
399
+ #
400
+ # === Parameters
401
+ # name(String):: Name of recipe that should be retrieved
402
+ # instance(Instance):: Instance on which recipe is defined
403
+ # options(Hash):: Query options:
404
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
405
+ #
406
+ # === Return
407
+ # recipe(ServerTemplateChefRecipe):: Corresponding recipe
408
+ def recipe_from_name(name, instance, options = {})
409
+ recipe = nil
410
+ template = run_query(options) { instance.server_template }
411
+ if template
412
+ recipe = run_query(options) { template.server_template_chef_recipes.find_by_recipe(name) }
413
+ end
414
+ recipe
415
+ end
416
+
417
+ # Retrieve Permission with given id
418
+ #
419
+ # === Parameters
420
+ # id(Integer):: Id of Permission to be retrieved
421
+ # options(Hash):: Query options:
422
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
423
+ #
424
+ # === Return
425
+ # (Permission|nil):: Corresponding permission, or nil if no permission with this id exists
426
+ def permission(id, options = {})
427
+ run_query(options) { Permission.find(id) }
428
+ end
429
+
430
+ # Retrieve UserCredential with given id
431
+ #
432
+ # === Parameters
433
+ # id(Integer):: Id of UserCredential to be retrieved
434
+ # options(Hash):: Query options:
435
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
436
+ #
437
+ # === Return
438
+ # (UserCredential|nil):: Corresponding user credential, or nil if no credential with this id exists
439
+ def user_credential(id, options = {})
440
+ run_query(options) { UserCredential.find(id) }
441
+ end
442
+
443
+ # Retrieve UserCredential with given public value fingerprint
444
+ #
445
+ # === Parameters
446
+ # public_value_fingerprint(String):: Public value fingerprint of UserCredential to be retrieved
447
+ # options(Hash):: Query options:
448
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
449
+ #
450
+ # === Return
451
+ # (UserCredential|nil):: Corresponding user credential, or nil if no credential with this fingerprint exists
452
+ def user_credential_from_fingerprint(public_value_fingerprint, options = {})
453
+ run_query(options) { UserCredential.find_by_public_value_fingerprint(public_value_fingerprint) }
454
+ end
455
+
456
+ # Retrieve Setting with given id
457
+ #
458
+ # === Parameters
459
+ # id(Integer):: Id of Setting to be retrieved
460
+ # options(Hash):: Query options:
461
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
462
+ #
463
+ # === Return
464
+ # (Setting|nil):: Corresponding setting, or nil if no setting with this id exists
465
+ def setting(id, options = {})
466
+ run_query(options) { Setting.find(id) }
467
+ end
468
+
469
+ # Retrieve all software repositories
470
+ #
471
+ # === Parameters
472
+ # options(Hash):: Query options:
473
+ # :retryable_error(Boolean):: Whether to raise RetryableError if exceed maximum retries
474
+ #
475
+ # === Return
476
+ # (Array):: Array of Repository
477
+ def repositories(options = {})
478
+ run_query(options) { Repository.find(:all) }
479
+ end
480
+
481
+ end # ModelHelpers
482
+
483
+ end # RightScale