dynflow 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -12,6 +12,9 @@ written in Ruby that allows to:
12
12
  * extend the workflows from third-party libraries
13
13
  * keep consistency between local transactional database and
14
14
  external services
15
+ * suspend the long-running steps, not blocking the thread pool
16
+ * cancel steps when possible
17
+ * extend the actions behavior with middlewares
15
18
  * define the input/output interface between the building blocks (planned)
16
19
  * define rollback for the workflow (planned)
17
20
  * have multiple workers for distributing the load (planned)
@@ -22,8 +25,12 @@ persistence, transaction layer or executor implementation, giving you
22
25
  the last word in choosing the right one (providing default
23
26
  implementations as well).
24
27
 
28
+ ![Screenshot](doc/images/screenshot.png)
29
+
25
30
  * [Current status](#current-status)
26
31
  * [How it works](#how-it-works)
32
+ * [Examples](#examples)
33
+ * [The Anatomy of Action Class](#the-anatomy-of-action-class)
27
34
  * [Glossary](#glossary)
28
35
  * [Related projects](#related-projects)
29
36
 
@@ -35,12 +42,6 @@ to support the services orchestration in the
35
42
  [Katello](http://katello.org) and [Foreman](http://theforeman.org/)
36
43
  projects, getting to production-ready state in couple of weeks.
37
44
 
38
- Requirements
39
- ------------
40
-
41
- - Ruby MRI 1.9.3, 2.0, or 2.1.
42
- - It does not work on JRuby nor Rubinius yet.
43
-
44
45
  How it works
45
46
  ------------
46
47
 
@@ -90,14 +91,28 @@ The output of this phase is a set of actions and their inputs.
90
91
 
91
92
  Every action can participate in every phase.
92
93
 
93
- Example
94
- -------
94
+ Examples
95
+ --------
95
96
 
96
- One code snippet is worth 1000 words:
97
+ The `examples` directory contains simple ruby scripts different
98
+ features in action. You can just run the example files and see the Dynflow
99
+ in action.
100
+
101
+ * `orchestrate.rb` - example worlflow of getting some infrastructure
102
+ up and running, with ability to rescue from some error states.
103
+
104
+ * `orchestrate_evented.rb` - the same workflow using the ability to
105
+ suspend/wakeup actions while waiting for some external event.
106
+ It also demonstrates the ability to cancel actions that support it.
107
+
108
+ * `remote_executor.rb` - example of executing the flows in external
109
+ process
97
110
 
98
- ```ruby
99
- # The anatomy of action class
100
111
 
112
+ The Anatomy of Action Class
113
+ ---------------------------
114
+
115
+ ```ruby
101
116
  # every action needs to inherit from Dynflow::Action
102
117
  class Action < Dynflow::Action
103
118
 
@@ -151,7 +166,6 @@ class Action < Dynflow::Action
151
166
  end
152
167
  end
153
168
  ```
154
-
155
169
  Every action should be as atomic as possible, providing better
156
170
  granularity when manipulating the process. Since every action can be
157
171
  subscribed by another one, adding new behaviour to an existing
@@ -160,8 +174,6 @@ workflow is really simple.
160
174
  The input and output format can be used for defining the interface
161
175
  that other developers can use when extending the workflows.
162
176
 
163
- See the examples directory for more complete examples.
164
-
165
177
  Glossary
166
178
  --------
167
179
 
@@ -208,6 +220,14 @@ Related projects
208
220
  for running system tasks with Dynflow, comes with simple Web-UI for
209
221
  testing it
210
222
 
223
+
224
+ Requirements
225
+ ------------
226
+
227
+ - Ruby MRI 1.9.3, 2.0, or 2.1.
228
+ - It does not work on JRuby nor Rubinius yet.
229
+
230
+
211
231
  License
212
232
  -------
213
233
 
Binary file
@@ -0,0 +1,47 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+
3
+ require 'dynflow'
4
+
5
+ class ExampleHelper
6
+ class << self
7
+ def world
8
+ @world ||= create_world
9
+ end
10
+
11
+ def create_world(options = {})
12
+ options = default_world_options.merge(options)
13
+ Dynflow::SimpleWorld.new(options)
14
+ end
15
+
16
+ def default_world_options
17
+ { logger_adapter: logger_adapter }
18
+ end
19
+
20
+ def logger_adapter
21
+ Dynflow::LoggerAdapters::Simple.new $stderr, 4
22
+ end
23
+
24
+
25
+ def run_web_console(world = ExampleHelper.world)
26
+ require 'dynflow/web_console'
27
+ dynflow_console = Dynflow::WebConsole.setup do
28
+ set :world, world
29
+ end
30
+ dynflow_console.run!
31
+ end
32
+
33
+ # for simulation of the execution failing for the first time
34
+ def something_should_fail!
35
+ @should_fail = true
36
+ end
37
+
38
+ # for simulation of the execution failing for the first time
39
+ def something_should_fail?
40
+ @should_fail
41
+ end
42
+
43
+ def nothing_should_fail!
44
+ @should_fail = false
45
+ end
46
+ end
47
+ end
@@ -1,7 +1,30 @@
1
- # Simple example on how Dynflow could be used for simple infrastructure orchestration
1
+ #!/usr/bin/env ruby
2
+
3
+ example_description = <<DESC
4
+ Orchestrate Example
5
+ ===================
6
+
7
+ This example simulates a workflow of setting up an infrastructure, using
8
+ more high-level steps in CreateInfrastructure, that expand to smaller steps
9
+ of PrepareDisk, CraeteVM etc.
10
+
11
+ It shows the possibility to run the independend actions concurrently, chaining
12
+ the actions (passing the output of PrepareDisk action to CreateVM, automatically
13
+ detecting the dependency making sure to run the one before the other).
14
+
15
+ It also simulates a failure and demonstrates the Dynflow ability to rescue
16
+ from the error and consinue with the run.
17
+
18
+ Once the Sinatra web console starts, you can navigate to http://localhost:4567
19
+ to see what's happening in the Dynflow world.
20
+
21
+ DESC
22
+
23
+ require_relative 'example_helper'
2
24
 
3
25
  module Orchestrate
4
26
 
27
+
5
28
  class CreateInfrastructure < Dynflow::Action
6
29
 
7
30
  def plan
@@ -16,7 +39,6 @@ module Orchestrate
16
39
  :db_machine => 'host1',
17
40
  :storage_machine => 'host2')
18
41
  end
19
- sleep 2
20
42
  end
21
43
  end
22
44
 
@@ -36,12 +58,19 @@ module Orchestrate
36
58
  end
37
59
 
38
60
  def finalize
39
- puts "We've create a machine #{input[:name]}"
61
+ # this is called after run methods of the actions in the
62
+ # execution plan were finished
40
63
  end
41
64
 
42
65
  end
43
66
 
44
- class PrepareDisk < Dynflow::Action
67
+ class Base < Dynflow::Action
68
+ def sleep!
69
+ sleep(rand(2))
70
+ end
71
+ end
72
+
73
+ class PrepareDisk < Base
45
74
 
46
75
  input_format do
47
76
  param :name
@@ -52,13 +81,13 @@ module Orchestrate
52
81
  end
53
82
 
54
83
  def run
55
- sleep(rand(5))
84
+ sleep!
56
85
  output[:path] = "/var/images/#{input[:name]}.img"
57
86
  end
58
87
 
59
88
  end
60
89
 
61
- class CreateVM < Dynflow::Action
90
+ class CreateVM < Base
62
91
 
63
92
  input_format do
64
93
  param :name
@@ -70,25 +99,25 @@ module Orchestrate
70
99
  end
71
100
 
72
101
  def run
73
- sleep(rand(5))
102
+ sleep!
74
103
  output[:ip] = "192.168.100.#{rand(256)}"
75
104
  end
76
105
 
77
106
  end
78
107
 
79
- class AddIPtoHosts < Dynflow::Action
108
+ class AddIPtoHosts < Base
80
109
 
81
110
  input_format do
82
111
  param :ip
83
112
  end
84
113
 
85
114
  def run
86
- sleep(rand(5))
115
+ sleep!
87
116
  end
88
117
 
89
118
  end
90
119
 
91
- class ConfigureMachine < Dynflow::Action
120
+ class ConfigureMachine < Base
92
121
 
93
122
  input_format do
94
123
  param :ip
@@ -98,24 +127,31 @@ module Orchestrate
98
127
 
99
128
  def run
100
129
  # for demonstration of resuming after error
101
- if Orchestrate.should_fail?
102
- Orchestrate.should_pass!
130
+ if ExampleHelper.something_should_fail?
131
+ ExampleHelper.nothing_should_fail!
132
+ puts <<-MSG.gsub(/^.*\|/, '')
133
+
134
+ | Execution plan #{execution_plan_id} is failing
135
+ | You can resume it at http://localhost:4567/#{execution_plan_id}
136
+
137
+ MSG
103
138
  raise "temporary unavailabe"
104
139
  end
105
140
 
106
- sleep(rand(5))
141
+ sleep!
107
142
  end
108
143
 
109
144
  end
145
+ end
110
146
 
111
-
112
- # for simulation of the execution failing for the first time
113
- def self.should_fail?
114
- ! @should_pass
115
- end
116
-
117
- def self.should_pass!
118
- @should_pass = true
147
+ if $0 == __FILE__
148
+ ExampleHelper.something_should_fail!
149
+ ExampleHelper.world.trigger(Orchestrate::CreateInfrastructure)
150
+ Thread.new do
151
+ 9.times do
152
+ ExampleHelper.world.trigger(Orchestrate::CreateInfrastructure)
153
+ end
119
154
  end
120
-
155
+ puts example_description
156
+ ExampleHelper.run_web_console
121
157
  end
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative 'example_helper'
4
+
5
+ example_description = <<DESC
6
+ Orchestrate Evented Example
7
+ ===========================
8
+
9
+ This example, how the `examples/orchestrate.rb` can be updated to not block
10
+ the threads while waiting for external tasks. In this cases, we usually wait
11
+ most of the time: and we can suspend the run of the action while waiting,
12
+ for the event. Therefore we suspend the action in the run, ask the world.clock
13
+ to wake us up few seconds later, so that the thread pool can do something useful
14
+ in the meantime.
15
+
16
+ Additional benefit besides being able to do more while waiting is allowing to
17
+ send external events to the action while it's suspended. One use case is being
18
+ able to cancel the action while it's running.
19
+
20
+ Once the Sinatra web console starts, you can navigate to http://localhost:4567
21
+ to see what's happening in the Dynflow world.
22
+
23
+ DESC
24
+
25
+ module OrchestrateEvented
26
+
27
+ class CreateInfrastructure < Dynflow::Action
28
+
29
+ def plan(get_stuck = false)
30
+ sequence do
31
+ concurrence do
32
+ plan_action(CreateMachine, 'host1', 'db', get_stuck: get_stuck)
33
+ plan_action(CreateMachine, 'host2', 'storage')
34
+ end
35
+ plan_action(CreateMachine,
36
+ 'host3',
37
+ 'web_server',
38
+ :db_machine => 'host1',
39
+ :storage_machine => 'host2')
40
+ end
41
+ end
42
+ end
43
+
44
+ class CreateMachine < Dynflow::Action
45
+
46
+ def plan(name, profile, config_options = {})
47
+ prepare_disk = plan_action(PrepareDisk, 'name' => name)
48
+ create_vm = plan_action(CreateVM,
49
+ :name => name,
50
+ :disk => prepare_disk.output['path'])
51
+ plan_action(AddIPtoHosts, :name => name, :ip => create_vm.output[:ip])
52
+ plan_action(ConfigureMachine,
53
+ :ip => create_vm.output[:ip],
54
+ :profile => profile,
55
+ :config_options => config_options)
56
+ plan_self(:name => name)
57
+ end
58
+
59
+ def finalize
60
+ end
61
+
62
+ end
63
+
64
+ class Base < Dynflow::Action
65
+
66
+ Finished = Algebrick.atom
67
+
68
+ def run(event = nil)
69
+ match(event,
70
+ (on Finished do
71
+ on_finish
72
+ end),
73
+ (on Dynflow::Action::Skip do
74
+ # do nothing
75
+ end),
76
+ (on nil do
77
+ suspend { |suspended_action| world.clock.ping suspended_action, rand(1), Finished }
78
+ end))
79
+ end
80
+
81
+ def on_finish
82
+ raise NotImplementedError
83
+ end
84
+
85
+ end
86
+
87
+ class PrepareDisk < Base
88
+
89
+ input_format do
90
+ param :name
91
+ end
92
+
93
+ output_format do
94
+ param :path
95
+ end
96
+
97
+ def on_finish
98
+ output[:path] = "/var/images/#{input[:name]}.img"
99
+ end
100
+
101
+ end
102
+
103
+ class CreateVM < Base
104
+
105
+ input_format do
106
+ param :name
107
+ param :disk
108
+ end
109
+
110
+ output_format do
111
+ param :ip
112
+ end
113
+
114
+ def on_finish
115
+ output[:ip] = "192.168.100.#{rand(256)}"
116
+ end
117
+
118
+ end
119
+
120
+ class AddIPtoHosts < Base
121
+
122
+ input_format do
123
+ param :ip
124
+ end
125
+
126
+ def on_finish
127
+ end
128
+
129
+ end
130
+
131
+ class ConfigureMachine < Base
132
+
133
+ # thanks to this Dynflow knows this action can be politely
134
+ # asked to get canceled
135
+ include ::Dynflow::Action::Cancellable
136
+
137
+ input_format do
138
+ param :ip
139
+ param :profile
140
+ param :config_options
141
+ end
142
+
143
+ def run(event = nil)
144
+ if event == Dynflow::Action::Cancellable::Cancel
145
+ output[:message] = "I was cancelled but we don't care"
146
+ else
147
+ super
148
+ end
149
+ end
150
+
151
+ def on_finish
152
+ if input[:config_options][:get_stuck]
153
+ puts <<-MSG.gsub(/^.*\|/, '')
154
+
155
+ | Execution plan #{execution_plan_id} got stuck
156
+ | You can cancel the stucked step at http://localhost:4567/#{execution_plan_id}
157
+
158
+ MSG
159
+ # we suspend the action but don't plan the wakeup event,
160
+ # causing it to wait forever (till we cancel it)
161
+ suspend
162
+ end
163
+ end
164
+
165
+ end
166
+
167
+ end
168
+
169
+ if $0 == __FILE__
170
+ ExampleHelper.world.trigger(OrchestrateEvented::CreateInfrastructure)
171
+ ExampleHelper.world.trigger(OrchestrateEvented::CreateInfrastructure, true)
172
+ puts example_description
173
+ ExampleHelper.run_web_console
174
+ end