motherbrain 0.0.0.placeholder → 0.13.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (259) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +9 -0
  5. data/CHANGELOG.md +196 -0
  6. data/COMMANDS.md +9 -0
  7. data/CONTRIBUTING.md +24 -0
  8. data/Dockerfile +26 -0
  9. data/Gemfile +60 -2
  10. data/Guardfile +30 -0
  11. data/LICENSE +10 -0
  12. data/MANIFESTS.md +90 -0
  13. data/OPERATORS_GUIDE.md +195 -0
  14. data/PLUGINS.md +268 -0
  15. data/README.md +304 -16
  16. data/Thorfile +123 -0
  17. data/VAGRANT.md +116 -0
  18. data/bin/boot +9 -0
  19. data/bin/mb +5 -0
  20. data/bin/mbsrv +5 -0
  21. data/config.json +32 -0
  22. data/features/cli/bootstrap_command/configurable_scripts.feature +32 -0
  23. data/features/cli/configure_command.feature +57 -0
  24. data/features/cli/environment_command/create_command.feature +22 -0
  25. data/features/cli/environment_command/destroy_command.feature +33 -0
  26. data/features/cli/environment_command/from_command.feature +29 -0
  27. data/features/cli/environment_command/list_command.feature +0 -0
  28. data/features/cli/node_limiting.feature +47 -0
  29. data/features/cli/plugin_command/list_command.feature +46 -0
  30. data/features/cli/service_command/service_command.feature +21 -0
  31. data/features/cli/template_command.feature +10 -0
  32. data/features/cli/validate_config.feature +46 -0
  33. data/features/step_definitions/bootstrap_steps.rb +57 -0
  34. data/features/step_definitions/chef_server_steps.rb +3 -0
  35. data/features/step_definitions/configuration_steps.rb +18 -0
  36. data/features/step_definitions/core_cli_steps.rb +33 -0
  37. data/features/step_definitions/environment_steps.rb +43 -0
  38. data/features/step_definitions/node_steps.rb +3 -0
  39. data/features/step_definitions/plugin_steps.rb +15 -0
  40. data/features/step_definitions/template_steps.rb +7 -0
  41. data/features/support/env.rb +68 -0
  42. data/lib/mb/api.rb +8 -0
  43. data/lib/mb/api/application.rb +7 -0
  44. data/lib/mb/api/endpoint.rb +5 -0
  45. data/lib/mb/api/helpers.rb +38 -0
  46. data/lib/mb/api/v1.rb +56 -0
  47. data/lib/mb/api/v1/config_endpoint.rb +12 -0
  48. data/lib/mb/api/v1/environments_endpoint.rb +174 -0
  49. data/lib/mb/api/v1/jobs_endpoint.rb +31 -0
  50. data/lib/mb/api/v1/plugins_endpoint.rb +90 -0
  51. data/lib/mb/api/validators.rb +5 -0
  52. data/lib/mb/api/validators/sem_ver.rb +18 -0
  53. data/lib/mb/application.rb +148 -0
  54. data/lib/mb/berkshelf.rb +50 -0
  55. data/lib/mb/bootstrap.rb +9 -0
  56. data/lib/mb/bootstrap/manager.rb +250 -0
  57. data/lib/mb/bootstrap/manifest.rb +131 -0
  58. data/lib/mb/bootstrap/routine.rb +199 -0
  59. data/lib/mb/bootstrap/template.rb +73 -0
  60. data/lib/mb/bootstrap/worker.rb +227 -0
  61. data/lib/mb/chef.rb +6 -0
  62. data/lib/mb/chef/config.rb +69 -0
  63. data/lib/mb/chef/run_list_item.rb +115 -0
  64. data/lib/mb/chef_mutex.rb +304 -0
  65. data/lib/mb/clean_room_base.rb +39 -0
  66. data/lib/mb/cli.rb +50 -0
  67. data/lib/mb/cli/base.rb +51 -0
  68. data/lib/mb/cli/shell.rb +29 -0
  69. data/lib/mb/cli/shell/basic.rb +11 -0
  70. data/lib/mb/cli/shell/color.rb +11 -0
  71. data/lib/mb/cli/shell/ext.rb +41 -0
  72. data/lib/mb/cli/sub_command.rb +95 -0
  73. data/lib/mb/cli/sub_command/component.rb +56 -0
  74. data/lib/mb/cli/sub_command/plugin.rb +232 -0
  75. data/lib/mb/cli_client.rb +178 -0
  76. data/lib/mb/cli_gateway.rb +426 -0
  77. data/lib/mb/cli_gateway/sub_commands.rb +3 -0
  78. data/lib/mb/cli_gateway/sub_commands/environment.rb +124 -0
  79. data/lib/mb/cli_gateway/sub_commands/plugin.rb +148 -0
  80. data/lib/mb/command.rb +88 -0
  81. data/lib/mb/command_invoker.rb +235 -0
  82. data/lib/mb/command_invoker/worker.rb +40 -0
  83. data/lib/mb/command_runner.rb +233 -0
  84. data/lib/mb/component.rb +245 -0
  85. data/lib/mb/config.rb +275 -0
  86. data/lib/mb/config_manager.rb +75 -0
  87. data/lib/mb/console.rb +35 -0
  88. data/lib/mb/cookbook_metadata.rb +73 -0
  89. data/lib/mb/core_ext.rb +3 -0
  90. data/lib/mb/core_ext/dir.rb +37 -0
  91. data/lib/mb/core_ext/enumerable.rb +48 -0
  92. data/lib/mb/core_ext/file.rb +24 -0
  93. data/lib/mb/core_ext/signal.rb +11 -0
  94. data/lib/mb/environment_manager.rb +195 -0
  95. data/lib/mb/error_handler.rb +212 -0
  96. data/lib/mb/errors.rb +693 -0
  97. data/lib/mb/file_system.rb +60 -0
  98. data/lib/mb/file_system/tempfile.rb +25 -0
  99. data/lib/mb/gear.rb +154 -0
  100. data/lib/mb/gears.rb +8 -0
  101. data/lib/mb/gears/dynamic_service.rb +218 -0
  102. data/lib/mb/gears/jmx.rb +24 -0
  103. data/lib/mb/gears/jmx/action.rb +46 -0
  104. data/lib/mb/gears/mysql.rb +20 -0
  105. data/lib/mb/gears/mysql/action.rb +190 -0
  106. data/lib/mb/gears/service.rb +163 -0
  107. data/lib/mb/gears/service/action.rb +58 -0
  108. data/lib/mb/gears/service/action_runner.rb +161 -0
  109. data/lib/mb/grape_ext.rb +3 -0
  110. data/lib/mb/grape_ext/endpoint.rb +13 -0
  111. data/lib/mb/group.rb +143 -0
  112. data/lib/mb/job.rb +183 -0
  113. data/lib/mb/job/state_machine.rb +34 -0
  114. data/lib/mb/job/states.rb +46 -0
  115. data/lib/mb/job_manager.rb +96 -0
  116. data/lib/mb/job_record.rb +67 -0
  117. data/lib/mb/job_ticket.rb +25 -0
  118. data/lib/mb/lock_manager.rb +116 -0
  119. data/lib/mb/logging.rb +134 -0
  120. data/lib/mb/logging/basic_format.rb +31 -0
  121. data/lib/mb/manifest.rb +128 -0
  122. data/lib/mb/mixin.rb +3 -0
  123. data/lib/mb/mixin/attribute_setting.rb +265 -0
  124. data/lib/mb/mixin/coded_exit.rb +49 -0
  125. data/lib/mb/mixin/locks.rb +54 -0
  126. data/lib/mb/mixin/services.rb +100 -0
  127. data/lib/mb/node_filter.rb +97 -0
  128. data/lib/mb/node_querier.rb +527 -0
  129. data/lib/mb/plugin.rb +300 -0
  130. data/lib/mb/plugin_manager.rb +589 -0
  131. data/lib/mb/provisioner.rb +186 -0
  132. data/lib/mb/provisioner/manager.rb +213 -0
  133. data/lib/mb/provisioner/manifest.rb +125 -0
  134. data/lib/mb/provisioner/provision_data.rb +96 -0
  135. data/lib/mb/provisioners.rb +5 -0
  136. data/lib/mb/provisioners/aws.rb +395 -0
  137. data/lib/mb/rest_gateway.rb +72 -0
  138. data/lib/mb/ridley_ext.rb +5 -0
  139. data/lib/mb/ridley_ext/cookbook_object.rb +15 -0
  140. data/lib/mb/srv_ctl.rb +183 -0
  141. data/lib/mb/test.rb +104 -0
  142. data/lib/mb/thor_ext.rb +49 -0
  143. data/lib/mb/upgrade.rb +6 -0
  144. data/lib/mb/upgrade/manager.rb +85 -0
  145. data/lib/mb/upgrade/worker.rb +149 -0
  146. data/lib/mb/version.rb +1 -1
  147. data/lib/motherbrain.rb +166 -2
  148. data/man/man_helper.rb +81 -0
  149. data/man/mb.1 +494 -0
  150. data/man/mb.1.html +300 -0
  151. data/man/mb.1.ronn.erb +62 -0
  152. data/motherbrain.gemspec +56 -20
  153. data/scripts/node_name.rb +14 -0
  154. data/spec/fixtures/cb_metadata.json +7 -0
  155. data/spec/fixtures/cb_metadata.rb +14 -0
  156. data/spec/fixtures/fake_id_rsa +27 -0
  157. data/spec/fixtures/fake_key.pem +27 -0
  158. data/spec/fixtures/myface-0.1.0/metadata.rb +14 -0
  159. data/spec/fixtures/myface-0.1.0/motherbrain.rb +0 -0
  160. data/spec/fixtures/test_env.json +15 -0
  161. data/spec/spec_helper.rb +67 -0
  162. data/spec/support/actor_mocking.rb +7 -0
  163. data/spec/support/berkshelf.rb +24 -0
  164. data/spec/support/chef_server.rb +102 -0
  165. data/spec/support/doubles.rb +11 -0
  166. data/spec/support/klass.rb +10 -0
  167. data/spec/support/matchers/each.rb +12 -0
  168. data/spec/support/matchers/error_codes.rb +5 -0
  169. data/spec/support/matchers/exit_codes.rb +57 -0
  170. data/spec/support/matchers/jobs.rb +11 -0
  171. data/spec/support/spec_helpers.rb +145 -0
  172. data/spec/unit/mb/api/application_spec.rb +11 -0
  173. data/spec/unit/mb/api/helpers_spec.rb +5 -0
  174. data/spec/unit/mb/api/v1/config_endpoint_spec.rb +19 -0
  175. data/spec/unit/mb/api/v1/environments_endpoint_spec.rb +71 -0
  176. data/spec/unit/mb/api/v1/jobs_endpoint_spec.rb +24 -0
  177. data/spec/unit/mb/api/v1/plugins_endpoint_spec.rb +298 -0
  178. data/spec/unit/mb/api/v1_spec.rb +37 -0
  179. data/spec/unit/mb/api/validators/sem_ver_spec.rb +5 -0
  180. data/spec/unit/mb/application_spec.rb +19 -0
  181. data/spec/unit/mb/berkshelf_spec.rb +38 -0
  182. data/spec/unit/mb/bootstrap/manager_spec.rb +347 -0
  183. data/spec/unit/mb/bootstrap/manifest_spec.rb +333 -0
  184. data/spec/unit/mb/bootstrap/routine_spec.rb +393 -0
  185. data/spec/unit/mb/bootstrap/template_spec.rb +100 -0
  186. data/spec/unit/mb/bootstrap/worker_spec.rb +194 -0
  187. data/spec/unit/mb/chef/config_spec.rb +33 -0
  188. data/spec/unit/mb/chef/run_list_item_spec.rb +34 -0
  189. data/spec/unit/mb/chef_mutex_spec.rb +314 -0
  190. data/spec/unit/mb/clean_room_base_spec.rb +31 -0
  191. data/spec/unit/mb/cli/base_spec.rb +43 -0
  192. data/spec/unit/mb/cli/shell/basic_spec.rb +5 -0
  193. data/spec/unit/mb/cli/shell/color_spec.rb +5 -0
  194. data/spec/unit/mb/cli/shell/ext_spec.rb +11 -0
  195. data/spec/unit/mb/cli/shell_spec.rb +38 -0
  196. data/spec/unit/mb/cli/sub_command/base_spec.rb +102 -0
  197. data/spec/unit/mb/cli/sub_command/component_spec.rb +5 -0
  198. data/spec/unit/mb/cli/sub_command/plugin_spec.rb +91 -0
  199. data/spec/unit/mb/cli/sub_command_spec.rb +43 -0
  200. data/spec/unit/mb/cli/ui.rb +0 -0
  201. data/spec/unit/mb/cli_client_spec.rb +51 -0
  202. data/spec/unit/mb/cli_gateway_spec.rb +386 -0
  203. data/spec/unit/mb/command_invoker/worker_spec.rb +43 -0
  204. data/spec/unit/mb/command_invoker_spec.rb +230 -0
  205. data/spec/unit/mb/command_runner_spec.rb +299 -0
  206. data/spec/unit/mb/command_spec.rb +76 -0
  207. data/spec/unit/mb/component_spec.rb +185 -0
  208. data/spec/unit/mb/config_manager_spec.rb +31 -0
  209. data/spec/unit/mb/config_spec.rb +408 -0
  210. data/spec/unit/mb/cookbook_metadata_spec.rb +89 -0
  211. data/spec/unit/mb/core_ext/dir_spec.rb +92 -0
  212. data/spec/unit/mb/core_ext/enumerable_spec.rb +104 -0
  213. data/spec/unit/mb/core_ext/file_spec.rb +58 -0
  214. data/spec/unit/mb/core_ext/signal_spec.rb +24 -0
  215. data/spec/unit/mb/environment_manager_spec.rb +166 -0
  216. data/spec/unit/mb/error_handler_spec.rb +173 -0
  217. data/spec/unit/mb/errors_spec.rb +132 -0
  218. data/spec/unit/mb/file_system/tempfile_spec.rb +14 -0
  219. data/spec/unit/mb/file_system_spec.rb +69 -0
  220. data/spec/unit/mb/gear_spec.rb +125 -0
  221. data/spec/unit/mb/gears/dynamic_service_spec.rb +187 -0
  222. data/spec/unit/mb/gears/jmx/action_spec.rb +34 -0
  223. data/spec/unit/mb/gears/jmx_spec.rb +32 -0
  224. data/spec/unit/mb/gears/mysql/action_spec.rb +118 -0
  225. data/spec/unit/mb/gears/mysql_spec.rb +21 -0
  226. data/spec/unit/mb/gears/service/action_runner_spec.rb +182 -0
  227. data/spec/unit/mb/gears/service/action_spec.rb +44 -0
  228. data/spec/unit/mb/gears/service_spec.rb +124 -0
  229. data/spec/unit/mb/group_spec.rb +280 -0
  230. data/spec/unit/mb/job_manager_spec.rb +56 -0
  231. data/spec/unit/mb/job_record_spec.rb +60 -0
  232. data/spec/unit/mb/job_spec.rb +201 -0
  233. data/spec/unit/mb/locks_manager_spec.rb +88 -0
  234. data/spec/unit/mb/logging_spec.rb +133 -0
  235. data/spec/unit/mb/manifest_spec.rb +105 -0
  236. data/spec/unit/mb/mixin/attribute_setting_spec.rb +180 -0
  237. data/spec/unit/mb/mixin/coded_exit_spec.rb +25 -0
  238. data/spec/unit/mb/mixin/locks_spec.rb +32 -0
  239. data/spec/unit/mb/mixin/services_spec.rb +75 -0
  240. data/spec/unit/mb/node_filter_spec.rb +86 -0
  241. data/spec/unit/mb/node_querier_spec.rb +532 -0
  242. data/spec/unit/mb/plugin_manager_spec.rb +724 -0
  243. data/spec/unit/mb/plugin_spec.rb +247 -0
  244. data/spec/unit/mb/provisioner/manager_spec.rb +141 -0
  245. data/spec/unit/mb/provisioner/manifest_spec.rb +182 -0
  246. data/spec/unit/mb/provisioner/provision_data_spec.rb +113 -0
  247. data/spec/unit/mb/provisioner_spec.rb +251 -0
  248. data/spec/unit/mb/provisioners/aws_spec.rb +392 -0
  249. data/spec/unit/mb/provisioners/environment_factory_spec.rb +108 -0
  250. data/spec/unit/mb/rest_gateway_spec.rb +13 -0
  251. data/spec/unit/mb/ridley_ext/cookbook_object_spec.rb +105 -0
  252. data/spec/unit/mb/srv_ctl_spec.rb +142 -0
  253. data/spec/unit/mb/upgrade/manager_spec.rb +37 -0
  254. data/spec/unit/mb/upgrade/worker_spec.rb +219 -0
  255. data/spec/unit/motherbrain_spec.rb +58 -0
  256. data/templates/bootstrap.json +8 -0
  257. data/templates/motherbrain.rb +44 -0
  258. metadata +694 -15
  259. data/Rakefile +0 -1
