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.
- 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
|