orca 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +29 -0
- data/LICENSE +7 -0
- data/README.md +109 -0
- data/Rakefile +9 -0
- data/bin/orca +6 -0
- data/config/template/files/.empty_directory +0 -0
- data/config/template/orca.rb +28 -0
- data/lib/orca.rb +48 -0
- data/lib/orca/cli.rb +53 -0
- data/lib/orca/dsl.rb +18 -0
- data/lib/orca/execution_context.rb +89 -0
- data/lib/orca/extensions/apt.rb +54 -0
- data/lib/orca/extensions/file_sync.rb +100 -0
- data/lib/orca/local_file.rb +77 -0
- data/lib/orca/node.rb +83 -0
- data/lib/orca/package.rb +52 -0
- data/lib/orca/package_index.rb +37 -0
- data/lib/orca/remote_file.rb +104 -0
- data/lib/orca/resolver.rb +37 -0
- data/lib/orca/runner.rb +81 -0
- data/lib/orca/suite.rb +18 -0
- data/orca.gemspec +18 -0
- data/test/dsl_test.rb +35 -0
- data/test/fixtures/example.txt +1 -0
- data/test/local_file_test.rb +59 -0
- data/test/package_index_test.rb +48 -0
- data/test/package_test.rb +51 -0
- data/test/remote_file_test.rb +91 -0
- data/test/resolver_test.rb +61 -0
- data/test/test_helper.rb +8 -0
- metadata +150 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
class Orca::Resolver
|
2
|
+
attr_reader :packages, :tree
|
3
|
+
def initialize(package)
|
4
|
+
@package = package
|
5
|
+
@last_seen = package
|
6
|
+
@tree = [@package]
|
7
|
+
@packages = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def resolve
|
11
|
+
dependancies = @package.dependancies.reverse.map { |d| Orca::PackageIndex.default.get(d) }
|
12
|
+
begin
|
13
|
+
@tree += dependancies.map {|d| Orca::Resolver.new(d).resolve.tree }
|
14
|
+
rescue SystemStackError
|
15
|
+
raise CircularDependancyError.new
|
16
|
+
end
|
17
|
+
@packages = @tree.flatten
|
18
|
+
@packages.reverse!
|
19
|
+
@packages.uniq!
|
20
|
+
add_children
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_children
|
25
|
+
@packages = @packages.reduce([]) do |arr, package|
|
26
|
+
arr << package
|
27
|
+
package.children.each do |child_name|
|
28
|
+
child = Orca::PackageIndex.default.get(child_name)
|
29
|
+
arr << child unless arr.include?(child)
|
30
|
+
end
|
31
|
+
arr
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class CircularDependancyError < StandardError
|
36
|
+
end
|
37
|
+
end
|
data/lib/orca/runner.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
class Orca::Runner
|
2
|
+
def initialize(node, package)
|
3
|
+
@node = node
|
4
|
+
@package = package
|
5
|
+
@perform = true
|
6
|
+
end
|
7
|
+
|
8
|
+
def packages
|
9
|
+
resolver = Orca::Resolver.new(@package)
|
10
|
+
resolver.resolve
|
11
|
+
resolver.packages
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(command_name)
|
15
|
+
@node.log command_name, packages.map(&:name).join(', ').yellow
|
16
|
+
packages.each do |pkg|
|
17
|
+
send(:"execute_#{command_name}", pkg)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def execute_apply(pkg)
|
22
|
+
return unless should_run?(pkg, :apply)
|
23
|
+
exec(pkg, :apply)
|
24
|
+
validate!(pkg)
|
25
|
+
end
|
26
|
+
|
27
|
+
def execute_remove(pkg)
|
28
|
+
return unless should_run?(pkg, :remove)
|
29
|
+
exec(pkg, :remove)
|
30
|
+
end
|
31
|
+
|
32
|
+
def execute_validate(pkg)
|
33
|
+
validate!(pkg)
|
34
|
+
end
|
35
|
+
|
36
|
+
def should_run?(pkg, command_name)
|
37
|
+
return false unless pkg.provides_command?(command_name)
|
38
|
+
return true unless @perform
|
39
|
+
return true unless command_name == :apply || command_name == :remove
|
40
|
+
return true unless pkg.provides_command?(:validate)
|
41
|
+
is_present = is_valid?(pkg)
|
42
|
+
return !is_present if command_name == :apply
|
43
|
+
return is_present
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate!(pkg)
|
47
|
+
return true unless @perform
|
48
|
+
return unless pkg.provides_command?(:validate)
|
49
|
+
return if is_valid?(pkg)
|
50
|
+
raise ValidationFailureError.new(@node, pkg)
|
51
|
+
end
|
52
|
+
|
53
|
+
def is_valid?(pkg)
|
54
|
+
results = exec(pkg, :validate)
|
55
|
+
results.all?
|
56
|
+
end
|
57
|
+
|
58
|
+
def demonstrate(command_name)
|
59
|
+
@perform = false
|
60
|
+
execute(command_name)
|
61
|
+
@perform = true
|
62
|
+
end
|
63
|
+
|
64
|
+
def exec(pkg, command_name)
|
65
|
+
@node.log pkg.name, command_name.to_s.yellow
|
66
|
+
context = @perform ? Orca::ExecutionContext.new(@node) : Orca::MockExecutionContext.new(@node)
|
67
|
+
cmds = pkg.command(command_name)
|
68
|
+
cmds.map {|cmd| context.apply(cmd) }
|
69
|
+
end
|
70
|
+
|
71
|
+
class ValidationFailureError < StandardError
|
72
|
+
def initialize(node, package)
|
73
|
+
@node = node
|
74
|
+
@package = package
|
75
|
+
end
|
76
|
+
|
77
|
+
def message
|
78
|
+
"Package #{@package.name} failed validation on #{@node.to_s}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/orca/suite.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
class Orca::Suite
|
2
|
+
|
3
|
+
def load_file(file)
|
4
|
+
Orca::DSL.module_eval(File.read(file))
|
5
|
+
end
|
6
|
+
|
7
|
+
def execute(node_name, pkg_name, command)
|
8
|
+
node = Orca::Node.find(node_name)
|
9
|
+
pkg = Orca::PackageIndex.default.get(pkg_name)
|
10
|
+
Orca::Runner.new(node, pkg).execute(command)
|
11
|
+
end
|
12
|
+
|
13
|
+
def demonstrate(node_name, pkg_name, command)
|
14
|
+
node = Orca::Node.find(node_name)
|
15
|
+
pkg = Orca::PackageIndex.default.get(pkg_name)
|
16
|
+
Orca::Runner.new(node, pkg).demonstrate(command)
|
17
|
+
end
|
18
|
+
end
|
data/orca.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Gem::Specification.new do |gem|
|
2
|
+
gem.authors = ["Andy Kent"]
|
3
|
+
gem.email = ["andy.kent@me.com"]
|
4
|
+
gem.description = %q{Orca is a super simple way to build and configure servers}
|
5
|
+
gem.summary = %q{Simplified Machine Building}
|
6
|
+
gem.homepage = ""
|
7
|
+
|
8
|
+
gem.files = `git ls-files`.split($\)
|
9
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
10
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
11
|
+
gem.name = "orca"
|
12
|
+
gem.require_paths = ["lib"]
|
13
|
+
gem.version = '0.1.0'
|
14
|
+
gem.add_dependency('colored')
|
15
|
+
gem.add_dependency('net-ssh')
|
16
|
+
gem.add_dependency('net-sftp')
|
17
|
+
gem.add_dependency('thor')
|
18
|
+
end
|
data/test/dsl_test.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Orca::DSL do
|
4
|
+
describe "'package' command" do
|
5
|
+
after :each do
|
6
|
+
reset_package_index!
|
7
|
+
end
|
8
|
+
|
9
|
+
it "adds a package to the index" do
|
10
|
+
Orca::DSL.package('my-package') { nil }
|
11
|
+
Orca::PackageIndex.default.get('my-package').must_be_instance_of Orca::Package
|
12
|
+
end
|
13
|
+
|
14
|
+
it "creates a new package based on the supplied name" do
|
15
|
+
Orca::DSL.package('my-package') { nil }
|
16
|
+
package = Orca::PackageIndex.default.get('my-package')
|
17
|
+
package.name.must_equal 'my-package'
|
18
|
+
end
|
19
|
+
|
20
|
+
it "executes the given definition block in the package context" do
|
21
|
+
Orca::DSL.package('my-package') { depends_on 'other-package' }
|
22
|
+
package = Orca::PackageIndex.default.get('my-package')
|
23
|
+
package.dependancies.must_equal ['other-package']
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "'node' command" do
|
28
|
+
it "creates a node and adds it to the index" do
|
29
|
+
Orca::DSL.node('node-name', 'node-host')
|
30
|
+
node = Orca::Node.find('node-name')
|
31
|
+
node.name.must_equal 'node-name'
|
32
|
+
node.host.must_equal 'node-host'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
example
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Orca::LocalFile do
|
4
|
+
before :each do
|
5
|
+
@local_file_path = File.join(File.dirname(__FILE__), 'fixtures', 'example.txt')
|
6
|
+
@local_file = Orca::LocalFile.new(@local_file_path)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe 'path' do
|
10
|
+
it "returns the absolute path of a local file" do
|
11
|
+
@local_file.path.must_equal @local_file_path
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'hash' do
|
16
|
+
it "returns the sha1 of a local file" do
|
17
|
+
@local_file.hash.must_equal 'c3499c2729730a7f807efb8676a92dcb6f8a3f8f'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'matches' do
|
22
|
+
it "returns true if the passed file has a matching hash" do
|
23
|
+
@local_file.matches?(@local_file).must_equal true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'exists?' do
|
28
|
+
it "checks if a file exists" do
|
29
|
+
@local_file.exists?.must_equal true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe 'copy_to' do
|
34
|
+
before(:each) { @destination = Orca::LocalFile.new("/tmp/example-#{Time.now.to_i}.txt") }
|
35
|
+
after(:each) { @destination.delete! }
|
36
|
+
|
37
|
+
it "copies a file to another local location" do
|
38
|
+
@destination.exists?.must_equal false
|
39
|
+
@local_file.copy_to(@destination).must_equal @destination
|
40
|
+
@destination.exists?.must_equal true
|
41
|
+
end
|
42
|
+
|
43
|
+
it "copies a file to a remote location by uploading it" do
|
44
|
+
@remote_destination_context = mock()
|
45
|
+
@remote_destination = Orca::RemoteFile.new(@remote_destination_context, "/tmp/example-dest.txt")
|
46
|
+
@remote_destination_context.expects(:upload).with(@local_file.path, @remote_destination.path)
|
47
|
+
@local_file.copy_to(@remote_destination).must_equal @remote_destination
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe 'set_permissions' do
|
52
|
+
it "sets permissions on the file based on a mask" do
|
53
|
+
@local_file.set_permissions(0664).must_equal @local_file
|
54
|
+
@local_file.permissions.must_equal 0664
|
55
|
+
@local_file.set_permissions(0644).must_equal @local_file
|
56
|
+
@local_file.permissions.must_equal 0644
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Orca::PackageIndex do
|
4
|
+
before :each do
|
5
|
+
@package = Orca::Package.new('my-package')
|
6
|
+
@default = Orca::PackageIndex.default
|
7
|
+
end
|
8
|
+
|
9
|
+
after :each do
|
10
|
+
reset_package_index!
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "default" do
|
14
|
+
it "returns a package index singleton named default" do
|
15
|
+
Orca::PackageIndex.default.index_name.must_equal 'default'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "allways returns the same package index" do
|
19
|
+
Orca::PackageIndex.default.must_equal Orca::PackageIndex.default
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "add" do
|
24
|
+
it "adds a package to the index" do
|
25
|
+
@default.add(@package)
|
26
|
+
@default.get(@package.name).must_equal @package
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'get' do
|
31
|
+
it "fetches a package by name" do
|
32
|
+
@default.add(@package)
|
33
|
+
@default.get(@package.name).must_equal @package
|
34
|
+
end
|
35
|
+
|
36
|
+
it "throws an execption if the package doesn't exist" do
|
37
|
+
assert_raises(Orca::PackageIndex::MissingPackageError) { @default.get(@package.name) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "clear!" do
|
42
|
+
it "wipes the index clean of packages" do
|
43
|
+
@default.add(@package)
|
44
|
+
@default.clear!
|
45
|
+
assert_raises(Orca::PackageIndex::MissingPackageError) { @default.get(@package.name) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Orca::Package do
|
4
|
+
before :each do
|
5
|
+
@package = Orca::Package.new('my-package')
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "depends_on" do
|
9
|
+
it "adds a dependancy" do
|
10
|
+
@package.dependancies.must_equal []
|
11
|
+
@package.depends_on('other-package')
|
12
|
+
@package.dependancies.must_equal ['other-package']
|
13
|
+
end
|
14
|
+
|
15
|
+
it "adds multiple dependancies at once" do
|
16
|
+
@package.dependancies.must_equal []
|
17
|
+
@package.depends_on('other-package', 'third-package')
|
18
|
+
@package.dependancies.must_equal ['other-package', 'third-package']
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "action" do
|
23
|
+
it "allows defining actions with a name and a block" do
|
24
|
+
@package.action('my-action') { 'foo' }
|
25
|
+
@package.actions['my-action'].call.must_equal 'foo'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "commands" do
|
30
|
+
it "can add an 'apply' command" do
|
31
|
+
@package.apply { 'my-apply' }
|
32
|
+
@package.command(:apply).first.call.must_equal 'my-apply'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "can add an 'remove' command" do
|
36
|
+
@package.remove { 'my-remove' }
|
37
|
+
@package.command(:remove).first.call.must_equal 'my-remove'
|
38
|
+
end
|
39
|
+
|
40
|
+
it "can add an 'validate' command" do
|
41
|
+
@package.validate { 'my-validate' }
|
42
|
+
@package.command(:validate).first.call.must_equal 'my-validate'
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'knows if a command is provided' do
|
46
|
+
@package.provides_command?(:apply).must_equal false
|
47
|
+
@package.apply { 'my-apply' }
|
48
|
+
@package.provides_command?(:apply).must_equal true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Orca::RemoteFile do
|
4
|
+
before :each do
|
5
|
+
@local_file_path = File.join(File.dirname(__FILE__), 'fixtures', 'example.txt')
|
6
|
+
@local_file = Orca::LocalFile.new(@local_file_path)
|
7
|
+
@remote_file_path = '/tmp/example.txt'
|
8
|
+
@context = mock()
|
9
|
+
@remote_file = Orca::RemoteFile.new(@context, @remote_file_path)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'path' do
|
13
|
+
it "returns the absolute path of a local file" do
|
14
|
+
@remote_file.path.must_equal @remote_file_path
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'hash' do
|
19
|
+
it "returns the sha1 of a remote file" do
|
20
|
+
@context.expects(:run)
|
21
|
+
.with(%[if [ -f #{@remote_file_path} ]; then echo "true"; else echo "false"; fi])
|
22
|
+
.returns("true\n")
|
23
|
+
@context.expects(:run)
|
24
|
+
.with("sha1sum #{@remote_file_path}")
|
25
|
+
.returns("c3499c2729730a7f807efb8676a92dcb6f8a3f8f #{@remote_file_path}")
|
26
|
+
@remote_file.hash.must_equal 'c3499c2729730a7f807efb8676a92dcb6f8a3f8f'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'matches' do
|
31
|
+
it "returns true if the passed file has a matching hash" do
|
32
|
+
@context.expects(:run)
|
33
|
+
.with(%[if [ -f #{@remote_file_path} ]; then echo "true"; else echo "false"; fi])
|
34
|
+
.returns("true\n")
|
35
|
+
@context.expects(:run)
|
36
|
+
.with("sha1sum #{@remote_file_path}")
|
37
|
+
.returns("c3499c2729730a7f807efb8676a92dcb6f8a3f8f #{@remote_file_path}")
|
38
|
+
@remote_file.matches?(@remote_file).must_equal true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'exists?' do
|
43
|
+
it "checks if a file exists" do
|
44
|
+
@context.expects(:run)
|
45
|
+
.with(%[if [ -f #{@remote_file_path} ]; then echo "true"; else echo "false"; fi])
|
46
|
+
.returns("true\n")
|
47
|
+
@remote_file.exists?.must_equal true
|
48
|
+
end
|
49
|
+
|
50
|
+
it "checks if a missing file exists" do
|
51
|
+
@context.expects(:run)
|
52
|
+
.with(%[if [ -f #{@remote_file_path} ]; then echo "true"; else echo "false"; fi])
|
53
|
+
.returns("false\n")
|
54
|
+
@remote_file.exists?.must_equal false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe 'copy_to' do
|
59
|
+
it "copies a file to another remote location" do
|
60
|
+
@remote_destination_context = mock()
|
61
|
+
@remote_destination = Orca::RemoteFile.new(@remote_destination_context, "/tmp/example-dest.txt")
|
62
|
+
@context.expects(:sudo)
|
63
|
+
.with(%[cp #{@remote_file.path} #{@remote_destination.path}])
|
64
|
+
.returns("true\n")
|
65
|
+
@remote_file.copy_to(@remote_destination).must_equal @remote_destination
|
66
|
+
end
|
67
|
+
|
68
|
+
it "copies a file to local location by downlaoding it" do
|
69
|
+
@local_destination = Orca::LocalFile.new("/tmp/example-#{Time.now.to_i}.txt")
|
70
|
+
@context.expects(:download)
|
71
|
+
.with(@remote_file_path, @local_destination.path)
|
72
|
+
@remote_file.copy_to(@local_destination).must_equal @local_destination
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'delete!' do
|
77
|
+
it "removes the file from the remote server" do
|
78
|
+
@context.expects(:remove).with(@remote_file.path)
|
79
|
+
@remote_file.delete!
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe 'set_permissions' do
|
84
|
+
it "sets permissions on the file based on a mask" do
|
85
|
+
@context.expects(:sudo).with('chmod -R 644 /tmp/example.txt')
|
86
|
+
@context.expects(:run).with("stat --format=%a /tmp/example.txt").returns("644\n")
|
87
|
+
@remote_file.set_permissions(0644).must_equal @remote_file
|
88
|
+
@remote_file.permissions.must_equal 0644
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|