@@ -0,0 +1,54 @@
1
+ module MotherBrain
2
+ module Mixin
3
+ # A mixin to provide easy access to creating, destroying, and executing within
4
+ # locks in a running motherbrain application.
5
+ module Locks
6
+ extend Forwardable
7
+
8
+ def_delegator "MB::LockManager.instance", :locks, :chef_locks
9
+ def_delegator "MB::LockManager.instance", :find, :find_lock
10
+
11
+ # Attempts to create a lock. Fails if the lock already exists.
12
+ #
13
+ # @see ChefMutex#initialize
14
+ #
15
+ # @raise [MotherBrain::InvalidLockType] if the lock type is invalid
16
+ # @raise [MotherBrain::ResourceLocked] if the lock is unobtainable
17
+ #
18
+ # @return [Boolean]
19
+ def chef_lock(options = {})
20
+ find_or_new(options).lock
21
+ end
22
+
23
+ # Creates a new ChefMutex on the given resource and runs the given block inside of it. The lock is
24
+ # released when the block completes.
25
+ #
26
+ # @see ChefMutex#initialize
27
+ #
28
+ # @raise [MotherBrain::InvalidLockType] if the lock type is invalid
29
+ # @raise [MotherBrain::ResourceLocked] if the lock is unobtainable
30
+ #
31
+ # @return [Boolean]
32
+ def chef_synchronize(options = {}, &block)
33
+ ChefMutex.synchronize(options, &block)
34
+ end
35
+
36
+ # Attempts to unlock the lock. Fails if the lock doesn't exist, or if it is
37
+ # held by someone else
38
+ #
39
+ # @see ChefMutex#initialize
40
+ #
41
+ # @raise [MotherBrain::InvalidLockType] if the lock type is invalid
42
+ # @raise [MotherBrain::ResourceLocked] if the lock is owned by someone else
43
+ def chef_unlock(options = {})
44
+ find_or_new(options).unlock
45
+ end
46
+
47
+ private
48
+
49
+ def find_or_new(*args)
50
+ find_lock(*args) || ChefMutex.new(*args)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,100 @@
1
+ module MotherBrain
2
+ module Mixin
3
+ # A mixin to provide easy access to the various services (actors) running in the
4
+ # motherbrain stack.
5
+ module Services
6
+ class << self
7
+ def included(base)
8
+ base.extend(ClassMethods)
9
+ base.send(:include, ClassMethods)
10
+ end
11
+
12
+ def extended(base)
13
+ base.send(:include, ClassMethods)
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ # @raise [Celluloid::DeadActorError] if the bootstrap manager has not been started
19
+ #
20
+ # @return [Celluloid::Actor(Bootstrap::Manager)]
21
+ def bootstrap_manager
22
+ Bootstrap::Manager.instance
23
+ end
24
+ alias_method :bootstrapper, :bootstrap_manager
25
+
26
+ # @raise [Celluloid::DeadActorError] if command invoker has not been started
27
+ #
28
+ # @return [Celluloid::Actor(CommandInvoker)]
29
+ def command_invoker
30
+ CommandInvoker.instance
31
+ end
32
+
33
+ # @raise [Celluloid::DeadActorError] if the config manager has not been started
34
+ #
35
+ # @return [Celluloid::Actor(ConfigManager)]
36
+ def config_manager
37
+ ConfigManager.instance
38
+ end
39
+
40
+ # @raise [Celluloid::DeadActorError] if the environment manager has not been started
41
+ #
42
+ # @return [Celluloid::Actor(EnvironmentManager)]
43
+ def environment_manager
44
+ EnvironmentManager.instance
45
+ end
46
+
47
+ # @raise [Celluloid::DeadActorError] if the job manager has not been started
48
+ #
49
+ # @return [Celluloid::Actor(JobManager)]
50
+ def job_manager
51
+ JobManager.instance
52
+ end
53
+
54
+ # @raise [Celluloid::DeadActorError] if the lock manager has not been started
55
+ #
56
+ # @return [Celluloid::Actor(LockManager)]
57
+ def lock_manager
58
+ LockManager.instance
59
+ end
60
+
61
+ # @raise [Celluloid::DeadActorError] if the node querier has not been started
62
+ #
63
+ # @return [Celluloid::Actor(NodeQuerier)]
64
+ def node_querier
65
+ NodeQuerier.instance
66
+ end
67
+
68
+ # @raise [Celluloid::DeadActorError] if the plugin manager has not been started
69
+ #
70
+ # @return [Celluloid::Actor(PluginManager)]
71
+ def plugin_manager
72
+ PluginManager.instance
73
+ end
74
+
75
+ # @raise [Celluloid::DeadActorError] if the provisioner manager has not been started
76
+ #
77
+ # @return [Celluloid::Actor(Provisioner::Manager)]
78
+ def provisioner_manager
79
+ Provisioner::Manager.instance
80
+ end
81
+ alias_method :provisioner, :provisioner_manager
82
+
83
+ # @raise [Celluloid::DeadActorError] if Ridley has not been started
84
+ #
85
+ # @return [Celluloid::Actor(Ridley::Connection)]
86
+ def ridley
87
+ MB::Application[:ridley] or raise Celluloid::DeadActorError, "Ridley not running"
88
+ end
89
+ alias_method :chef_connection, :ridley
90
+
91
+ # @raise [Celluloid::DeadActorError] if the upgrade manager has not been started
92
+ #
93
+ # @return [Celluloid::Actor(Upgrade::Manager)]
94
+ def upgrade_manager
95
+ Upgrade::Manager.instance
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,97 @@
1
+ module MotherBrain
2
+ class NodeFilter
3
+ class << self
4
+ # Filters the given nodes based on the given segments
5
+ #
6
+ # @param [Array<String>] segments
7
+ # strings to match nodes on
8
+ # @param [Array<Ridley::NodeObject>] nodes
9
+ #
10
+ # @return [Array] nodes that matched the segments
11
+ def filter(segments, nodes)
12
+ new(segments).filter(nodes)
13
+ end
14
+
15
+ # Expands any IP address ranges in the given segments and
16
+ # returns the segments Array with any IP ranges expanded.
17
+ #
18
+ # @param [Array<String>] segments
19
+ # an Array of hostnames or IPs
20
+ #
21
+ # @return [Array<String>]
22
+ def expand_ipranges(segments)
23
+ node_filter = new(segments)
24
+ segments.collect do |segment|
25
+ range = node_filter.iprange(segment)
26
+ range.nil? ? segment : range
27
+ end.flatten
28
+ end
29
+ end
30
+
31
+ # @return [Array<String>]
32
+ attr_reader :segments
33
+
34
+ # @param [Array<String>, String] segments
35
+ # an Array of hostnames or IPs
36
+ def initialize(segments)
37
+ @segments = Array(segments).flatten
38
+ end
39
+
40
+ # Filters the given array of nodes against the segments
41
+ # and returns the matched nodes.
42
+ #
43
+ # @param [Array<Ridley::NodeObject>] nodes
44
+ #
45
+ # @return [Array] nodes that matched
46
+ def filter(nodes)
47
+ nodes.select { |node| matches?(node) }
48
+ end
49
+
50
+ # Checks the node against the instance's segments
51
+ # and returns nodes that match an ipaddress or hostname.
52
+ #
53
+ # @param [Ridley::NodeObject] node
54
+ #
55
+ # @return [Boolean]
56
+ def matches?(node)
57
+ segments.empty? ||
58
+ segments.any? do |s|
59
+ if ipaddress?(s)
60
+ s == node.public_ipv4
61
+ elsif r = iprange(s)
62
+ r.include?(node.public_ipv4)
63
+ # elsif regex?(s)
64
+ else
65
+ s == node.public_hostname || s == node.public_hostname.sub(/\..*/,'')
66
+ end
67
+ end
68
+ end
69
+
70
+ # Checks the given segment and returns true if it
71
+ # is an ipaddress.
72
+ #
73
+ # @param [String] segment
74
+ #
75
+ # @return [Boolean]
76
+ def ipaddress?(segment)
77
+ segment.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)
78
+ end
79
+
80
+ # Checks the given segment and either returns nil, if
81
+ # it is not a range of IPs or expands and returns the range
82
+ # as an array.
83
+ #
84
+ # @param [String] segment
85
+ #
86
+ # @example
87
+ # iprange("192.168.2.1-2") #=> ["192.168.2.1", "192.168.2.2"]
88
+ #
89
+ # @return [Array]
90
+ def iprange(segment)
91
+ match = segment.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3})\.(\d{1,3}-\d{1,3})$/)
92
+ return nil unless match
93
+ first,last = match[2].split('-')
94
+ (first..last).to_a.collect {|l| "#{match[1]}.#{l}" }
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,527 @@
1
+ require 'net/sftp'
2
+
3
+ module MotherBrain
4
+ class NodeQuerier
5
+
6
+ DISABLED_RUN_LIST_ENTRY = "recipe[disabled]".freeze
7
+
8
+ class << self
9
+ # @raise [Celluloid::DeadActorError] if Node Querier has not been started
10
+ #
11
+ # @return [Celluloid::Actor(NodeQuerier)]
12
+ def instance
13
+ MB::Application[:node_querier] or raise Celluloid::DeadActorError, "node querier not running"
14
+ end
15
+ end
16
+
17
+ extend Forwardable
18
+ include Celluloid
19
+ include MB::Logging
20
+ include MB::Mixin::Locks
21
+ include MB::Mixin::Services
22
+
23
+ finalizer :finalize_callback
24
+
25
+ def initialize
26
+ log.debug { "Node Querier starting..." }
27
+ end
28
+
29
+ # List all of the nodes on the target Chef Server
30
+ #
31
+ # @return [Array<Hash>]
32
+ def list
33
+ chef_connection.node.all
34
+ end
35
+
36
+ # Run Chef on a group of nodes, and update a job status with the result
37
+ # @param [Job] job
38
+ # @param [Array(Ridley::NodeResource)] nodes
39
+ # The collection of nodes to run Chef on
40
+ # @param [Array<String>] override_recipes
41
+ # An array of run list entries that will override the node's current run list
42
+ #
43
+ # @raise [RemoteCommandError]
44
+ def bulk_chef_run(job, nodes, override_recipes = nil)
45
+ job.set_status("Performing a chef client run on #{nodes.collect(&:name).join(', ')}")
46
+
47
+ node_successes_count = 0
48
+ node_successes = Array.new
49
+
50
+ node_failures_count = 0
51
+ node_failures = Array.new
52
+
53
+ futures = nodes.map { |node| node_querier.future(:chef_run, node.public_hostname, node_object: node, override_recipes: override_recipes) }
54
+
55
+ futures.each do |future|
56
+ begin
57
+ response = future.value
58
+ node_successes_count += 1
59
+ node_successes << response.host
60
+ rescue RemoteCommandError => error
61
+ node_failures_count += 1
62
+ node_failures << error.host
63
+ end
64
+ end
65
+
66
+ if node_failures_count > 0
67
+ abort RemoteCommandError.new("chef client run failed on #{node_failures_count} node(s) - #{node_failures.join(', ')}")
68
+ else
69
+ job.set_status("Finished chef client run on #{node_successes_count} node(s) - #{node_successes.join(', ')}")
70
+ end
71
+ end
72
+
73
+ # Return the Chef node_name of the target host. A nil value is returned if a
74
+ # node_name cannot be determined
75
+ #
76
+ # @param [String] host
77
+ # hostname of the target node
78
+ # @option options [String] :user
79
+ # a shell user that will login to each node and perform the bootstrap command on (required)
80
+ # @option options [String] :password
81
+ # the password for the shell user that will perform the bootstrap
82
+ # @option options [Array, String] :keys
83
+ # an array of keys (or a single key) to authenticate the ssh user with instead of a password
84
+ # @option options [Float] :timeout (10.0)
85
+ # timeout value for SSH bootstrap
86
+ # @option options [Boolean] :sudo (true)
87
+ # bootstrap with sudo
88
+ #
89
+ # @return [String, nil]
90
+ def node_name(host, options = {})
91
+ ruby_script('node_name', host, options).split("\n").last
92
+ rescue MB::RemoteScriptError
93
+ # TODO: catch auth error?
94
+ nil
95
+ end
96
+
97
+ # Run Chef-Client on the target host
98
+ #
99
+ # @param [String] host
100
+ #
101
+ # @option options [String] :user
102
+ # a shell user that will login to each node and perform the bootstrap command on (required)
103
+ # @option options [String] :password
104
+ # the password for the shell user that will perform the bootstrap
105
+ # @option options [Array, String] :keys
106
+ # an array of keys (or a single key) to authenticate the ssh user with instead of a password
107
+ # @option options [Float] :timeout (10.0)
108
+ # timeout value for SSH bootstrap
109
+ # @option options [Boolean] :sudo
110
+ # bootstrap with sudo
111
+ # @option options [String] :override_recipe
112
+ # a recipe that will override the nodes current run list
113
+ # @option options [Ridley::NodeObject] :node
114
+ # the actual node object
115
+ #
116
+ # @raise [RemoteCommandError] if an execution error occurs in the remote command
117
+ # @raise [RemoteCommandError] if given a blank or nil hostname
118
+ #
119
+ # @return [Ridley::HostConnector::Response]
120
+ def chef_run(host, options = {})
121
+ options = options.dup
122
+
123
+ unless host.present?
124
+ abort RemoteCommandError.new("cannot execute a chef-run without a hostname or ipaddress")
125
+ end
126
+
127
+ response = if options[:override_recipes]
128
+ node = options[:node_object]
129
+ node.reload
130
+ old_recipes = node.automatic_attributes.recipes
131
+ override_recipes = options[:override_recipes]
132
+
133
+ cmd_recipe_syntax = override_recipes.join(',') { |recipe| "recipe[#{recipe}]" }
134
+ log.info { "Running Chef client with override runlist '#{cmd_recipe_syntax}' on: #{host}" }
135
+ chef_run_response = chef_connection.node.execute_command(host, "chef-client --override-runlist #{cmd_recipe_syntax}")
136
+
137
+ # reset the run list
138
+ node.reload
139
+ log.info { "Resetting node's recipes attribute back to #{old_recipes}" }
140
+ node.automatic_attributes.recipes = old_recipes
141
+ node.save
142
+
143
+ chef_run_response
144
+ else
145
+ log.info { "Running Chef client on: #{host}" }
146
+ chef_connection.node.chef_run(host)
147
+ end
148
+
149
+ if response.error?
150
+ log.info { "Failed Chef client run on: #{host}" }
151
+ abort RemoteCommandError.new(response.stderr.chomp, host)
152
+ end
153
+
154
+ log.info { "Completed Chef client run on: #{host}" }
155
+ response
156
+ rescue Ridley::Errors::HostConnectionError => ex
157
+ log.info { "Failed Chef client run on: #{host}" }
158
+ abort RemoteCommandError.new(ex, host)
159
+ end
160
+
161
+ # Place an encrypted data bag secret on the target host
162
+ #
163
+ # @param [String] host
164
+ #
165
+ # @option options [String] :secret
166
+ # the encrypted data bag secret of the node querier's chef conn will be used
167
+ # as the default key
168
+ # @option options [String] :user
169
+ # a shell user that will login to each node and perform the bootstrap command on (required)
170
+ # @option options [String] :password
171
+ # the password for the shell user that will perform the bootstrap
172
+ # @option options [Array, String] :keys
173
+ # an array of keys (or a single key) to authenticate the ssh user with instead of a password
174
+ # @option options [Float] :timeout (10.0)
175
+ # timeout value for SSH bootstrap
176
+ # @option options [Boolean] :sudo
177
+ # bootstrap with sudo
178
+ #
179
+ # @raise [RemoteFileCopyError]
180
+ #
181
+ # @return [Ridley::HostConnector::Response]
182
+ def put_secret(host, options = {})
183
+ options = options.reverse_merge(secret: Application.config.chef.encrypted_data_bag_secret_path)
184
+
185
+ if options[:secret].nil? || !File.exists?(options[:secret])
186
+ return nil
187
+ end
188
+
189
+ response = chef_connection.node.put_secret(host)
190
+
191
+ if response.error?
192
+ log.info { "Failed to put secret file on: #{host}" }
193
+ return nil
194
+ end
195
+
196
+ log.info { "Successfully put secret file on: #{host}" }
197
+ response
198
+ end
199
+
200
+ # Executes the given command on the host using the best worker
201
+ # available for the host.
202
+ #
203
+ # @param [String] host
204
+ # @param [String] command
205
+ #
206
+ # @return [Ridley::HostConnection::Response]
207
+ def execute_command(host, command)
208
+ response = chef_connection.node.execute_command(host, command)
209
+
210
+ if response.error?
211
+ log.info { "Failed to execute command on: #{host}" }
212
+ abort RemoteCommandError.new(response.stderr.chomp)
213
+ end
214
+
215
+ log.info { "Successfully executed command on: #{host}" }
216
+ response
217
+ end
218
+
219
+ # Check if the target host is registered with the Chef server. If the node does not have Chef and
220
+ # ruby installed by omnibus it will be considered unregistered.
221
+ #
222
+ # @example showing a node who is registered to Chef
223
+ # node_querier.registered?("192.168.1.101") #=> true
224
+ # @example showing a node who does not have ruby or is not registered to Chef
225
+ # node_querier.registered?("192.168.1.102") #=> false
226
+ #
227
+ # @param [String] host
228
+ # public hostname of the target node
229
+ #
230
+ # @return [Boolean]
231
+ def registered?(host)
232
+ !!registered_as(host)
233
+ end
234
+
235
+ # Returns the client name the target node is registered to Chef with.
236
+ #
237
+ # If the node does not have a client registered with the Chef server or if Chef and ruby were not installed
238
+ # by omnibus this function will return nil.
239
+ #
240
+ # @example showing a node who is registered to Chef
241
+ # node_querier.registered_as("192.168.1.101") #=> "reset.riotgames.com"
242
+ # @example showing a node who does not have ruby or is not registered to Chef
243
+ # node_querier.registered_as("192.168.1.102") #=> nil
244
+ #
245
+ # @param [String] host
246
+ # public hostname of the target node
247
+ #
248
+ # @return [String, nil]
249
+ def registered_as(host)
250
+ if (client_id = node_name(host)).nil?
251
+ return nil
252
+ end
253
+ chef_connection.client.find(client_id).try(:name)
254
+ end
255
+
256
+ # Asynchronously remove Chef from a target host and purge it's client and node object from the
257
+ # Chef server.
258
+ #
259
+ # @param [String] host
260
+ # public hostname of the target node
261
+ #
262
+ # @option options [Boolean] :skip_chef (false)
263
+ # skip removal of the Chef package and the contents of the installation
264
+ # directory. Setting this to true will only remove any data and configurations
265
+ # generated by running Chef client.
266
+ #
267
+ # @return [MB::JobTicket]
268
+ def async_purge(host, options = {})
269
+ job = Job.new(:purge_node)
270
+ async(:purge, job, host, options)
271
+ job.ticket
272
+ end
273
+
274
+ # Asynchronously disable a node to stop services @host and prevent
275
+ # chef-client from being run on @host until @host is reenabled
276
+ #
277
+ # @param [String] host
278
+ # public hostname of the target node
279
+ # @param [Hash] options
280
+ #
281
+ # @option options [Boolean] :force (false) Ignore environment lock and execute anyway.
282
+ #
283
+ # @return [MB::JobTicket]
284
+ def async_disable(host, options = {})
285
+ job = Job.new(:disable_node)
286
+ async(:disable, job, host, options)
287
+ job.ticket
288
+ end
289
+
290
+ # Asynchronously enable a node
291
+ #
292
+ # @param [String] host
293
+ # public hostname of the target node
294
+ # @param [Hash] options
295
+ #
296
+ # @option options [Boolean] :force (false) Ignore environment lock and execute anyway.
297
+ #
298
+ # @return [MB::JobTicket]
299
+ def async_enable(host, options = {})
300
+ job = Job.new(:enable_node)
301
+ async(:enable, job, host, options)
302
+ job.ticket
303
+ end
304
+
305
+ # Remove Chef from a target host and purge it's client and node object from the Chef
306
+ # server.
307
+ #
308
+ # @param [MB::Job] job
309
+ # @param [String] host
310
+ # public hostname of the target node
311
+ #
312
+ # @option options [Boolean] :skip_chef (false)
313
+ # skip removal of the Chef package and the contents of the installation
314
+ # directory. Setting this to true will only remove any data and configurations
315
+ # generated by running Chef client.
316
+ def purge(job, host, options = {})
317
+ options = options.reverse_merge(skip_chef: false)
318
+ futures = Array.new
319
+
320
+ job.report_running("Discovering host's registered node name")
321
+ if node_name = registered_as(host)
322
+ job.set_status("Host registered as #{node_name}. Destroying client and node objects.")
323
+ futures << chef_connection.client.future(:delete, node_name)
324
+ futures << chef_connection.node.future(:delete, node_name)
325
+ else
326
+ job.set_status "Could not discover the host's node name. The host may not be registered with Chef or the " +
327
+ "embedded Ruby used to identify the node name may not be available."
328
+ end
329
+
330
+ job.set_status("Cleaning up the host's file system.")
331
+ futures << chef_connection.node.future(:uninstall_chef, host, options.slice(:skip_chef))
332
+ futures.map(&:value)
333
+
334
+ job.report_success
335
+ ensure
336
+ job.terminate if job && job.alive?
337
+ end
338
+
339
+ # Remove explicit service state on @host and remove disabled entry
340
+ # from run list to allow chef-client to run on @host
341
+ #
342
+ # @param [MB::Job] job
343
+ # @param [String] host
344
+ # public hostname of the target node
345
+ # @param [Hash] options
346
+ #
347
+ # @option options [Boolean] :force (false) Ignore environment lock and execute anyway.
348
+ #
349
+ def enable(job, host, options = {})
350
+ job.report_running("Discovering host's registered node name")
351
+ node_name = registered_as(host)
352
+
353
+ if !node_name
354
+ # TODO auth could fail and cause this to throw
355
+ job.report_failure("Could not discover the host's node name. The host may not be " +
356
+ "registered with Chef or the embedded Ruby used to identify the " +
357
+ "node name may not be available. #{host} was not enabled!")
358
+ end
359
+
360
+ job.set_status("Host registered as #{node_name}.")
361
+ node = chef_connection.node.find(node_name)
362
+
363
+ required_run_list = []
364
+ chef_synchronize(chef_environment: node.chef_environment, force: options[:force], job: job) do
365
+ if node.run_list.include?(DISABLED_RUN_LIST_ENTRY)
366
+ required_run_list = on_dynamic_services(job, node) do |dynamic_service, plugin|
367
+ dynamic_service.remove_node_state_change(job,
368
+ plugin,
369
+ node,
370
+ false)
371
+
372
+ end
373
+ if !required_run_list.empty?
374
+ self.bulk_chef_run(job, [node], required_run_list.flatten.uniq)
375
+ end
376
+
377
+ node.run_list = node.run_list.reject { |r| r == DISABLED_RUN_LIST_ENTRY }
378
+
379
+ if node.save
380
+ job.report_success "#{node.name} enabled successfully."
381
+ else
382
+ job.report_failure "#{node.name} did not save! Disabled run_list entry was unable to be removed to the node."
383
+ end
384
+ else
385
+ job.report_success("#{node.name} is not disabled. No need to enable.")
386
+ end
387
+ end
388
+ rescue MotherBrain::ResourceLocked => e
389
+ job.report_failure e.message
390
+ ensure
391
+ job.terminate if job && job.alive?
392
+ end
393
+
394
+ # Stop services on @host and prevent chef-client from being run on
395
+ # @host until @host is reenabled
396
+ #
397
+ # @param [MB::Job] job
398
+ # @param [String] host
399
+ # public hostname of the target node
400
+ # @param [Hash] options
401
+ #
402
+ # @option options [Boolean] :force (false) Ignore environment lock and execute anyway.
403
+ #
404
+ def disable(job, host, options = {})
405
+ job.report_running("Discovering host's registered node name")
406
+ node_name = registered_as(host)
407
+ if !node_name
408
+ # TODO auth could fail and cause this to throw
409
+ job.report_failure("Could not discover the host's node name. The host may not be " +
410
+ "registered with Chef or the embedded Ruby used to identify the " +
411
+ "node name may not be available. #{host} was not disabled!")
412
+ end
413
+ job.set_status("Host registered as #{node_name}.")
414
+ node = chef_connection.node.find(node_name)
415
+ required_run_list = []
416
+ chef_synchronize(chef_environment: node.chef_environment, force: options[:force], job: job) do
417
+ if node.run_list.include?(DISABLED_RUN_LIST_ENTRY)
418
+ job.report_success("#{node.name} is already disabled.")
419
+ else
420
+ required_run_list = on_dynamic_services(job, node) do |dynamic_service, plugin|
421
+ dynamic_service.node_state_change(job,
422
+ plugin,
423
+ node,
424
+ MB::Gear::DynamicService::STOP,
425
+ false)
426
+ end
427
+ end
428
+ if !required_run_list.empty?
429
+ job.set_status "Running chef with the following run list: #{required_run_list.inspect}"
430
+ self.bulk_chef_run(job, [node], required_run_list)
431
+ else
432
+ job.set_status "No recipes required to run."
433
+ end
434
+
435
+ node.run_list = [DISABLED_RUN_LIST_ENTRY].concat(node.run_list)
436
+ if node.save
437
+ job.report_success "#{node.name} disabled."
438
+ else
439
+ job.report_failure "#{node.name} did not save! Disabled run_list entry was unable to be added to the node."
440
+ end
441
+ end
442
+ rescue MotherBrain::ResourceLocked => e
443
+ job.report_failure e.message
444
+ ensure
445
+ job.terminate if job && job.alive?
446
+ end
447
+
448
+ private
449
+
450
+ def finalize_callback
451
+ log.debug { "Node Querier stopping..." }
452
+ end
453
+
454
+ # Run a Ruby script on the target host and return the result of STDOUT. Only scripts
455
+ # that are located in the Mother Brain scripts directory can be used and they should
456
+ # be identified just by their filename minus the extension
457
+ #
458
+ # @example
459
+ # node_querier.ruby_script('node_name', '33.33.33.10') => 'vagrant.localhost'
460
+ #
461
+ # @param [String] name
462
+ # name of the script to run on the target node
463
+ # @param [String] host
464
+ # hostname of the target node
465
+ # the MotherBrain scripts directory
466
+ # @option options [String] :user
467
+ # a shell user that will login to each node and perform the bootstrap command on (required)
468
+ # @option options [String] :password
469
+ # the password for the shell user that will perform the bootstrap
470
+ # @option options [Array, String] :keys
471
+ # an array of keys (or a single key) to authenticate the ssh user with instead of a password
472
+ # @option options [Float] :timeout (10.0)
473
+ # timeout value for SSH bootstrap
474
+ # @option options [Boolean] :sudo (true)
475
+ # bootstrap with sudo
476
+ #
477
+ # @raise [RemoteScriptError] if there was an error in execution
478
+ # @raise [RuntimeError] if an unknown response is returned from Ridley
479
+ #
480
+ # @note
481
+ # Use {#_ruby_script_} if the caller of this function is same actor as the receiver. You will
482
+ # not be able to rescue from the RemoteScriptError thrown by {#ruby_script} but you will
483
+ # be able to rescue from {#_ruby_script_}.
484
+ #
485
+ # @return [String]
486
+ def ruby_script(name, host, options = {})
487
+ name = name.split('.rb')[0]
488
+ lines = File.readlines(MB.scripts.join("#{name}.rb"))
489
+ command_lines = lines.collect { |line| line.gsub('"', "'").strip.chomp }
490
+
491
+ response = chef_connection.node.ruby_script(host, command_lines)
492
+
493
+ if response.error?
494
+ raise RemoteScriptError.new(response.stderr.chomp)
495
+ end
496
+ response.stdout.chomp
497
+ end
498
+
499
+ def on_dynamic_services(job, node)
500
+ [].tap do |required_run_list|
501
+ node.run_list.each do |run_list_entry|
502
+ next if run_list_entry == DISABLED_RUN_LIST_ENTRY
503
+ plugin = plugin_manager.for_run_list_entry(run_list_entry, node.chef_environment, remote: true)
504
+ if plugin.nil?
505
+ job.report_failure("Could not find plugin for #{run_list_entry}. Aborting command.")
506
+ end
507
+ plugin.components.each do |component|
508
+ services = component.gears(MB::Gear::Service)
509
+ services.each do |service|
510
+ if service.dynamic_service?
511
+ if component.group(service.service_group).includes_recipe? run_list_entry
512
+ dynamic_service = service.to_dynamic_service
513
+ yield(dynamic_service, plugin)
514
+ required_run_list << service.service_recipe
515
+ end
516
+ else
517
+ job.report_failure("Service #{component.name}.#{service.name} is not a dynamic service;" +
518
+ " MotherBrain cannot set the state of this service automatically." +
519
+ " Aborting command.")
520
+ end
521
+ end
522
+ end
523
+ end
524
+ end.flatten.uniq
525
+ end
526
+ end
527
+ end