right_infrastructure_agent 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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