loom-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|