collins_state 0.2.10
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.
- data/Gemfile +15 -0
- data/Gemfile.lock +55 -0
- data/README.md +6 -0
- data/Rakefile +67 -0
- data/VERSION +1 -0
- data/collins_state.gemspec +53 -0
- data/lib/collins/persistent_state.rb +108 -0
- data/lib/collins/state/mixin.rb +435 -0
- data/lib/collins/state/mixin_class_methods.rb +128 -0
- data/lib/collins/state/specification.rb +192 -0
- data/lib/collins/workflows/provisioning_workflow.rb +57 -0
- data/lib/collins_state.rb +4 -0
- metadata +93 -0
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source :rubygems
|
2
|
+
|
3
|
+
Encoding.default_external = Encoding::UTF_8
|
4
|
+
gem 'collins_client', '~> 0.2.7'
|
5
|
+
gem 'escape', '~> 0.0.4'
|
6
|
+
|
7
|
+
group :development do
|
8
|
+
gem "rspec", "~> 2.10.0"
|
9
|
+
gem "yard", "~> 0.8"
|
10
|
+
gem 'redcarpet'
|
11
|
+
gem 'webmock'
|
12
|
+
gem "bundler", "~> 1.1.4"
|
13
|
+
gem "jeweler", "~> 1.8.4"
|
14
|
+
gem 'simplecov'
|
15
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
addressable (2.3.2)
|
5
|
+
collins_client (0.2.7)
|
6
|
+
httparty (~> 0.8.3)
|
7
|
+
crack (0.3.1)
|
8
|
+
diff-lcs (1.1.3)
|
9
|
+
escape (0.0.4)
|
10
|
+
git (1.2.5)
|
11
|
+
httparty (0.8.3)
|
12
|
+
multi_json (~> 1.0)
|
13
|
+
multi_xml
|
14
|
+
jeweler (1.8.4)
|
15
|
+
bundler (~> 1.0)
|
16
|
+
git (>= 1.2.5)
|
17
|
+
rake
|
18
|
+
rdoc
|
19
|
+
json (1.7.5)
|
20
|
+
multi_json (1.3.6)
|
21
|
+
multi_xml (0.5.1)
|
22
|
+
rake (0.9.2.2)
|
23
|
+
rdoc (3.12)
|
24
|
+
json (~> 1.4)
|
25
|
+
redcarpet (2.2.2)
|
26
|
+
rspec (2.10.0)
|
27
|
+
rspec-core (~> 2.10.0)
|
28
|
+
rspec-expectations (~> 2.10.0)
|
29
|
+
rspec-mocks (~> 2.10.0)
|
30
|
+
rspec-core (2.10.1)
|
31
|
+
rspec-expectations (2.10.0)
|
32
|
+
diff-lcs (~> 1.1.3)
|
33
|
+
rspec-mocks (2.10.1)
|
34
|
+
simplecov (0.7.1)
|
35
|
+
multi_json (~> 1.0)
|
36
|
+
simplecov-html (~> 0.7.1)
|
37
|
+
simplecov-html (0.7.1)
|
38
|
+
webmock (1.8.11)
|
39
|
+
addressable (>= 2.2.7)
|
40
|
+
crack (>= 0.1.7)
|
41
|
+
yard (0.8.3)
|
42
|
+
|
43
|
+
PLATFORMS
|
44
|
+
ruby
|
45
|
+
|
46
|
+
DEPENDENCIES
|
47
|
+
bundler (~> 1.1.4)
|
48
|
+
collins_client (~> 0.2.7)
|
49
|
+
escape (~> 0.0.4)
|
50
|
+
jeweler (~> 1.8.4)
|
51
|
+
redcarpet
|
52
|
+
rspec (~> 2.10.0)
|
53
|
+
simplecov
|
54
|
+
webmock
|
55
|
+
yard (~> 0.8)
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
jeweler = Jeweler::Tasks.new do |gem|
|
16
|
+
gem.name = "collins_state"
|
17
|
+
gem.homepage = "https://github.com/tumblr/collins/tree/master/support/ruby/collins-state"
|
18
|
+
gem.license = "APL 2.0"
|
19
|
+
gem.summary = %Q{Collins based state management}
|
20
|
+
gem.description = %Q{Provides basic framework for managing stateful processes with collins}
|
21
|
+
gem.email = "bmatheny@tumblr.com"
|
22
|
+
gem.authors = ["Blake Matheny"]
|
23
|
+
gem.files.exclude "spec/**/*"
|
24
|
+
gem.files.exclude '.gitignore'
|
25
|
+
gem.files.exclude '.rspec'
|
26
|
+
gem.files.exclude '.rvmrc'
|
27
|
+
gem.add_runtime_dependency 'collins_client', '~> 0.2.7'
|
28
|
+
gem.add_runtime_dependency 'escape', '~> 0.0.4'
|
29
|
+
end
|
30
|
+
|
31
|
+
task :help do
|
32
|
+
puts("rake -T # See available rake tasks")
|
33
|
+
puts("rake publish # generate gemspec, build it, push it to repo")
|
34
|
+
puts("rake version:bump:patch # Bump patch number")
|
35
|
+
puts("rake all # bump patch and publish")
|
36
|
+
puts("rake # Run tests")
|
37
|
+
end
|
38
|
+
|
39
|
+
task :publish => [:gemspec, :build] do
|
40
|
+
package_abs = jeweler.jeweler.gemspec_helper.gem_path
|
41
|
+
package_name = File.basename(package_abs)
|
42
|
+
|
43
|
+
["repo.tumblr.net","repo.ewr01.tumblr.net"].each do |host|
|
44
|
+
puts("Copying #{package_abs} to #{host} and installing, you may be prompted for your password")
|
45
|
+
system "scp #{package_abs} #{host}:"
|
46
|
+
system "ssh -t #{host} 'sudo tumblr_gem install #{package_name}'"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
task :all => ["version:bump:patch", :publish] do
|
51
|
+
puts("Done!")
|
52
|
+
end
|
53
|
+
|
54
|
+
require 'rspec/core'
|
55
|
+
require 'rspec/core/rake_task'
|
56
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
57
|
+
spec.fail_on_error = false
|
58
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
59
|
+
end
|
60
|
+
|
61
|
+
task :default => :spec
|
62
|
+
|
63
|
+
require 'yard'
|
64
|
+
YARD::Rake::YardocTask.new do |t|
|
65
|
+
t.files = ['lib/**/*.rb']
|
66
|
+
t.options = ['--markup', 'markdown']
|
67
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.10
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "collins_state"
|
8
|
+
s.version = "0.2.10"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Blake Matheny"]
|
12
|
+
s.date = "2012-10-31"
|
13
|
+
s.description = "Provides basic framework for managing stateful processes with collins"
|
14
|
+
s.email = "bmatheny@tumblr.com"
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.md"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
"Gemfile",
|
20
|
+
"Gemfile.lock",
|
21
|
+
"README.md",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION",
|
24
|
+
"collins_state.gemspec",
|
25
|
+
"lib/collins/persistent_state.rb",
|
26
|
+
"lib/collins/state/mixin.rb",
|
27
|
+
"lib/collins/state/mixin_class_methods.rb",
|
28
|
+
"lib/collins/state/specification.rb",
|
29
|
+
"lib/collins/workflows/provisioning_workflow.rb",
|
30
|
+
"lib/collins_state.rb"
|
31
|
+
]
|
32
|
+
s.homepage = "https://github.com/tumblr/collins/tree/master/support/ruby/collins-state"
|
33
|
+
s.licenses = ["APL 2.0"]
|
34
|
+
s.require_paths = ["lib"]
|
35
|
+
s.rubygems_version = "1.8.24"
|
36
|
+
s.summary = "Collins based state management"
|
37
|
+
|
38
|
+
if s.respond_to? :specification_version then
|
39
|
+
s.specification_version = 3
|
40
|
+
|
41
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
42
|
+
s.add_runtime_dependency(%q<collins_client>, ["~> 0.2.7"])
|
43
|
+
s.add_runtime_dependency(%q<escape>, ["~> 0.0.4"])
|
44
|
+
else
|
45
|
+
s.add_dependency(%q<collins_client>, ["~> 0.2.7"])
|
46
|
+
s.add_dependency(%q<escape>, ["~> 0.0.4"])
|
47
|
+
end
|
48
|
+
else
|
49
|
+
s.add_dependency(%q<collins_client>, ["~> 0.2.7"])
|
50
|
+
s.add_dependency(%q<escape>, ["~> 0.0.4"])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'collins/state/mixin'
|
2
|
+
require 'escape'
|
3
|
+
|
4
|
+
module Collins
|
5
|
+
|
6
|
+
# Provides state management via collins tags
|
7
|
+
class PersistentState
|
8
|
+
|
9
|
+
include ::Collins::State::Mixin
|
10
|
+
include ::Collins::Util
|
11
|
+
|
12
|
+
attr_reader :collins_client, :path, :exec_type, :logger
|
13
|
+
|
14
|
+
def initialize collins_client, options = {}
|
15
|
+
@collins_client = collins_client
|
16
|
+
@exec_type = :client
|
17
|
+
@logger = get_logger({:logger => collins_client.logger}.merge(options).merge(:progname => 'Collins_PersistentState'))
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
self.class.managed_state(collins_client)
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# @deprecated Use {#use_netcat} instead. Replace in 0.3.0
|
26
|
+
def use_curl path = nil
|
27
|
+
use_netcat(path)
|
28
|
+
end
|
29
|
+
|
30
|
+
def use_client
|
31
|
+
@path = nil
|
32
|
+
@exec_type = :client
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def use_netcat path = nil
|
37
|
+
@path = Collins::Option(path).get_or_else("nc")
|
38
|
+
@exec_type = :netcat
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
# @override update_asset(asset, key, spec)
|
43
|
+
def update_asset asset, key, spec
|
44
|
+
username = collins_client.username
|
45
|
+
password = collins_client.password
|
46
|
+
host = collins_client.host
|
47
|
+
tag = ::Collins::Util.get_asset_or_tag(asset).tag
|
48
|
+
case @exec_type
|
49
|
+
when :netcat
|
50
|
+
netcat_command = [path, '-i', '1'] + get_hostname_port(host)
|
51
|
+
timestamp_padding, json = format_spec_for_netcat spec
|
52
|
+
body = "attribute=#{key};#{json}"
|
53
|
+
length = body.size + timestamp_padding
|
54
|
+
request = [request_line(tag)] + request_headers(username, password, length)
|
55
|
+
request_string = request.join("\\r\\n") + "\\r\\n\\r\\n" + body
|
56
|
+
current_time = 'TIMESTAMP=$(' + get_time_cmds(host).join(' | ') + ')'
|
57
|
+
args = ['printf', request_string]
|
58
|
+
"#{current_time}\n" + Escape.shell_command(args) + ' $TIMESTAMP | ' + Escape.shell_command(netcat_command)
|
59
|
+
else
|
60
|
+
super(asset, key, spec)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_time_cmds host
|
65
|
+
hostname, port = get_hostname_port host
|
66
|
+
get_cmd = %q{printf 'GET /api/timestamp HTTP/1.0\r\n\r\n'}
|
67
|
+
nc = sprintf('%s -i 1 %s %d', path, hostname, port)
|
68
|
+
only_time = %q{grep -e '^[0-9]'}
|
69
|
+
last_line = %q{tail -n 1}
|
70
|
+
[get_cmd, nc, only_time, last_line]
|
71
|
+
end
|
72
|
+
|
73
|
+
def request_line tag
|
74
|
+
"POST /api/asset/#{tag} HTTP/1.0"
|
75
|
+
end
|
76
|
+
|
77
|
+
def request_headers username, password, length
|
78
|
+
[
|
79
|
+
"User-Agent: collins_state",
|
80
|
+
auth_header(username, password),
|
81
|
+
"Content-Type: application/x-www-form-urlencoded",
|
82
|
+
"Content-Length: #{length}",
|
83
|
+
"Connection: Close"
|
84
|
+
]
|
85
|
+
end
|
86
|
+
|
87
|
+
def auth_header username, password
|
88
|
+
auth = Base64.strict_encode64("#{username}:#{password}")
|
89
|
+
"Authorization: Basic #{auth}"
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Array<Fixnum,String>] padding for format string and json to use
|
93
|
+
def format_spec_for_netcat spec
|
94
|
+
expected_timestamp_size = Time.now.utc.to_i.to_s.size
|
95
|
+
spec.timestamp = '%s'
|
96
|
+
actual_timestamp_size = spec.timestamp.to_s.size
|
97
|
+
timestamp_padding = (expected_timestamp_size - actual_timestamp_size).abs
|
98
|
+
json = spec.to_json
|
99
|
+
[timestamp_padding, json]
|
100
|
+
end
|
101
|
+
# @return [Array<String,String>] hostname and port
|
102
|
+
def get_hostname_port host
|
103
|
+
host = URI.parse(host)
|
104
|
+
[host.host, host.port.to_s]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
@@ -0,0 +1,435 @@
|
|
1
|
+
require 'collins/state/mixin_class_methods'
|
2
|
+
require 'collins/state/specification'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Collins; module State; module Mixin
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# Classes that include Mixin will also be extended by ClassMethods
|
9
|
+
def included(base)
|
10
|
+
base.extend Collins::State::Mixin::ClassMethods
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# @abstract Classes mixing this in must supply a collins client
|
15
|
+
# @return [Collins::Client] collins client
|
16
|
+
# @raise [NotImplementedError] if not specified
|
17
|
+
def collins_client
|
18
|
+
raise NotImplementedError.new("no collins client available")
|
19
|
+
end
|
20
|
+
|
21
|
+
# @abstract Classes mixing this in must supply a logger
|
22
|
+
# @return [Logger] logger instance
|
23
|
+
# @raise [NotImplementedError] if not specified
|
24
|
+
def logger
|
25
|
+
raise NotImplementedError.new("no logger available")
|
26
|
+
end
|
27
|
+
|
28
|
+
# The attribute name used for storing the sate value
|
29
|
+
# @note we append _json to the managed_state_name since it will serialize this way
|
30
|
+
# @return [String] the key to use for storing the state value on an asset
|
31
|
+
def attribute_name
|
32
|
+
self.class.managed_state_name.to_s + "_json"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Has the specified asset state expired?
|
36
|
+
#
|
37
|
+
# Read the state from the specified asset, and determine whether the state is expired yet or not.
|
38
|
+
# The timestamp + expiration is compared to now.
|
39
|
+
#
|
40
|
+
# @note This method will return true if no specification is associated with the asset
|
41
|
+
# @param [Collins::Asset] asset The asset to look at
|
42
|
+
# @return [Boolean] True if the specification has expired, false otherwise
|
43
|
+
def expired? asset
|
44
|
+
specification_expired?(state_specification(asset))
|
45
|
+
end
|
46
|
+
|
47
|
+
# Whether we are done processing or not
|
48
|
+
# @param [Collins::Asset] asset
|
49
|
+
# @return [Boolean] whether asset is in done state or not
|
50
|
+
def finished? asset
|
51
|
+
state_specification(asset).to_option.map do |spec|
|
52
|
+
specification_expired?(spec) && event(spec.name)[:terminus]
|
53
|
+
end.get_or_else(false)
|
54
|
+
end
|
55
|
+
|
56
|
+
# The things that would be done if the asset was transitioned
|
57
|
+
# @param [Collins::Asset] asset
|
58
|
+
# @return [Array<Array<Symbol,String>>] array of arrays. Each sub array has two elements
|
59
|
+
def plan asset
|
60
|
+
plans = []
|
61
|
+
state_specification(asset).to_option.map { |specification|
|
62
|
+
event(specification.name).to_option.map { |ev|
|
63
|
+
if not specification_expired?(specification) then
|
64
|
+
plans << [:noop, "not yet expired"]
|
65
|
+
else
|
66
|
+
if ev[:transition] then
|
67
|
+
event(ev[:transition]).to_option.map { |ev2|
|
68
|
+
plans << [:event, ev2.name]
|
69
|
+
}.get_or_else {
|
70
|
+
plans << [:exception, "invalid event name #{ev[:transition]}"]
|
71
|
+
}
|
72
|
+
elsif not ev[:on_transition] then
|
73
|
+
plans << [:noop, "no transition specified, and no on_transition action specified"]
|
74
|
+
end
|
75
|
+
if ev[:on_transition] then
|
76
|
+
action(ev[:on_transition]).to_option.map { |ae|
|
77
|
+
plans << [:action, ae.name]
|
78
|
+
}.get_or_else {
|
79
|
+
plans << [:exception, "invalid action specified #{ev[:on_transition]}"]
|
80
|
+
}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
}.get_or_else {
|
84
|
+
plans << [:exception, "invalid event name #{e.name}"]
|
85
|
+
}
|
86
|
+
}.get_or_else {
|
87
|
+
Collins::Option(initial).map { |init|
|
88
|
+
event(init).to_option.map { |ev|
|
89
|
+
if ev[:before_transition] then
|
90
|
+
action(ev[:before_transition]).to_option.map do |act|
|
91
|
+
plans << [:action, act.name]
|
92
|
+
end.get_or_else {
|
93
|
+
plans << [:exception, "action #{ev[:before_transition]} not defined"]
|
94
|
+
}
|
95
|
+
end
|
96
|
+
plans << [:event, init]
|
97
|
+
}.get_or_else {
|
98
|
+
plans << [:exception, "initial state #{init} is undefined"]
|
99
|
+
}
|
100
|
+
}.get_or_else {
|
101
|
+
plans << [:exception, "no initial state defined"]
|
102
|
+
}
|
103
|
+
}
|
104
|
+
plans
|
105
|
+
end
|
106
|
+
|
107
|
+
# Reset (delete) the attribute once the process is complete
|
108
|
+
#
|
109
|
+
# @param [Collins::Asset] asset The asset on which to delete the attribute
|
110
|
+
# @return [Boolean] True if the value was successfully deleted
|
111
|
+
def reset! asset
|
112
|
+
collins_client.delete_attribute! asset, attribute_name
|
113
|
+
end
|
114
|
+
|
115
|
+
# Return the name of the current sate. Will be :None if not initialized
|
116
|
+
# @param [Collins::Asset] asset
|
117
|
+
# @return [Symbol] state name
|
118
|
+
def state_name asset
|
119
|
+
state_specification(asset).name
|
120
|
+
end
|
121
|
+
|
122
|
+
# Get the state specification associated with the asset
|
123
|
+
#
|
124
|
+
# @param [Collins::Asset] asset The asset to retrieve
|
125
|
+
# @return [Collins::State::Specification] The spec (be sure to check `defined?`)
|
126
|
+
def state_specification asset
|
127
|
+
updated = asset_from_cache asset
|
128
|
+
result = updated.send(attribute_name.to_sym)
|
129
|
+
if result then
|
130
|
+
res = JSON.parse(result) rescue nil
|
131
|
+
if res.nil? then # for backwards compatibility
|
132
|
+
res = JSON.parse(result, :create_additions => false)
|
133
|
+
res = ::Collins::State::Specification.json_create(res) if (res.is_a?(Hash) and res.key?('data'))
|
134
|
+
end
|
135
|
+
if res.is_a?(::Collins::State::Specification) then
|
136
|
+
res
|
137
|
+
else
|
138
|
+
logger.warn("Could not deserialize #{result} to a State Specification")
|
139
|
+
::Collins::State::Specification.empty
|
140
|
+
end
|
141
|
+
else
|
142
|
+
::Collins::State::Specification.empty
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Transition the asset to the next appropriate state
|
147
|
+
#
|
148
|
+
# This method will either initialize the state on the specified asset (if needed), or process the
|
149
|
+
# current state. If processing the current state, the expiration time will be checked, followed by
|
150
|
+
# running any specified transition event, followed by any `on_transition` action. The transition
|
151
|
+
# event is run before the `on_transition` action, because the `on_transition` action should only be
|
152
|
+
# called if we have successfully transitioned to a new state. In the event that the transition
|
153
|
+
# event has a `before_transition` defined that fails, we don't want to execute the `on_transition`
|
154
|
+
# code since we have not yet successfully transitioned.
|
155
|
+
#
|
156
|
+
# @param [Collins::Asset] asset The asset to transition
|
157
|
+
# @param [Hash] options Transition options
|
158
|
+
# @option options [Boolean] :quiet Don't throw an exception if a transition fails
|
159
|
+
# @raise [CollinsError] if state needs to be initialized and no `:initial` key is found in the manage_state options hash
|
160
|
+
# @raise [CollinsError] if a specification is found on the asset, but the named state isn't found as a registered event
|
161
|
+
# @raise [CollinsError] if an action is specified as a `:before_transition` or `:on_transition` value but not registered
|
162
|
+
# @return [Collins::State::Specification] The current state, after the transition is run
|
163
|
+
def transition asset, options = {}
|
164
|
+
state_specification(asset).to_option.map { |specification|
|
165
|
+
event(specification.name).to_option.or_else {
|
166
|
+
raise CollinsError.new("no event defined with name #{specification.name}")
|
167
|
+
}.filter { |e| specification_expired?(specification) }.map { |e|
|
168
|
+
if e[:transition] then
|
169
|
+
spec = run_event(asset, e[:transition], options)
|
170
|
+
run_action(asset, e[:on_transition]) if e[:on_transition]
|
171
|
+
# If we transitioned and no expiration is set, rerun
|
172
|
+
if specification_expired?(spec) and spec.name != e.name then
|
173
|
+
transition(asset, options)
|
174
|
+
else
|
175
|
+
spec
|
176
|
+
end
|
177
|
+
else
|
178
|
+
logger.debug("No transition event specified for #{e.name}")
|
179
|
+
run_action(asset, e[:on_transition], :log => true) if e[:on_transition]
|
180
|
+
specification
|
181
|
+
end
|
182
|
+
}.get_or_else {
|
183
|
+
logger.trace("Specification #{specification.name} not yet expired")
|
184
|
+
specification
|
185
|
+
}
|
186
|
+
}.get_or_else {
|
187
|
+
init = Collins::Option(initial).get_or_else {
|
188
|
+
raise Collins::CollinsError.new("no initial state defined for transition")
|
189
|
+
}
|
190
|
+
options = Collins::Option(self.class.managed_state_options).get_or_else({})
|
191
|
+
run_event(asset, init, options)
|
192
|
+
}
|
193
|
+
end
|
194
|
+
|
195
|
+
# Allow registered events to be executed. Allow predicate calls to respond true if the asset is
|
196
|
+
# currently in the specified state or false otherwise
|
197
|
+
def method_missing method, *args, &block
|
198
|
+
if args.length == 0 then
|
199
|
+
return super
|
200
|
+
end
|
201
|
+
asset = args[0]
|
202
|
+
options = args[1]
|
203
|
+
if not (asset.is_a?(Collins::Asset) || asset.is_a?(String)) then
|
204
|
+
return super
|
205
|
+
end
|
206
|
+
if not options.nil? and not options.is_a?(Hash) then
|
207
|
+
return super
|
208
|
+
elsif options.nil? then
|
209
|
+
options = {}
|
210
|
+
end
|
211
|
+
question_only = method.to_s.end_with?('?')
|
212
|
+
if question_only then
|
213
|
+
meth = method.to_s[0..-2].to_sym # drop ? at end
|
214
|
+
if event?(meth) then
|
215
|
+
state_name(asset) == meth
|
216
|
+
else
|
217
|
+
false
|
218
|
+
end
|
219
|
+
elsif event?(method) then
|
220
|
+
run_event(asset, method, options)
|
221
|
+
else
|
222
|
+
super
|
223
|
+
end
|
224
|
+
end # method_missing
|
225
|
+
|
226
|
+
def respond_to? method
|
227
|
+
question_only = method.to_s.end_with?('?')
|
228
|
+
if question_only then
|
229
|
+
method = method.to_s[0..-2] # drop? at end
|
230
|
+
end
|
231
|
+
if not event?(method) then
|
232
|
+
super
|
233
|
+
else
|
234
|
+
true
|
235
|
+
end
|
236
|
+
end # respond_to?
|
237
|
+
|
238
|
+
protected
|
239
|
+
# Get the callback associated with the specified action name
|
240
|
+
# @param [Symbol] name Action name
|
241
|
+
# @return [Collins::SimpleCallback] always returns, check `defined?`
|
242
|
+
def action name
|
243
|
+
name_sym = name.to_sym
|
244
|
+
self.class.actions.fetch(name_sym, ::Collins::SimpleCallback.empty)
|
245
|
+
end
|
246
|
+
|
247
|
+
# True if an event with the given name is registered, false otherwise
|
248
|
+
# @param [Symbol] name Event name
|
249
|
+
# @return [Boolean] Registered or not
|
250
|
+
def event? name
|
251
|
+
event(name).defined?
|
252
|
+
end
|
253
|
+
|
254
|
+
# Get the callback associated with the specified event name
|
255
|
+
# @param [Symbol] name Event name
|
256
|
+
# @return [Collins::SimpleCallback] always returns, check `defined?`
|
257
|
+
def event name
|
258
|
+
name_sym = name.to_sym
|
259
|
+
self.class.events.fetch(name_sym, ::Collins::SimpleCallback.empty)
|
260
|
+
end
|
261
|
+
|
262
|
+
# Get the expires time associated with the specified event
|
263
|
+
#
|
264
|
+
# @param [Symbol] name Name of the event
|
265
|
+
# @param [Fixnum] default Value if event not found or expires not specified
|
266
|
+
# @return [Fixnum] An integer representing the number of seconds before expiration
|
267
|
+
def event_expires name, default = 0
|
268
|
+
event(name).to_option.map do |e|
|
269
|
+
e.options.fetch(:expires, default.to_i)
|
270
|
+
end.get_or_else(default.to_i)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Initial event or nil
|
274
|
+
def initial
|
275
|
+
Collins::Option(self.class.managed_state_options).
|
276
|
+
filter{|h| h.key?(:initial)}.
|
277
|
+
map{|h| h[:initial]}.
|
278
|
+
get_or_else(nil)
|
279
|
+
end
|
280
|
+
|
281
|
+
# Run the specified action
|
282
|
+
#
|
283
|
+
# @param [Collins::Asset] asset the asset to run the action for
|
284
|
+
# @param [Symbol] name the action name
|
285
|
+
# @param [Hash] options
|
286
|
+
# @option options [Boolean] :log (true) log action run
|
287
|
+
# @return [Boolean] the result of executing the action
|
288
|
+
def run_action asset, name, options = {}
|
289
|
+
action(name).to_option.map do |a|
|
290
|
+
result = case a.arity
|
291
|
+
when 2
|
292
|
+
a.call(asset, self)
|
293
|
+
else
|
294
|
+
a.call(asset)
|
295
|
+
end
|
296
|
+
log_run_action(asset, name, result) if options.fetch(:log, false)
|
297
|
+
result
|
298
|
+
end.get_or_else {
|
299
|
+
raise CollinsError.new("Action #{name} not defined")
|
300
|
+
}
|
301
|
+
end
|
302
|
+
|
303
|
+
# Update the asset spec that the action was run
|
304
|
+
# @param [Collins::Asset] asset
|
305
|
+
# @param [Symbol] action_name
|
306
|
+
# @param [Boolean] result of the action that was run
|
307
|
+
# @return [Collins::State::Specification]
|
308
|
+
def log_run_action asset, action_name, result
|
309
|
+
current_spec = state_specification asset
|
310
|
+
count = current_spec.fetch(:log, []).size
|
311
|
+
log = Hash[:count => count, :timestamp => Time.now.utc.to_i, :name => action_name, :result => result]
|
312
|
+
current_spec.<<(:log, log)
|
313
|
+
asset_cache_delete asset
|
314
|
+
update_asset asset, attribute_name, current_spec
|
315
|
+
current_spec
|
316
|
+
end
|
317
|
+
|
318
|
+
# Run the specified event, and execute :before_transition if specified
|
319
|
+
#
|
320
|
+
# @param [Collins::Asset] asset the asset to process the event for
|
321
|
+
# @param [Symbol] name the name of the event to process
|
322
|
+
# @param [Hash] options Option for executing the event
|
323
|
+
# @option options [Boolean] :quiet Do not throw an exception if an error occurs running actions
|
324
|
+
# @return [Collins::State::Specification] the new state the asset is in
|
325
|
+
def run_event asset, name, options = {}
|
326
|
+
event(name).to_option.map do |e|
|
327
|
+
update_state(asset, e, options)
|
328
|
+
end.get_or_else {
|
329
|
+
raise CollinsError.new("Event #{name} not defined")
|
330
|
+
}
|
331
|
+
end
|
332
|
+
|
333
|
+
# Whether the given specification has expired or not
|
334
|
+
#
|
335
|
+
# This method is primarily useful for testing whether a stored specification has expired yet or
|
336
|
+
# not. If a specification has just been created (and has an expiration set), this will obviously
|
337
|
+
# return false.
|
338
|
+
#
|
339
|
+
# @param [Collins::State::Specification] specification
|
340
|
+
# @return [Boolean] true if expired, false otherwise
|
341
|
+
def specification_expired? specification
|
342
|
+
timestamp = specification.timestamp
|
343
|
+
name = specification.name
|
344
|
+
expires_at = timestamp + event_expires(name)
|
345
|
+
# Must be >= otherwise 0 + Time.now as expires_at will fail
|
346
|
+
Time.now.utc.to_i >= expires_at
|
347
|
+
end
|
348
|
+
|
349
|
+
# Update asset state using the specified event information
|
350
|
+
#
|
351
|
+
# This method also handles executing appropriate transition related actions and managing failures.
|
352
|
+
# The actual code for updating the asset itself is in #update_asset
|
353
|
+
#
|
354
|
+
# @param [Collins::Asset] asset the asset to update
|
355
|
+
# @param [Collins::SimpleCallback] event the event
|
356
|
+
# @param [Hash] options Option for executing the event
|
357
|
+
# @option options [Boolean] :quiet Do not throw an exception if an error occurs running actions
|
358
|
+
def update_state asset, event, options = {}
|
359
|
+
if event[:before_transition] then
|
360
|
+
run_before_transition asset, event[:before_transition], event, options
|
361
|
+
else
|
362
|
+
specification = Collins::State::Specification.new event.name, event[:desc], Time.now
|
363
|
+
specification = specification.merge(state_specification(asset))
|
364
|
+
asset_cache_delete asset
|
365
|
+
res = update_asset asset, attribute_name, specification
|
366
|
+
# Horrible hack to allow update_asset to be overridden such that it can provide a string
|
367
|
+
# for performing an update, in which case we need the actual command
|
368
|
+
if res.is_a?(String) then
|
369
|
+
return res
|
370
|
+
else
|
371
|
+
return specification
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
# Run the specified before_transition
|
377
|
+
# We try running the specified action, and if successful update the asset with the specification.
|
378
|
+
# If running the action fails, we update the attempts count on the current specification and
|
379
|
+
# either throw an exception (default behavior) or return the updated spec (options[:quiet] ==
|
380
|
+
# true). On success we return the new specification.
|
381
|
+
def run_before_transition asset, action_name, event, options
|
382
|
+
before_result =
|
383
|
+
begin
|
384
|
+
run_action(asset, action_name)
|
385
|
+
rescue Exception => e
|
386
|
+
logger.warn("Failed running #{action_name}: #{e}")
|
387
|
+
false
|
388
|
+
end
|
389
|
+
if before_result != false
|
390
|
+
specification = Collins::State::Specification.new event.name, event[:desc], Time.now
|
391
|
+
specification = specification.merge(state_specification(asset))
|
392
|
+
asset_cache_delete asset
|
393
|
+
update_asset asset, attribute_name, specification
|
394
|
+
specification
|
395
|
+
else
|
396
|
+
current_spec = state_specification(asset)
|
397
|
+
count = current_spec.fetch(:attempts, []).size
|
398
|
+
attempts = Hash[:timestamp => Time.now.utc.to_i, :count => count, :name => event.name]
|
399
|
+
current_spec.<<(:attempts, attempts)
|
400
|
+
asset_cache_delete asset
|
401
|
+
update_asset asset, attribute_name, current_spec
|
402
|
+
if options.fetch(:quiet, false) then
|
403
|
+
return current_spec
|
404
|
+
else
|
405
|
+
raise CollinsError.new("Can't transition from #{current_spec.name} to #{event.name}, before_transition failed")
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
# Update the asset using the specified key and JSON
|
411
|
+
#
|
412
|
+
# This method is broken out this way so it can be easily overriden
|
413
|
+
# @param [Collins::Asset] asset The asset to update
|
414
|
+
# @param [String] key The attribute to update on the asset
|
415
|
+
# @param [Specification] spec The state specification to set as the value associated with the key
|
416
|
+
# @return [Boolean] indication of success or failure
|
417
|
+
def update_asset asset, key, spec
|
418
|
+
collins_client.set_attribute! asset, key, spec.to_json
|
419
|
+
end
|
420
|
+
|
421
|
+
private
|
422
|
+
def asset_cache_delete asset
|
423
|
+
tag = Collins::Util.get_asset_or_tag(asset).tag
|
424
|
+
@_asset_cache.delete(tag) if (@_asset_cache && @_asset_cache.key?(tag))
|
425
|
+
end
|
426
|
+
def asset_from_cache asset
|
427
|
+
tag = Collins::Util.get_asset_or_tag(asset).tag
|
428
|
+
@_asset_cache = {} unless @_asset_cache
|
429
|
+
if not @_asset_cache.key?(tag) then
|
430
|
+
@_asset_cache[tag] = collins_client.get(tag)
|
431
|
+
end
|
432
|
+
@_asset_cache[tag]
|
433
|
+
end
|
434
|
+
|
435
|
+
end; end; end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'collins/simple_callback'
|
2
|
+
|
3
|
+
module Collins; module State; module Mixin
|
4
|
+
|
5
|
+
# Static (Class) methods to be added to classes that mixin this module. These methods provide
|
6
|
+
# functionality for registering new actions and events, along with access to the managed state.
|
7
|
+
# This is accomplished via class instance variables which allow each class that include the
|
8
|
+
# {Collins::State::Mixin} its own managed state and variables.
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
# @return [Hash<Symbol, Collins::SimpleCallback>] Registered actions for the managed state
|
12
|
+
attr_reader :actions
|
13
|
+
|
14
|
+
# @return [Hash<Symbol, Collins::SimpleCallback>] Registered events for the managed state
|
15
|
+
attr_reader :events
|
16
|
+
|
17
|
+
# Register a managed state for this class
|
18
|
+
#
|
19
|
+
# @note Only one managed state per class is allowed.
|
20
|
+
# @param [Symbol] name The name of the managed state, e.g. `:some_process`
|
21
|
+
# @param [Hash] options Managed state options
|
22
|
+
# @option options [Symbol] :initial First event to fire if the state is being initialized
|
23
|
+
# @yieldparam [Collins::Client] block Managed state description, registering events and actions
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# manage_state :a_process, :initial => :start do |client|
|
27
|
+
# action :log_it do |asset|
|
28
|
+
# client.log! asset, "Did some logging"
|
29
|
+
# end
|
30
|
+
# event :start, :desc => 'Initial state', :expires => after(30, :minutes), :on_transition => :log_it, :transition => :done
|
31
|
+
# event :done, :desc => 'Done'
|
32
|
+
# end
|
33
|
+
def manage_state name, options = {}, &block
|
34
|
+
@actions = {}
|
35
|
+
@events = {}
|
36
|
+
@managed_state = ::Collins::SimpleCallback.new(name, options, block)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [String] Name of managed state associated with this class
|
40
|
+
def managed_state_name
|
41
|
+
@managed_state.name
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Hash] Options associated with this managed state
|
45
|
+
def managed_state_options
|
46
|
+
@managed_state.options
|
47
|
+
end
|
48
|
+
|
49
|
+
# Get and execute the managed state associated with this class
|
50
|
+
#
|
51
|
+
# @see {#manage_state}
|
52
|
+
# @param [Collins::Client] client Collins client instance
|
53
|
+
def managed_state client
|
54
|
+
@managed_state.call(client)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Register a named action for use as a transition execution target
|
58
|
+
#
|
59
|
+
# Actions are called when specified by a `:before_transition` or `:on_transition` option to an
|
60
|
+
# event. Options are not currently used.
|
61
|
+
#
|
62
|
+
# @param [Symbol] name Action name
|
63
|
+
# @param [Hash] options Action options
|
64
|
+
# @yieldparam [Collins::Asset] block Given an asset, perform the specified action
|
65
|
+
# @yieldreturn [Boolean,Object] indicates success or failure of operation. Only `false` (or an exception) indicate failure. Other return values are fine.
|
66
|
+
# @return [Collins::SimpleCallback] the callback created for the action
|
67
|
+
def action name, options = {}, &block
|
68
|
+
name_sym = name.to_sym
|
69
|
+
new_action = ::Collins::SimpleCallback.new(name_sym, options, block)
|
70
|
+
@actions[name_sym] = new_action
|
71
|
+
new_action
|
72
|
+
end
|
73
|
+
|
74
|
+
# Register a named event associated with the managed state
|
75
|
+
#
|
76
|
+
# Events are typically called as method invocations on the class, or as the result of a state
|
77
|
+
# being expired and a transition being specified.
|
78
|
+
#
|
79
|
+
# The example (and events in general) can be read as: Execute `:before_transition` before
|
80
|
+
# successfully transitioning to this event. Once transitioned, after `:expires` is reached the
|
81
|
+
# `:on_transition` action should be called, followed by the event associated with the specified
|
82
|
+
# `:transition`.
|
83
|
+
#
|
84
|
+
# @param [Symbol] name Event name
|
85
|
+
# @param [Hash] options Event options
|
86
|
+
# @option options [Symbol] :before_transition An action to execute successfully before transitioning to this state
|
87
|
+
# @option options [String] :desc A description of the event, required
|
88
|
+
# @option options [#to_i] :expires Do not consider `:on_transition` or `:transition` until after this amount of time
|
89
|
+
# @option options [Symbol] :on_transition Once the expiration time has passed execute this action
|
90
|
+
# @option options [Symbol] :transition The event to call after its appropriate for transition (due to timeout and successful `:before` calls)
|
91
|
+
# @return [Collins::SimpleCallback] the callback created for the action
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# event :stuff, :desc => 'I do things', :before_transition => :try_action, :expires => after(5, :minutes),
|
95
|
+
# :transition => :after_stuff_event, :on_transition => :stuff_action
|
96
|
+
#
|
97
|
+
# @note A transition will not occur unless the :before_transition is successful (does not return false or throw an exception)
|
98
|
+
# @raise [KeyError] if the options hash is missing a `:desc` key
|
99
|
+
def event name, options = {}
|
100
|
+
name_sym = name.to_sym
|
101
|
+
::Collins::Option(options[:desc]).or_else {
|
102
|
+
raise KeyError.new("Event #{name} is missing :desc key")
|
103
|
+
}
|
104
|
+
new_event = ::Collins::SimpleCallback.new(name_sym, options)
|
105
|
+
@events[name_sym] = new_event
|
106
|
+
new_event
|
107
|
+
end
|
108
|
+
|
109
|
+
# Convert a `Fixnum` into seconds based on the specified `time_unit`
|
110
|
+
#
|
111
|
+
# @param [Fixnum] duration Time value
|
112
|
+
# @param [Symbol] time_unit Unit of time, one of `:hour`, `:hours`, `:minute`, `:minutes`.
|
113
|
+
# @return [Fixnum] Value in seconds
|
114
|
+
def after duration, time_unit = :seconds
|
115
|
+
multiplier = case time_unit
|
116
|
+
when :hours, :hour
|
117
|
+
60*60
|
118
|
+
when :minutes, :minute
|
119
|
+
60
|
120
|
+
else
|
121
|
+
1
|
122
|
+
end
|
123
|
+
multiplier * duration.to_i
|
124
|
+
end
|
125
|
+
|
126
|
+
end # Collins::State::Mixin::ClassMethods
|
127
|
+
|
128
|
+
end; end; end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Collins; module State
|
4
|
+
|
5
|
+
# Represents a managed state
|
6
|
+
#
|
7
|
+
# Modeling a state machine like process in collins is useful for multi-step processes such as
|
8
|
+
# decommissioning hardware (where you want the process to span several days, with several
|
9
|
+
# discrete steps), or monitoring some process and taking action. A
|
10
|
+
# {Specification} provides a common format for storing state information
|
11
|
+
# as a value of an asset.
|
12
|
+
#
|
13
|
+
# This will rarely be used directly, but rather is a byproduct of using
|
14
|
+
# {Collins::State::Mixin}
|
15
|
+
class Specification
|
16
|
+
include ::Collins::Util
|
17
|
+
|
18
|
+
# Used as name placeholder when unspecified
|
19
|
+
EMPTY_NAME = :None
|
20
|
+
# Used as description placeholder when unspecified
|
21
|
+
EMPTY_DESCRIPTION = "Unspecified"
|
22
|
+
|
23
|
+
# Create an empty specification
|
24
|
+
# @return [Collins::State::Specification] spec
|
25
|
+
def self.empty
|
26
|
+
::Collins::State::Specification.new :none => true
|
27
|
+
end
|
28
|
+
|
29
|
+
# Create an instance from JSON data
|
30
|
+
#
|
31
|
+
# @note This method is required by the JSON module for deserialization
|
32
|
+
# @param [Hash] json JSON data
|
33
|
+
# @return [Collins::State::Specification] spec
|
34
|
+
def self.json_create json
|
35
|
+
::Collins::State::Specification.new json['data']
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Symbol] Name of the specification.
|
39
|
+
# @see Collins::State::Mixin::ClassMethods#event
|
40
|
+
# @note This is a unique key and should not change.
|
41
|
+
attr_reader :name
|
42
|
+
|
43
|
+
# @return [String] State description, for humans
|
44
|
+
# @see Collins::State::Mixin::ClassMethods#event
|
45
|
+
attr_reader :description
|
46
|
+
|
47
|
+
# @return [Fixnum] Unixtime, UTC, when this state was entered
|
48
|
+
attr_accessor :timestamp
|
49
|
+
|
50
|
+
# @return [Hash] Additional meta-data
|
51
|
+
attr_reader :extras
|
52
|
+
|
53
|
+
# Instantiate a new Specification
|
54
|
+
#
|
55
|
+
# @param [Hash,(Symbol,String,Fixnum)] args Arguments for instantiation
|
56
|
+
# @option args [String,Symbol] :name The name of the specification
|
57
|
+
# @option args [String] :description A description of the specification
|
58
|
+
# @option args [Fixnum,Time,String] :timestamp (Time.at(0).utcto_i) The time the event occurred
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# Specification.new :start, 'I am a state', Time.now
|
62
|
+
# Specification.new :start, :description => 'Hello World', :timestamp => 0
|
63
|
+
#
|
64
|
+
# @note If the specified timestamp is not a `Fixnum` (unixtime), the value is converted to a fixnum
|
65
|
+
# @raise [ArgumentError] when `timestamp` is not a `Time`, `String` or `Fixnum`
|
66
|
+
# @raise [ArgumentError] when `name` or `description` not specified
|
67
|
+
def initialize *args
|
68
|
+
opts = {}
|
69
|
+
while arg = args.shift do
|
70
|
+
if arg.is_a?(Hash) then
|
71
|
+
opts.update(arg)
|
72
|
+
else
|
73
|
+
key = [:name, :description, :timestamp].select{|k| !opts.key?(k)}.first
|
74
|
+
opts.update(key => arg) unless key.nil?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
opts = symbolize_hash(opts)
|
78
|
+
|
79
|
+
if opts.fetch(:none, false) then
|
80
|
+
@name = EMPTY_NAME
|
81
|
+
@description = EMPTY_DESCRIPTION
|
82
|
+
else
|
83
|
+
@name = ::Collins::Option(opts.delete(:name)).map{|s| s.to_sym}.get_or_else {
|
84
|
+
raise ArgumentError.new("Name not specified")
|
85
|
+
}
|
86
|
+
@description = ::Collins::Option(opts.delete(:description)).get_or_else {
|
87
|
+
raise ArgumentError.new("Description not specified")
|
88
|
+
}
|
89
|
+
end
|
90
|
+
ts = ::Collins::Option(opts.delete(:timestamp)).get_or_else(Time.at(0))
|
91
|
+
@timestamp = parse_timestamp(ts)
|
92
|
+
# Flatten if needed
|
93
|
+
if opts.key?(:extras) then
|
94
|
+
@extras = opts[:extras]
|
95
|
+
else
|
96
|
+
@extras = opts
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# merges appropriate extras from the other spec into this one
|
101
|
+
# @param [Collins::State::Specification] other
|
102
|
+
def merge other
|
103
|
+
ext = other.extras.merge(@extras)
|
104
|
+
ext.delete(:none)
|
105
|
+
@extras = ext
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [Boolean] Indicate whether Specification is empty or not
|
110
|
+
def empty?
|
111
|
+
!self.defined?
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [Boolean] Indicate whether Specification is defined or not
|
115
|
+
def defined?
|
116
|
+
@name != EMPTY_NAME || @description != EMPTY_DESCRIPTION
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [Collins::Option] None if undefined/empty
|
120
|
+
def to_option
|
121
|
+
if self.defined? then
|
122
|
+
::Collins::Some(self)
|
123
|
+
else
|
124
|
+
::Collins::None()
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def [](key)
|
129
|
+
@extras[key.to_sym]
|
130
|
+
end
|
131
|
+
def key?(key)
|
132
|
+
@extras.key?(key.to_sym)
|
133
|
+
end
|
134
|
+
def fetch(key, default)
|
135
|
+
@extras.fetch(key.to_sym, default)
|
136
|
+
end
|
137
|
+
def []=(key, value)
|
138
|
+
@extras[key.to_sym] = value
|
139
|
+
end
|
140
|
+
|
141
|
+
def <<(key, value)
|
142
|
+
@extras[key.to_sym] = [] unless @extras.key?(key.to_sym)
|
143
|
+
@extras[key.to_sym] << value
|
144
|
+
@extras[key.to_sym]
|
145
|
+
end
|
146
|
+
|
147
|
+
# Convert this instance to JSON
|
148
|
+
#
|
149
|
+
# @note this is required by the JSON module
|
150
|
+
# @return [String] JSON string representation of object
|
151
|
+
def to_json(*a)
|
152
|
+
{
|
153
|
+
'json_class' => self.class.name,
|
154
|
+
'data' => to_hash
|
155
|
+
}.to_json(*a)
|
156
|
+
end
|
157
|
+
|
158
|
+
# @return [Hash] Hash representation of data
|
159
|
+
def to_hash
|
160
|
+
h = Hash[:name => name, :description => description, :timestamp => timestamp]
|
161
|
+
h[:extras] = extras unless extras.empty?
|
162
|
+
h
|
163
|
+
end
|
164
|
+
|
165
|
+
# @return [String] human readable
|
166
|
+
def to_s
|
167
|
+
"Specification(name = #{name}, description = #{description}, timestamp = #{timestamp}, extras = #{extras})"
|
168
|
+
end
|
169
|
+
|
170
|
+
# Mostly used for testing
|
171
|
+
def ==(other)
|
172
|
+
(other.class == self.class) &&
|
173
|
+
other.name == self.name &&
|
174
|
+
other.timestamp == self.timestamp
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
def parse_timestamp ts
|
179
|
+
if ts.is_a?(String) then
|
180
|
+
ts.to_s.to_i
|
181
|
+
elsif ts.is_a?(Time) then
|
182
|
+
ts.utc.to_i
|
183
|
+
elsif ts.is_a?(Fixnum) then
|
184
|
+
ts
|
185
|
+
else
|
186
|
+
raise ArgumentError.new("timestamp is not a String, Time, or Fixnum")
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
end # class Specification
|
191
|
+
|
192
|
+
end; end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'collins_state'
|
2
|
+
|
3
|
+
module Collins
|
4
|
+
class ProvisioningWorkflow < ::Collins::PersistentState
|
5
|
+
|
6
|
+
manage_state :provisioning_process, :initial => :start do |client|
|
7
|
+
|
8
|
+
action :reboot_hard do |asset, p|
|
9
|
+
p.logger.warn "Calling rebootHard on asset #{Collins::Util.get_asset_or_tag(asset).tag}"
|
10
|
+
client.power! asset, "rebootHard"
|
11
|
+
end
|
12
|
+
|
13
|
+
action :reprovision do |asset, p|
|
14
|
+
detailed = client.get asset
|
15
|
+
p.logger.warn "Reprovisioning #{detailed.tag}"
|
16
|
+
client.set_status! asset, "Maintenance"
|
17
|
+
client.provision asset, detailed.nodeclass, detailed.contact, :suffix => detailed.suffix,
|
18
|
+
:primary_role => detailed.primary_role, :secondary_role => detailed.secondary_role,
|
19
|
+
:pool => detailed.pool
|
20
|
+
end
|
21
|
+
|
22
|
+
action :toggle_status do |asset, p|
|
23
|
+
tag = Collins::Util.get_asset_or_tag(asset).tag
|
24
|
+
p.logger.warn "Toggling status (Provisioning then Provisioned) for #{tag}"
|
25
|
+
client.set_status!(asset, 'Provisioning') && client.set_status!(asset, 'Provisioned')
|
26
|
+
end
|
27
|
+
|
28
|
+
# No transitions are defined here, since they are managed fully asynchronously by the
|
29
|
+
# supervisor process. We only defines actions to take once the timeout has passed. Each
|
30
|
+
# part of the process makes an event call, the supervisor process just continuously does a
|
31
|
+
# collins.managed_process("ProvisioningWorkflow").transition(asset) which will
|
32
|
+
# either execute the on_transition action due to expiration or do nothing.
|
33
|
+
|
34
|
+
# After 30 minutes reprovision if still in the start state - ewr_start_provisioning - collins.managed_process("ProvisioningWorkflow").start(asset)
|
35
|
+
event :start, :desc => 'Provisioning Started', :expires => after(30, :minutes), :on_transition => :reprovision
|
36
|
+
# After 10 minutes if we haven't seen an ipxe request, reprovision (possible failed move) - vlan changer & provisioning status - collins.mp.vlan_moved_to_provisioning(asset)
|
37
|
+
event :vlan_moved_to_provisioning, :desc => 'Moved to provisioning VLAN', :expires => after(15, :minutes), :on_transition => :reprovision
|
38
|
+
# After 5 minutes if we haven't yet seen a kickstart request, reboot (possibly stuck at boot) - phil ipxe
|
39
|
+
event :ipxe_seen, :desc => 'Asset has made iPXE request to Phil', :expires => after(5, :minutes), :on_transition => :reboot_hard
|
40
|
+
# After 5 minutes if the kickstart process hasn't begun, reboot (possibly stuck at boot) - phil kickstart
|
41
|
+
event :kickstart_seen, :desc => 'Asset has made kickstart request to Phil', :expires => after(15, :minutes), :on_transition => :reboot_hard
|
42
|
+
# After 45 minutes if we haven't gotten to post, reboot to reinstall - phil kickstart pre
|
43
|
+
event :kickstart_started, :desc => 'Asset has started kickstart process', :expires => after(45, :minutes), :on_transition => :reboot_hard
|
44
|
+
# After 45 minutes if the kickstart process hasn't completed, reboot to reinstall - phil kickstart post
|
45
|
+
event :kickstart_post_started, :desc => 'Asset has started kickstart post section', :expires => after(45, :minutes), :on_transition => :reboot_hard
|
46
|
+
# After 10 minutes if we haven't been moved to the production VLAN, toggle our status to get us moved - phil kickstart post
|
47
|
+
event :kickstart_finished, :desc => 'Asset has finished kickstart process', :expires => after(15, :minutes), :on_transition => :toggle_status
|
48
|
+
# After another 15 minutes if still not moved toggle our status again - vlan changer when discover=true
|
49
|
+
event :vlan_move_to_production, :desc => 'Moved to production VLAN', :expires => after(15, :minutes), :on_transition => :toggle_status
|
50
|
+
# After another 10 minutes if we're not reachable by IP, toggle the status - supervisor
|
51
|
+
event :reachable_by_ip, :desc => 'Now reachable by IP address', :expires => after(10, :minutes), :on_transition => :toggle_status
|
52
|
+
# Place holder for when we finish
|
53
|
+
event :done, :desc => 'Finished', :terminus => true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: collins_state
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.10
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Blake Matheny
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: collins_client
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.2.7
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.2.7
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: escape
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.0.4
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.0.4
|
46
|
+
description: Provides basic framework for managing stateful processes with collins
|
47
|
+
email: bmatheny@tumblr.com
|
48
|
+
executables: []
|
49
|
+
extensions: []
|
50
|
+
extra_rdoc_files:
|
51
|
+
- README.md
|
52
|
+
files:
|
53
|
+
- Gemfile
|
54
|
+
- Gemfile.lock
|
55
|
+
- README.md
|
56
|
+
- Rakefile
|
57
|
+
- VERSION
|
58
|
+
- collins_state.gemspec
|
59
|
+
- lib/collins/persistent_state.rb
|
60
|
+
- lib/collins/state/mixin.rb
|
61
|
+
- lib/collins/state/mixin_class_methods.rb
|
62
|
+
- lib/collins/state/specification.rb
|
63
|
+
- lib/collins/workflows/provisioning_workflow.rb
|
64
|
+
- lib/collins_state.rb
|
65
|
+
homepage: https://github.com/tumblr/collins/tree/master/support/ruby/collins-state
|
66
|
+
licenses:
|
67
|
+
- APL 2.0
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
hash: -3397090300883848228
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
requirements: []
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.8.24
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Collins based state management
|
93
|
+
test_files: []
|