right_infrastructure_agent 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +10 -0
- data/README.rdoc +65 -0
- data/Rakefile +86 -0
- data/lib/right_infrastructure_agent.rb +26 -0
- data/lib/right_infrastructure_agent/command_constants.rb +34 -0
- data/lib/right_infrastructure_agent/global_object_replicator_sink.rb +337 -0
- data/lib/right_infrastructure_agent/global_object_replicator_source.rb +117 -0
- data/lib/right_infrastructure_agent/infrastructure_auth_client.rb +88 -0
- data/lib/right_infrastructure_agent/infrastructure_helpers.rb +85 -0
- data/lib/right_infrastructure_agent/login_policy_factory.rb +137 -0
- data/lib/right_infrastructure_agent/models_helper.rb +483 -0
- data/lib/right_infrastructure_agent/rainbows_agent_controller.rb +192 -0
- data/lib/right_infrastructure_agent/scripts/infrastructure_agent_deployer.rb +278 -0
- data/right_infrastructure_agent.gemspec +54 -0
- data/spec/global_object_replicator_sink_spec.rb +305 -0
- data/spec/global_object_replicator_source_spec.rb +113 -0
- data/spec/infrastructure_auth_client_spec.rb +140 -0
- data/spec/infrastructure_helpers_spec.rb +80 -0
- data/spec/login_policy_factory_spec.rb +279 -0
- data/spec/models_helper_spec.rb +546 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +85 -0
- metadata +116 -0
@@ -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
|