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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +99 -0
  6. data/Guardfile +54 -0
  7. data/Rakefile +6 -0
  8. data/bin/loom +185 -0
  9. data/lib/env/development.rb +1 -0
  10. data/lib/loom.rb +44 -0
  11. data/lib/loom/all.rb +20 -0
  12. data/lib/loom/config.rb +106 -0
  13. data/lib/loom/core_ext.rb +37 -0
  14. data/lib/loom/dsl.rb +60 -0
  15. data/lib/loom/facts.rb +13 -0
  16. data/lib/loom/facts/all.rb +2 -0
  17. data/lib/loom/facts/fact_file_provider.rb +86 -0
  18. data/lib/loom/facts/fact_set.rb +138 -0
  19. data/lib/loom/host_spec.rb +32 -0
  20. data/lib/loom/inventory.rb +124 -0
  21. data/lib/loom/logger.rb +141 -0
  22. data/lib/loom/method_signature.rb +174 -0
  23. data/lib/loom/mods.rb +4 -0
  24. data/lib/loom/mods/action_proxy.rb +105 -0
  25. data/lib/loom/mods/all.rb +3 -0
  26. data/lib/loom/mods/mod_loader.rb +80 -0
  27. data/lib/loom/mods/module.rb +113 -0
  28. data/lib/loom/pattern.rb +15 -0
  29. data/lib/loom/pattern/all.rb +7 -0
  30. data/lib/loom/pattern/definition_context.rb +74 -0
  31. data/lib/loom/pattern/dsl.rb +176 -0
  32. data/lib/loom/pattern/hook.rb +28 -0
  33. data/lib/loom/pattern/loader.rb +48 -0
  34. data/lib/loom/pattern/reference.rb +71 -0
  35. data/lib/loom/pattern/reference_set.rb +169 -0
  36. data/lib/loom/pattern/result_reporter.rb +77 -0
  37. data/lib/loom/runner.rb +209 -0
  38. data/lib/loom/shell.rb +12 -0
  39. data/lib/loom/shell/all.rb +10 -0
  40. data/lib/loom/shell/api.rb +48 -0
  41. data/lib/loom/shell/cmd_result.rb +33 -0
  42. data/lib/loom/shell/cmd_wrapper.rb +164 -0
  43. data/lib/loom/shell/core.rb +226 -0
  44. data/lib/loom/shell/harness_blob.rb +26 -0
  45. data/lib/loom/shell/harness_command_builder.rb +50 -0
  46. data/lib/loom/shell/session.rb +25 -0
  47. data/lib/loom/trap.rb +44 -0
  48. data/lib/loom/version.rb +3 -0
  49. data/lib/loomext/all.rb +4 -0
  50. data/lib/loomext/corefacts.rb +6 -0
  51. data/lib/loomext/corefacts/all.rb +8 -0
  52. data/lib/loomext/corefacts/facter_provider.rb +24 -0
  53. data/lib/loomext/coremods.rb +5 -0
  54. data/lib/loomext/coremods/all.rb +13 -0
  55. data/lib/loomext/coremods/exec.rb +50 -0
  56. data/lib/loomext/coremods/files.rb +104 -0
  57. data/lib/loomext/coremods/net.rb +33 -0
  58. data/lib/loomext/coremods/package/adapter.rb +100 -0
  59. data/lib/loomext/coremods/package/package.rb +62 -0
  60. data/lib/loomext/coremods/user.rb +82 -0
  61. data/lib/loomext/coremods/vm.rb +0 -0
  62. data/lib/loomext/coremods/vm/all.rb +6 -0
  63. data/lib/loomext/coremods/vm/vbox.rb +84 -0
  64. data/loom.gemspec +39 -0
  65. data/loom/inventory.yml +13 -0
  66. data/scripts/harness.sh +242 -0
  67. data/spec/loom/host_spec_spec.rb +101 -0
  68. data/spec/loom/inventory_spec.rb +154 -0
  69. data/spec/loom/method_signature_spec.rb +275 -0
  70. data/spec/loom/pattern/dsl_spec.rb +207 -0
  71. data/spec/loom/shell/cmd_wrapper_spec.rb +239 -0
  72. data/spec/loom/shell/harness_blob_spec.rb +42 -0
  73. data/spec/loom/shell/harness_command_builder_spec.rb +36 -0
  74. data/spec/runloom.sh +35 -0
  75. data/spec/scripts/harness_spec.rb +385 -0
  76. data/spec/spec_helper.rb +94 -0
  77. data/spec/test.loom +370 -0
  78. data/spec/test_loom_spec.rb +57 -0
  79. 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,13 @@
1
+ require_relative "facts/all"
2
+
3
+ module Loom
4
+ module Facts
5
+ class << self
6
+
7
+ def fact_set(host_spec, shell, loom_config)
8
+ FactSet.create_for_host host_spec, shell, loom_config
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "fact_set"
2
+ require_relative "fact_file_provider"
@@ -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