pd-blender 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rubocop.yml +2 -0
- data/.travis.yml +10 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +14 -0
- data/README.md +342 -0
- data/Rakefile +21 -0
- data/bin/blend +20 -0
- data/blender.gemspec +36 -0
- data/lib/blender.rb +67 -0
- data/lib/blender/cli.rb +71 -0
- data/lib/blender/configuration.rb +45 -0
- data/lib/blender/discovery.rb +41 -0
- data/lib/blender/drivers/base.rb +40 -0
- data/lib/blender/drivers/compound.rb +29 -0
- data/lib/blender/drivers/ruby.rb +55 -0
- data/lib/blender/drivers/shellout.rb +63 -0
- data/lib/blender/drivers/ssh.rb +93 -0
- data/lib/blender/drivers/ssh_multi.rb +102 -0
- data/lib/blender/event_dispatcher.rb +45 -0
- data/lib/blender/exceptions.rb +26 -0
- data/lib/blender/handlers/base.rb +39 -0
- data/lib/blender/handlers/doc.rb +73 -0
- data/lib/blender/job.rb +73 -0
- data/lib/blender/lock/flock.rb +64 -0
- data/lib/blender/log.rb +24 -0
- data/lib/blender/rspec.rb +68 -0
- data/lib/blender/rspec/stub_registry.rb +45 -0
- data/lib/blender/scheduled_job.rb +66 -0
- data/lib/blender/scheduler.rb +114 -0
- data/lib/blender/scheduler/dsl.rb +160 -0
- data/lib/blender/scheduling_strategies/base.rb +30 -0
- data/lib/blender/scheduling_strategies/default.rb +37 -0
- data/lib/blender/scheduling_strategies/per_host.rb +38 -0
- data/lib/blender/scheduling_strategies/per_task.rb +37 -0
- data/lib/blender/tasks/base.rb +72 -0
- data/lib/blender/tasks/ruby.rb +31 -0
- data/lib/blender/tasks/shell_out.rb +30 -0
- data/lib/blender/tasks/ssh.rb +25 -0
- data/lib/blender/timer.rb +54 -0
- data/lib/blender/utils/refinements.rb +45 -0
- data/lib/blender/utils/thread_pool.rb +54 -0
- data/lib/blender/utils/ui.rb +51 -0
- data/lib/blender/version.rb +20 -0
- data/spec/blender/blender_rspec.rb +31 -0
- data/spec/blender/discovery_spec.rb +16 -0
- data/spec/blender/drivers/ssh_multi_spec.rb +16 -0
- data/spec/blender/drivers/ssh_spec.rb +17 -0
- data/spec/blender/dsl_spec.rb +19 -0
- data/spec/blender/event_dispatcher_spec.rb +17 -0
- data/spec/blender/job_spec.rb +42 -0
- data/spec/blender/lock_spec.rb +129 -0
- data/spec/blender/scheduled_job_spec.rb +30 -0
- data/spec/blender/scheduler_spec.rb +140 -0
- data/spec/blender/scheduling_strategies/default_spec.rb +75 -0
- data/spec/blender/utils/refinements_spec.rb +16 -0
- data/spec/blender/utils/thread_pool_spec.rb +16 -0
- data/spec/blender_spec.rb +37 -0
- data/spec/data/example.rb +12 -0
- data/spec/spec_helper.rb +35 -0
- metadata +304 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8b9983c59a3ba2c697b2d11c20cdb4d8a8a236f8
|
4
|
+
data.tar.gz: 979315d46ed457760f89ff5692b475b69e94cfc5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5019047e53c66b01505a3a77c037f79fb395b342b5e3962dd33edb10e97803a20f0da5bc75211e65b64f9ac29211db6317af3a386ebc665f18a079e7f964375d
|
7
|
+
data.tar.gz: 89107f6c920895540a00093cde7eb03df1e1a3f160935ebf75e1afb8d6a4eb4cb156427bd2cb78802ad3d5c74755e39413d76f3e5ef6ec6446ad9c0b7886c09c
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Copyright:: Copyright (c) 2014 PagerDuty, Inc.
|
2
|
+
License:: Apache License, Version 2.0
|
3
|
+
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
you may not use this file except in compliance with the License.
|
6
|
+
You may obtain a copy of the License at
|
7
|
+
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
See the License for the specific language governing permissions and
|
14
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,342 @@
|
|
1
|
+
[![Built on Travis](https://secure.travis-ci.org/PagerDuty/blender.png?branch=master)](http://travis-ci.org/PagerDuty/blender)
|
2
|
+
# Blender
|
3
|
+
|
4
|
+
Blender is a modular remote command execution framework. Blender provides few basic
|
5
|
+
primitives to automate cross server workflows. Workflows can be expressed in plain
|
6
|
+
ruby DSL and executed using the CLI.
|
7
|
+
|
8
|
+
Following is an example of a simple blender script that will update the package
|
9
|
+
index of three ubuntu servers.
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
# example.rb
|
13
|
+
ssh_task 'update' do
|
14
|
+
execute 'sudo apt-get update -y'
|
15
|
+
members ['ubuntu01', 'ubuntu02', 'ubuntu03']
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
Which can execute it as:
|
20
|
+
```sh
|
21
|
+
blend -f example.rb
|
22
|
+
```
|
23
|
+
Output:
|
24
|
+
```
|
25
|
+
Run[example.rb] started
|
26
|
+
3 job(s) computed using 'Default' strategy
|
27
|
+
Job 1 [update on ubuntu01] finished
|
28
|
+
Job 2 [update on ubuntu02] finished
|
29
|
+
Job 3 [update on ubuntu03] finished
|
30
|
+
Run finished (42.228923876 s)
|
31
|
+
```
|
32
|
+
An workflow can have multiple tasks, individual tasks can have different members
|
33
|
+
which can be run in parallel.
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
# example.rb
|
37
|
+
ssh_task 'update' do
|
38
|
+
execute 'sudo apt-get update -y'
|
39
|
+
members ['ubuntu01', 'ubuntu02', 'ubuntu03']
|
40
|
+
end
|
41
|
+
|
42
|
+
ssh_task 'install' do
|
43
|
+
execute 'sudo apt-get install screen -y'
|
44
|
+
members ['ubuntu01', 'ubuntu03']
|
45
|
+
end
|
46
|
+
|
47
|
+
concurrency 2
|
48
|
+
```
|
49
|
+
Output:
|
50
|
+
```sh
|
51
|
+
Run[blends/example.rb] started
|
52
|
+
5 job(s) computed using 'Default' strategy
|
53
|
+
Job 1 [update on ubuntu01] finished
|
54
|
+
Job 2 [update on ubuntu02] finished
|
55
|
+
Job 4 [install on ubuntu01] finished
|
56
|
+
Job 3 [update on ubuntu03] finished
|
57
|
+
Job 5 [install on ubuntu03] finished
|
58
|
+
Run finished (4.462043017 s)
|
59
|
+
```
|
60
|
+
|
61
|
+
Blender provides various types of task execution (like arbitrary ruby code,
|
62
|
+
commands over ssh, serf handlers etc) which can ease automating large cluster
|
63
|
+
maintenance, multi stage provisioning, establishing cross server feedback
|
64
|
+
loops etc.
|
65
|
+
|
66
|
+
## Concepts
|
67
|
+
|
68
|
+
Blender is composed of two components:
|
69
|
+
|
70
|
+
* **Tasks and drivers** - Tasks encapsulate commands (or equivalent abstraction). A blender
|
71
|
+
script can have multiple tasks. Tasks are executed using drivers. Tasks can declare their
|
72
|
+
target hosts.
|
73
|
+
|
74
|
+
* **Scheduling stratgy** - Determines the order of task execution across the hosts.
|
75
|
+
Every blender scripts has one and only one scheduling strategy. Scheduling strategies
|
76
|
+
uses the task list as input and produces a list of jobs, to be executed using drivers.
|
77
|
+
|
78
|
+
|
79
|
+
### Tasks
|
80
|
+
|
81
|
+
Tasks and drivers compliment each other. Tasks act as front end, where we declare
|
82
|
+
what needs to be done, while drivers are used to interpret how those tasks can be done.
|
83
|
+
For example `ssh_task` can be used to declare tasks, while `ssh` and `ssh_multi` driver
|
84
|
+
can execute `ssh_task`s. Blender core ships with following tasks and drivers:
|
85
|
+
|
86
|
+
- **shell_task**: execute commands on current host. shell tasks can only have 'localhost'
|
87
|
+
as its members. presence of any other hosts in members list will raise exception. shell_tasks
|
88
|
+
are executed using shell_out driver.
|
89
|
+
Example:
|
90
|
+
```ruby
|
91
|
+
shell_task 'foo' do
|
92
|
+
execute 'sudo apt-get update -y'
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
- **ruby_task**: execute ruby blocks against current host. host names from members list is passed
|
97
|
+
to the block. ruby_tasks are executed using `Blender::Ruby` driver.
|
98
|
+
Example:
|
99
|
+
```ruby
|
100
|
+
ruby_task 'baz' do
|
101
|
+
execute do |host|
|
102
|
+
puts "Host name is: #{host}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
- **ssh_task**: execute commands against remote hosts using ssh. Blender ships with two ssh drivers,
|
108
|
+
one based on a vanilla Ruby `net-ssh` binding, another based on `net-ssh-multi` (which supports parallel
|
109
|
+
execution)
|
110
|
+
Example:
|
111
|
+
```ruby
|
112
|
+
ssh_task 'bar' do
|
113
|
+
execute 'sudo apt-get update -y'
|
114
|
+
members ['host1', 'host2']
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
As mentioned earlier tasks are executed using drivers. Tasks can declare their preferred driver or
|
119
|
+
Blender will assign a driver to them automatically. Blender will reuse the global driver if its
|
120
|
+
compatible, else it will create one. By default the ```global_driver``` is a ```shell_out``` driver.
|
121
|
+
Drivers can expose host concurrency, stdout/stderr streaming and various other customizations,
|
122
|
+
specific to their own implementations.
|
123
|
+
|
124
|
+
### Scheduling strategies
|
125
|
+
|
126
|
+
Scheduling strategies are the most crucial part of a blender script. They decide the
|
127
|
+
order of command execution across distributed nodes in blender. Each blender script is
|
128
|
+
invoked using one strategy. Consider them as a transformation, where the input is tasks and ouput is
|
129
|
+
jobs. Tasks and job are pretty similar in their structures (both holds command and hosts),
|
130
|
+
except a jobs can hold multiple tasks within them. We'll come to this later, but first, lets
|
131
|
+
see how the default strategy work.
|
132
|
+
|
133
|
+
- **default strategy**: the default strategy takes the list of declared tasks (and associated members
|
134
|
+
in each tasks) breaks them up into per node jobs.
|
135
|
+
For example:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
members ['host1', 'host2', 'host3']
|
139
|
+
|
140
|
+
ruby_task 'test' do
|
141
|
+
execute do |host|
|
142
|
+
Blender::Log.info(host)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
will result in 3 jobs. each with `ruby_task[test]` on host1, `ruby_task[test]` on host2 and
|
148
|
+
`ruby_task[test]` on host3. And then these three tasks will be executed serially.
|
149
|
+
Following will create 6 jobs.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
members ['host1', 'host2', 'host3']
|
153
|
+
|
154
|
+
ruby_task 'test 1' do
|
155
|
+
execute do |host|
|
156
|
+
Blender::Log.info("test 1 on #{host}")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
ruby_task 'test 2' do
|
161
|
+
execute do |host|
|
162
|
+
Blender::Log.info("test 2 on #{host}")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
```
|
166
|
+
|
167
|
+
While the next one will create 4 jobs (second task will give only one job).
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
members ['host1', 'host2', 'host3']
|
171
|
+
|
172
|
+
ruby_task 'test 1' do
|
173
|
+
execute do |host|
|
174
|
+
Blender::Log.info("test 1 on #{host}")
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
ruby_task 'test 2' do
|
179
|
+
execute do |host|
|
180
|
+
Blender::Log.info("test 2 on #{host}")
|
181
|
+
end
|
182
|
+
members ['host3']
|
183
|
+
end
|
184
|
+
```
|
185
|
+
The default strategy is conservative, and allows drivers that work against a single remote
|
186
|
+
host to be integrated with blender. Also this allows the highest level of fine grain job control.
|
187
|
+
|
188
|
+
Apart from the default strategy, Blender ships with two more strategy, they are:
|
189
|
+
|
190
|
+
- **per task strategy**: this creates one job per task. Following example will
|
191
|
+
create 2 jobs, each with three hosts and one of the `ruby_task` in them.
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
members ['host1', 'host2', 'host3']
|
195
|
+
|
196
|
+
strategy :per_task
|
197
|
+
|
198
|
+
ruby_task 'test 1' do
|
199
|
+
execute do |host|
|
200
|
+
Blender::Log.info("test 1 on #{host}")
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
ruby_task 'test 2' do
|
205
|
+
execute do |host|
|
206
|
+
Blender::Log.info("test 2 on #{host}")
|
207
|
+
end
|
208
|
+
end
|
209
|
+
```
|
210
|
+
|
211
|
+
per task strategy allows drivers to optimize individual command execution accross multiple hosts. For
|
212
|
+
example `ssh_multi` driver allows parallel command execution across many hosts. And can be used
|
213
|
+
as:
|
214
|
+
```ruby
|
215
|
+
strategy :per_task
|
216
|
+
global_driver(:ssh_multi, concurrency: 50)
|
217
|
+
ssh_task 'run chef' do
|
218
|
+
execute 'sudo chef-client --no-fork'
|
219
|
+
end
|
220
|
+
```
|
221
|
+
Note: if we use the default strategy, ssh_multi driver wont be able to leverage its
|
222
|
+
concurrency features, as the resultant jobs (the driver will receive) will have only one host.
|
223
|
+
|
224
|
+
- **per host strategy**: it creates one job per host. Following example will create
|
225
|
+
3 jobs. each with one host and 2 ruby tasks. Thus two tasks will be executed in one
|
226
|
+
host, then on the next one.. follow on. Think of deployments with rolling restart like
|
227
|
+
scenarios. This also allows drivers to optimize multiple tasks/commandsi execution
|
228
|
+
against individual hosts (session reuse etc).
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
strategy :per_host
|
232
|
+
members ['host1', 'host2', 'host3']
|
233
|
+
|
234
|
+
ruby_task 'test 1' do
|
235
|
+
execute do |host|
|
236
|
+
Blender::Log.info("test 1 on #{host}")
|
237
|
+
end
|
238
|
+
end
|
239
|
+
ruby_task 'test 2' do
|
240
|
+
execute do |host|
|
241
|
+
Blender::Log.info("test 2 on #{host}")
|
242
|
+
end
|
243
|
+
end
|
244
|
+
```
|
245
|
+
Note: this strategy does not work if you have different hosts per tasks.
|
246
|
+
|
247
|
+
Its fairly easy to write custom scheduling strategies and they can be used to rewrite or
|
248
|
+
rearrange hosts/tasks as you wish. For example, null strategy that return 0 jobs irrespective
|
249
|
+
of what tasks or members you pass, or a custome strategy that takes the hosts lists of every
|
250
|
+
tasks and considers only one of them dynamically based on some metrics for jobs, etc.
|
251
|
+
|
252
|
+
### Host discovery
|
253
|
+
|
254
|
+
For workflows that depends on dynamic infrastructure, where host names are changing,
|
255
|
+
Blender provides abstractions that facilitate discovering them.
|
256
|
+
[blender-chef](https://github.com/PagerDuty/blender-chef) and
|
257
|
+
[blender-serf](https://github.com/PagerDuty/blender-serf) uses this and allows remote job orchestration
|
258
|
+
for chef or serf managed infrastructure.
|
259
|
+
|
260
|
+
Following are some examples:
|
261
|
+
|
262
|
+
- **serf**: discover hosts using serf membership
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
require 'blender/serf'
|
266
|
+
|
267
|
+
ruby_task 'print host name' do
|
268
|
+
execute do |host|
|
269
|
+
Blender::Log.info("Host: #{host}")
|
270
|
+
end
|
271
|
+
members search(:serf, name: '^lt-.*$')
|
272
|
+
end
|
273
|
+
```
|
274
|
+
|
275
|
+
- **chef**: discover hosts using Chef search
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
require 'blender/dscoveries/chef'
|
279
|
+
|
280
|
+
ruby_task 'print host name' do
|
281
|
+
execute do |host|
|
282
|
+
Blender::Log.info("Host: #{host}")
|
283
|
+
end
|
284
|
+
members search(:chef, 'roles:web')
|
285
|
+
end
|
286
|
+
```
|
287
|
+
|
288
|
+
## Invoking blender periodially with Rufus schedler
|
289
|
+
|
290
|
+
Blender is designed to be used as a standalone script that can be invoked on-demand or
|
291
|
+
consumed as a library, i.e. workflows are written in plain Ruby objects and invoked
|
292
|
+
from other tools or application. Apart from these, Blender can be use for periodic
|
293
|
+
job execution also. Underneath it uses `Rufus::Scheduler` to trigger Blender run, after
|
294
|
+
a fixed interval (can be expressed via cron syntax as well, thanks to Rufus).
|
295
|
+
|
296
|
+
Following will run `example.rb` blender script after every 4 hours.
|
297
|
+
```ruby
|
298
|
+
schedule '/path/to/example.rb' do
|
299
|
+
cron '* */4 * * *'
|
300
|
+
end
|
301
|
+
```
|
302
|
+
|
303
|
+
## Ignore failure
|
304
|
+
|
305
|
+
Blender will fail the execution immediately if any of the job fails. `ignore_failure`
|
306
|
+
attribute can be used to proceed execution even after failure. This can be declared
|
307
|
+
both per task level as well as globally.
|
308
|
+
|
309
|
+
```ruby
|
310
|
+
shell_task 'fail' do
|
311
|
+
command 'ls /does/not/exists'
|
312
|
+
ignore_failure true
|
313
|
+
end
|
314
|
+
shell_task 'will be executed' do
|
315
|
+
command 'echo "Thrust is what we need"'
|
316
|
+
end
|
317
|
+
```
|
318
|
+
|
319
|
+
|
320
|
+
## Event handlers
|
321
|
+
|
322
|
+
Blender provides an event dispatchment facility (inspired from Chef), where arbitrary logic can
|
323
|
+
be hooked into the event system (e.g. HipChat notification handlers, statsd handlers, etc) and blender will automatically invoke them during key events. As of now, events are available before and after run and per job execution. Event dispatch system is likely to get more elaborate and blender might have few common event handlers (metric, notifications etc) in near future.
|
324
|
+
|
325
|
+
## Supported ruby versions
|
326
|
+
|
327
|
+
Blender currently support the following Ruby implementations:
|
328
|
+
|
329
|
+
* *Ruby 1.9.3*
|
330
|
+
* *Ruby 2.1.0*
|
331
|
+
* *Ruby 2.1.2*
|
332
|
+
|
333
|
+
## License
|
334
|
+
[Apache 2](http://www.apache.org/licenses/LICENSE-2.0)
|
335
|
+
|
336
|
+
## Contributing
|
337
|
+
|
338
|
+
1. Fork it ( https://github.com/PagerDuty/blender/fork )
|
339
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
340
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
341
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
342
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rubocop/rake_task'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
require 'yard'
|
5
|
+
|
6
|
+
YARD::Rake::YardocTask.new do |t|
|
7
|
+
t.files = ['lib/**/*.rb']
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
11
|
+
t.pattern = %w{spec/**/*_spec.rb}
|
12
|
+
end
|
13
|
+
|
14
|
+
RSpec::Core::RakeTask.new(:rspec) do |t|
|
15
|
+
t.pattern = %w{spec/**/*_rspec.rb}
|
16
|
+
end
|
17
|
+
|
18
|
+
RuboCop::RakeTask.new(:rubocop) do |t|
|
19
|
+
t.patterns = %w{Rakefile Gemfile lib/**/*.rb}
|
20
|
+
t.fail_on_error = true
|
21
|
+
end
|
data/bin/blend
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Author:: Ranjib Dey (<ranjib@pagerduty.com>)
|
4
|
+
# Copyright:: Copyright (c) 2014 PagerDuty, Inc.
|
5
|
+
# License:: Apache License, Version 2.0
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
require 'blender/cli'
|
20
|
+
Blender::CLI.start(ARGV.dup)
|