loom-core 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 +1 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +99 -0
- data/Guardfile +54 -0
- data/Rakefile +6 -0
- data/bin/loom +185 -0
- data/lib/env/development.rb +1 -0
- data/lib/loom.rb +44 -0
- data/lib/loom/all.rb +20 -0
- data/lib/loom/config.rb +106 -0
- data/lib/loom/core_ext.rb +37 -0
- data/lib/loom/dsl.rb +60 -0
- data/lib/loom/facts.rb +13 -0
- data/lib/loom/facts/all.rb +2 -0
- data/lib/loom/facts/fact_file_provider.rb +86 -0
- data/lib/loom/facts/fact_set.rb +138 -0
- data/lib/loom/host_spec.rb +32 -0
- data/lib/loom/inventory.rb +124 -0
- data/lib/loom/logger.rb +141 -0
- data/lib/loom/method_signature.rb +174 -0
- data/lib/loom/mods.rb +4 -0
- data/lib/loom/mods/action_proxy.rb +105 -0
- data/lib/loom/mods/all.rb +3 -0
- data/lib/loom/mods/mod_loader.rb +80 -0
- data/lib/loom/mods/module.rb +113 -0
- data/lib/loom/pattern.rb +15 -0
- data/lib/loom/pattern/all.rb +7 -0
- data/lib/loom/pattern/definition_context.rb +74 -0
- data/lib/loom/pattern/dsl.rb +176 -0
- data/lib/loom/pattern/hook.rb +28 -0
- data/lib/loom/pattern/loader.rb +48 -0
- data/lib/loom/pattern/reference.rb +71 -0
- data/lib/loom/pattern/reference_set.rb +169 -0
- data/lib/loom/pattern/result_reporter.rb +77 -0
- data/lib/loom/runner.rb +209 -0
- data/lib/loom/shell.rb +12 -0
- data/lib/loom/shell/all.rb +10 -0
- data/lib/loom/shell/api.rb +48 -0
- data/lib/loom/shell/cmd_result.rb +33 -0
- data/lib/loom/shell/cmd_wrapper.rb +164 -0
- data/lib/loom/shell/core.rb +226 -0
- data/lib/loom/shell/harness_blob.rb +26 -0
- data/lib/loom/shell/harness_command_builder.rb +50 -0
- data/lib/loom/shell/session.rb +25 -0
- data/lib/loom/trap.rb +44 -0
- data/lib/loom/version.rb +3 -0
- data/lib/loomext/all.rb +4 -0
- data/lib/loomext/corefacts.rb +6 -0
- data/lib/loomext/corefacts/all.rb +8 -0
- data/lib/loomext/corefacts/facter_provider.rb +24 -0
- data/lib/loomext/coremods.rb +5 -0
- data/lib/loomext/coremods/all.rb +13 -0
- data/lib/loomext/coremods/exec.rb +50 -0
- data/lib/loomext/coremods/files.rb +104 -0
- data/lib/loomext/coremods/net.rb +33 -0
- data/lib/loomext/coremods/package/adapter.rb +100 -0
- data/lib/loomext/coremods/package/package.rb +62 -0
- data/lib/loomext/coremods/user.rb +82 -0
- data/lib/loomext/coremods/vm.rb +0 -0
- data/lib/loomext/coremods/vm/all.rb +6 -0
- data/lib/loomext/coremods/vm/vbox.rb +84 -0
- data/loom.gemspec +39 -0
- data/loom/inventory.yml +13 -0
- data/scripts/harness.sh +242 -0
- data/spec/loom/host_spec_spec.rb +101 -0
- data/spec/loom/inventory_spec.rb +154 -0
- data/spec/loom/method_signature_spec.rb +275 -0
- data/spec/loom/pattern/dsl_spec.rb +207 -0
- data/spec/loom/shell/cmd_wrapper_spec.rb +239 -0
- data/spec/loom/shell/harness_blob_spec.rb +42 -0
- data/spec/loom/shell/harness_command_builder_spec.rb +36 -0
- data/spec/runloom.sh +35 -0
- data/spec/scripts/harness_spec.rb +385 -0
- data/spec/spec_helper.rb +94 -0
- data/spec/test.loom +370 -0
- data/spec/test_loom_spec.rb +57 -0
- metadata +287 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
module Loom
|
2
|
+
module CoreExt
|
3
|
+
refine String do
|
4
|
+
def underscore
|
5
|
+
uncamelify = self.gsub /[a-z\W][A-Z]/ do |m|
|
6
|
+
m.gsub /(^.)/, '\1_'
|
7
|
+
end
|
8
|
+
uncamelify.downcase.gsub(/[^a-z0-9]+/, '_')
|
9
|
+
end
|
10
|
+
|
11
|
+
def demodulize
|
12
|
+
self.split('::').last
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ModuleExt
|
17
|
+
##
|
18
|
+
# A method like :attr_accessor, except the setter and getter
|
19
|
+
# methods are combined into one. It acts as a setter if an
|
20
|
+
# argument is passed, and always returns the currnet value.
|
21
|
+
def loom_accessor(*names)
|
22
|
+
names.each do |name|
|
23
|
+
instance_variable_set "@#{name}", nil
|
24
|
+
|
25
|
+
define_method name do |*args|
|
26
|
+
raise 'expected 0..1 arguments' if args.size > 1
|
27
|
+
instance_variable_set("@#{name}", args.first) if args.size > 0
|
28
|
+
instance_variable_get "@#{name}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
::Module.include ModuleExt
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
data/lib/loom/dsl.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Loom
|
4
|
+
|
5
|
+
# TODO: Rename this to something like SSHKitWrapper, DSL is a
|
6
|
+
# terribly misinformative name.
|
7
|
+
module DSL
|
8
|
+
|
9
|
+
UnexpectedHostError = Class.new Loom::LoomError
|
10
|
+
SSHConnectionError = Class.new Loom::LoomError
|
11
|
+
|
12
|
+
##
|
13
|
+
# Runs the given patter_block on each host in
|
14
|
+
# +host_specs+. +host_specs+ should be an array of String
|
15
|
+
# hostname/connection strings or +SSHKitHost+ instances.
|
16
|
+
#
|
17
|
+
# +&block+ should accept an SSHKit::Backend and SSHKit::Host
|
18
|
+
def on_host(host_specs, &run_block)
|
19
|
+
host_specs.each do |spec|
|
20
|
+
raise UnexpectedHostError, "not a HostSpec => #{spec}" unless spec.is_a? HostSpec
|
21
|
+
end
|
22
|
+
|
23
|
+
host_spec_map = host_specs.reduce({}) do |map, spec|
|
24
|
+
map[spec.hostname] = spec
|
25
|
+
map
|
26
|
+
end
|
27
|
+
|
28
|
+
execution_block = lambda do |sshkit_host|
|
29
|
+
host_spec = host_spec_map[sshkit_host.hostname]
|
30
|
+
Loom.log.debug1(self) { "connecting to host => #{host_spec.hostname}" }
|
31
|
+
sshkit_backend = self
|
32
|
+
|
33
|
+
begin
|
34
|
+
yield sshkit_backend, host_spec
|
35
|
+
rescue SocketError => e
|
36
|
+
Loom.log.error "error connecting to host => #{host_spec.hostname}"
|
37
|
+
raise SSHConnectionError, e
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
local_specs = host_specs.select { |s| s.is_localhost? }
|
42
|
+
unless local_specs.empty?
|
43
|
+
Loom.log.debug1(self) do
|
44
|
+
"local execution for host entry => #{local_specs.first}"
|
45
|
+
end
|
46
|
+
SSHKitDSLShadow.run_locally &execution_block
|
47
|
+
end
|
48
|
+
|
49
|
+
remote_specs = host_specs.select { |s| s.is_remote? }.map(&:sshkit_host)
|
50
|
+
unless remote_specs.empty?
|
51
|
+
Loom.log.debug1(self) { "remoted execution for #{remote_specs.size} hosts" }
|
52
|
+
SSHKitDSLShadow.on remote_specs, &execution_block
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class SSHKitDSLShadow
|
57
|
+
extend SSHKit::DSL
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/loom/facts.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module Loom::Facts
|
4
|
+
|
5
|
+
class FactFileProvider < Provider
|
6
|
+
|
7
|
+
InvalidFactFileConversion = Class.new Loom::LoomError
|
8
|
+
|
9
|
+
YAML_FILE_GLOBS = [
|
10
|
+
"facts.yml",
|
11
|
+
"facts/**/*.yml",
|
12
|
+
"facts/**/*.yaml"
|
13
|
+
]
|
14
|
+
|
15
|
+
TXT_FILE_GLOBS = [
|
16
|
+
"facts.txt",
|
17
|
+
"facts/**/*.txt"
|
18
|
+
]
|
19
|
+
|
20
|
+
ALL_FILE_GLOBS = [
|
21
|
+
"facts/**/*"
|
22
|
+
]
|
23
|
+
|
24
|
+
Provider.register_factory(self) do |host_spec, shell, loom_config|
|
25
|
+
providers = []
|
26
|
+
|
27
|
+
yaml_paths = loom_config.files.find YAML_FILE_GLOBS
|
28
|
+
providers << YAMLFactFileProvider.new(yaml_paths)
|
29
|
+
|
30
|
+
txt_paths = loom_config.files.find TXT_FILE_GLOBS
|
31
|
+
providers << TxtFileProvider.new(txt_paths)
|
32
|
+
providers
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(paths)
|
36
|
+
@fact_map = convert_file_paths paths
|
37
|
+
end
|
38
|
+
|
39
|
+
def collect_facts
|
40
|
+
@fact_map.dup
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
def convert_path_to_map
|
45
|
+
raise 'not implemented'
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def convert_file_paths(paths)
|
50
|
+
paths.reduce({}) do |memo, path|
|
51
|
+
Loom.log.debug { "loading fact file provider for => #{path}" }
|
52
|
+
tmp_map = convert_path_to_map path
|
53
|
+
raise InvalidFactFileConversion, path unless tmp_map.is_a? Hash
|
54
|
+
memo.merge! tmp_map
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def load_config(config)
|
59
|
+
file_paths = config.files.find @file_globs
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class YAMLFactFileProvider < FactFileProvider
|
64
|
+
|
65
|
+
def convert_path_to_map(path)
|
66
|
+
YAML.load_file path
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
class TxtFileProvider < FactFileProvider
|
72
|
+
|
73
|
+
def convert_path_to_map(path)
|
74
|
+
map = {}
|
75
|
+
File.open(path, 'r') do |io|
|
76
|
+
io.each_line do |line|
|
77
|
+
next if line.match /^\s*#/
|
78
|
+
k,v = line.split "="
|
79
|
+
map[k] = v.strip
|
80
|
+
end
|
81
|
+
map
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module Loom::Facts
|
2
|
+
|
3
|
+
class Provider
|
4
|
+
attr_reader :fact_map
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def disable_for_host(host_spec, klass)
|
8
|
+
Loom.log.warn "disabling fact provider => #{klass} on #{host_spec.hostname}"
|
9
|
+
@disabled_providers ||= {}
|
10
|
+
@disabled_providers[host_spec.hostname] ||= []
|
11
|
+
@disabled_providers[host_spec.hostname] << klass
|
12
|
+
end
|
13
|
+
|
14
|
+
def disabled_for_host?(host_spec, klass)
|
15
|
+
@disabled_providers ||= {}
|
16
|
+
@disabled_providers[host_spec.hostname] ||= []
|
17
|
+
@disabled_providers[host_spec.hostname].include? klass
|
18
|
+
end
|
19
|
+
|
20
|
+
def register_factory(klass, &block)
|
21
|
+
@provider_factories ||= []
|
22
|
+
@provider_factories << block
|
23
|
+
Loom.log.debug1(self) { "registered fact provider => #{klass}" }
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_fact_providers(host_spec, shell, loom_config)
|
27
|
+
@provider_factories.map do |block|
|
28
|
+
block.call(host_spec, shell, loom_config)
|
29
|
+
end.flatten
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def disable(host_spec)
|
34
|
+
Provider.disable_for_host host_spec, self.class
|
35
|
+
end
|
36
|
+
|
37
|
+
# Should return a Hash
|
38
|
+
def collect_facts
|
39
|
+
raise 'not implemented'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class FactSet
|
44
|
+
|
45
|
+
InvalidFactName = Class.new Loom::LoomError
|
46
|
+
InvalidFactValue = Class.new Loom::LoomError
|
47
|
+
UnmarshalableError = Class.new Loom::LoomError
|
48
|
+
|
49
|
+
class << self
|
50
|
+
def create_for_host(host_spec, shell, loom_config)
|
51
|
+
fact_map = {}
|
52
|
+
fact_providers = Provider.create_fact_providers(host_spec, shell, loom_config)
|
53
|
+
fact_providers.each do |provider|
|
54
|
+
next if Provider.disabled_for_host?(host_spec, provider.class)
|
55
|
+
|
56
|
+
Loom.log.debug { "loading facts from provider => #{provider}" }
|
57
|
+
|
58
|
+
begin
|
59
|
+
provider.collect_facts.each do |k, v|
|
60
|
+
k = k.to_sym
|
61
|
+
if fact_map[k]
|
62
|
+
Loom.log.warn "overriding fact => #{k}"
|
63
|
+
end
|
64
|
+
Loom.log.debug5(self) { "adding fact => #{k}=#{v.to_s}" }
|
65
|
+
fact_map[k] = v
|
66
|
+
end
|
67
|
+
rescue => e
|
68
|
+
Loom.log.error "error executing fact provider #{provider.class} => #{e.message}"
|
69
|
+
provider.disable(host_spec)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
FactSet.new host_spec, fact_map
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def initialize(host_spec, fact_map)
|
78
|
+
raise unless fact_map.is_a? Hash
|
79
|
+
validate_facts fact_map
|
80
|
+
|
81
|
+
@fact_map = YAML.load(fact_map.to_yaml).reduce({}) do |memo, tuple|
|
82
|
+
memo[tuple.first.to_sym] = tuple.last
|
83
|
+
memo
|
84
|
+
end
|
85
|
+
@host_spec = host_spec
|
86
|
+
end
|
87
|
+
|
88
|
+
attr_reader :host_spec
|
89
|
+
|
90
|
+
def merge(facts)
|
91
|
+
facts = case facts
|
92
|
+
when FactSet
|
93
|
+
facts.facts
|
94
|
+
when Hash
|
95
|
+
facts
|
96
|
+
else
|
97
|
+
raise "unable to merge facts => #{facts.class}:#{facts}"
|
98
|
+
end
|
99
|
+
merged_facts = @fact_map.merge facts
|
100
|
+
FactSet.new @host_spec, merged_facts
|
101
|
+
end
|
102
|
+
|
103
|
+
def hostname
|
104
|
+
host_spec.hostname
|
105
|
+
end
|
106
|
+
|
107
|
+
def get(fact_name)
|
108
|
+
result = @fact_map[fact_name.to_sym]
|
109
|
+
result.dup rescue result
|
110
|
+
end
|
111
|
+
alias_method :[], :get
|
112
|
+
|
113
|
+
def facts
|
114
|
+
@fact_map.dup
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_s
|
118
|
+
@fact_map.to_a.map { |tuple| tuple.join "=" }.join "\n"
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
def validate_facts(fact_map)
|
123
|
+
fact_map.each do |k, v|
|
124
|
+
validate_fact_name k.to_sym
|
125
|
+
validate_fact_value v
|
126
|
+
end
|
127
|
+
raise UnmarshalableError unless fact_map.eql? YAML.load(fact_map.to_yaml)
|
128
|
+
end
|
129
|
+
|
130
|
+
def validate_fact_name(name)
|
131
|
+
raise InvalidFactName, name unless name.eql? YAML.load(name.to_yaml)
|
132
|
+
end
|
133
|
+
|
134
|
+
def validate_fact_value(value)
|
135
|
+
raise InvalidFactValue, value unless value.eql? YAML.load(value.to_yaml)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
require "sshkit"
|
3
|
+
|
4
|
+
module Loom
|
5
|
+
|
6
|
+
UnparseableHostStringError = Class.new Loom::LoomError
|
7
|
+
|
8
|
+
class HostSpec
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
attr_accessor :disabled
|
12
|
+
attr_reader :sshkit_host
|
13
|
+
|
14
|
+
def initialize(host_string)
|
15
|
+
@sshkit_host = parse host_string
|
16
|
+
end
|
17
|
+
def_delegators :@sshkit_host, :hostname, :user, :port
|
18
|
+
|
19
|
+
def is_remote?
|
20
|
+
!is_localhost?
|
21
|
+
end
|
22
|
+
|
23
|
+
def is_localhost?
|
24
|
+
hostname == "localhost" && port.nil? && user.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def parse(host_string)
|
29
|
+
SSHKit::Host.new host_string
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Loom
|
4
|
+
module Inventory
|
5
|
+
|
6
|
+
InvalidHostEntry = Class.new Loom::LoomError
|
7
|
+
InventoryFileEntryError = Class.new Loom::LoomError
|
8
|
+
|
9
|
+
INVENTORY_FILE_NAMES = [
|
10
|
+
"inventory.yml",
|
11
|
+
"inventory.yaml"
|
12
|
+
]
|
13
|
+
|
14
|
+
class InventoryList
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def total_inventory(loom_config)
|
18
|
+
fileset = InventoryFileSet.new inventory_files(loom_config)
|
19
|
+
config_hostlist = loom_config.inventory_hosts
|
20
|
+
hostlist = fileset.hostlist + config_hostlist
|
21
|
+
InventoryList.new hostlist, fileset.hostgroup_map
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# The list of hosts to apply patterns to
|
26
|
+
def active_inventory(loom_config)
|
27
|
+
return total_inventory loom_config if loom_config.inventory_all_hosts
|
28
|
+
|
29
|
+
fileset = InventoryFileSet.new inventory_files(loom_config)
|
30
|
+
groups = loom_config.inventory_groups.map(&:to_sym).reduce({}) do |map, group|
|
31
|
+
Loom.log.debug2(self) { "looking for group => #{group}" }
|
32
|
+
key = group.to_sym
|
33
|
+
map[key] = fileset.hostgroup_map[key] if fileset.hostgroup_map.key? key
|
34
|
+
map
|
35
|
+
end
|
36
|
+
Loom.log.debug1(self) { "groups map => #{groups}" }
|
37
|
+
|
38
|
+
InventoryList.new loom_config.inventory_hosts, groups
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def inventory_files(loom_config)
|
43
|
+
loom_config.files.find INVENTORY_FILE_NAMES
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_reader :hosts
|
48
|
+
|
49
|
+
def initialize(hostlist, hostgroup_map={})
|
50
|
+
@hostgroup_map = hostgroup_map
|
51
|
+
|
52
|
+
all_hosts = hostgroup_map.values.flatten + hostlist
|
53
|
+
@hosts = parse_hosts(all_hosts).uniq { |h| h.hostname }
|
54
|
+
@disabled_hosts = {}
|
55
|
+
end
|
56
|
+
|
57
|
+
def disable(hostname)
|
58
|
+
@disabled_hosts[hostname] = true
|
59
|
+
end
|
60
|
+
|
61
|
+
def disabled?(hostname)
|
62
|
+
!!@disabled_hosts[hostname]
|
63
|
+
end
|
64
|
+
|
65
|
+
def hostnames
|
66
|
+
@hosts.map { |h| h.hostname }
|
67
|
+
end
|
68
|
+
|
69
|
+
def group_names
|
70
|
+
@hostgroup_map.keys
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
def parse_hosts(list)
|
75
|
+
list.map do |hoststring|
|
76
|
+
raise InvalidHostEntry, hoststring.class.name unless hoststring.is_a? String
|
77
|
+
HostSpec.new hoststring
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
class InventoryFileSet
|
84
|
+
def initialize(inventory_files)
|
85
|
+
@hostgroup_map = nil
|
86
|
+
@hostlist = nil
|
87
|
+
|
88
|
+
@raw_inventories = inventory_files.map do |path|
|
89
|
+
Loom.log.debug "loading inventory file #{path}"
|
90
|
+
YAML.load_file path
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def hostgroup_map
|
95
|
+
@hostgroup_map ||= @raw_inventories.reduce({}) do |map, i|
|
96
|
+
i.each do |entry|
|
97
|
+
if entry.is_a? Hash
|
98
|
+
Loom.log.debug "merging groups in #{entry}"
|
99
|
+
entry.each do |k,v|
|
100
|
+
map[k.to_sym] = v
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
map
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def hostlist
|
109
|
+
@hostlist ||= @raw_inventories.map do |i|
|
110
|
+
i.map do |entry|
|
111
|
+
case entry
|
112
|
+
when String
|
113
|
+
entry
|
114
|
+
when Hash
|
115
|
+
entry.values
|
116
|
+
else
|
117
|
+
raise InventoryFileEntryError, "unexpected entry #{entry}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end.flatten
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|