loom-core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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