collins_state 0.2.10
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|