zergrush 0.0.2 → 0.0.4

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.
@@ -0,0 +1,334 @@
1
+ require 'singleton'
2
+ require 'rubygems'
3
+
4
+ # GemPlugin is copyrighted free software by Zed A. Shaw
5
+ # <zedshaw at zedshaw dot com> You can redistribute it and/or modify it under
6
+ # either the terms of the GPL or the conditions below:
7
+
8
+ # 1. You may make and give away verbatim copies of the source form of the
9
+ # software without restriction, provided that you duplicate all of the
10
+ # original copyright notices and associated disclaimers.
11
+
12
+ # 2. You may modify your copy of the software in any way, provided that
13
+ # you do at least ONE of the following:
14
+
15
+ # a) place your modifications in the Public Domain or otherwise make them
16
+ # Freely Available, such as by posting said modifications to Usenet or an
17
+ # equivalent medium, or by allowing the author to include your
18
+ # modifications in the software.
19
+
20
+ # b) use the modified software only within your corporation or
21
+ # organization.
22
+
23
+ # c) rename any non-standard executables so the names do not conflict with
24
+ # standard executables, which must also be provided.
25
+
26
+ # d) make other distribution arrangements with the author.
27
+
28
+ # 3. You may distribute the software in object code or executable
29
+ # form, provided that you do at least ONE of the following:
30
+
31
+ # a) distribute the executables and library files of the software,
32
+ # together with instructions (in the manual page or equivalent) on where
33
+ # to get the original distribution.
34
+
35
+ # b) accompany the distribution with the machine-readable source of the
36
+ # software.
37
+
38
+ # c) give non-standard executables non-standard names, with
39
+ # instructions on where to get the original software distribution.
40
+
41
+ # d) make other distribution arrangements with the author.
42
+
43
+ # 4. You may modify and include the part of the software into any other
44
+ # software (possibly commercial). But some files in the distribution
45
+ # are not written by the author, so that they are not under this terms.
46
+
47
+ # 5. The scripts and library files supplied as input to or produced as
48
+ # output from the software do not automatically fall under the
49
+ # copyright of the software, but belong to whomever generated them,
50
+ # and may be sold commercially, and may be aggregated with this
51
+ # software.
52
+
53
+ # 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
54
+ # IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
55
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
56
+ # PURPOSE.
57
+
58
+ # Implements a dynamic plugin loading, configuration, and discovery system
59
+ # based on RubyGems and a simple additional name space that looks like a URI.
60
+ #
61
+ # A plugin is created and put into a category with the following code:
62
+ #
63
+ # class MyThing < GemPlugin::Plugin "/things"
64
+ # ...
65
+ # end
66
+ #
67
+ # What this does is sets up your MyThing in the plugin registry via GemPlugin::Manager.
68
+ # You can then later get this plugin with GemPlugin::Manager.create("/things/mything")
69
+ # and can also pass in options as a second parameter.
70
+ #
71
+ # This isn't such a big deal, but the power is really from the GemPlugin::Manager.load
72
+ # method. This method will go through the installed gems and require_gem any
73
+ # that depend on the zergrush RubyGem. You can arbitrarily include or exclude
74
+ # gems based on what they also depend on, thus letting you load these gems when appropriate.
75
+ #
76
+ # Since this system was written originally for the Mongrel project that'll be the
77
+ # best example of using it.
78
+ #
79
+ # Imagine you have a neat plugin for Mongrel called snazzy_command that gives the
80
+ # mongrel_rails a new command snazzy (like: mongrel_rails snazzy). You'd like
81
+ # people to be able to grab this plugin if they want and use it, because it's snazzy.
82
+ #
83
+ # First thing you do is create a gem of your project and make sure that it depends
84
+ # on "mongrel" AND "zergrush". This signals to the GemPlugin system that this is
85
+ # a plugin for mongrel.
86
+ #
87
+ # Next you put this code into a file like lib/init.rb (can be anything really):
88
+ #
89
+ # class Snazzy < ZergGemPlugin::Plugin "/commands"
90
+ # ...
91
+ # end
92
+ #
93
+ # Then when you create your gem you have the following bits in your Rakefile:
94
+ #
95
+ # spec.add_dependency('mongrel', '>= 0.3.9')
96
+ # spec.add_dependency('zergrush', '>= 0.1')
97
+ # spec.autorequire = 'init.rb'
98
+ #
99
+ # Finally, you just have to now publish this gem for people to install and Mongrel
100
+ # will "magically" be able to install it.
101
+ #
102
+ # The "magic" part though is pretty simple and done via the GemPlugin::Manager.load
103
+ # method. Read that to see how it is really done.
104
+ module ZergGemPlugin
105
+
106
+ EXCLUDE = true
107
+ INCLUDE = false
108
+
109
+ class PluginNotLoaded < StandardError; end
110
+
111
+
112
+ # This class is used by people who use gem plugins (but don't necessarily make them)
113
+ # to add plugins to their own systems. It provides a way to load plugins, list them,
114
+ # and create them as needed.
115
+ #
116
+ # It is a singleton so you use like this: GemPlugins::Manager.instance.load
117
+ class Manager
118
+ include Singleton
119
+ attr_reader :plugins
120
+ attr_reader :gems
121
+
122
+
123
+ def initialize
124
+ # plugins that have been loaded
125
+ @plugins = {}
126
+
127
+ # keeps track of gems which have been loaded already by the manager *and*
128
+ # where they came from so that they can be referenced later
129
+ @gems = {}
130
+ end
131
+
132
+
133
+ # Responsible for going through the list of available gems and loading
134
+ # any plugins requested. It keeps track of what it's loaded already
135
+ # and won't load them again.
136
+ #
137
+ # It accepts one parameter which is a hash of gem depends that should include
138
+ # or exclude a gem from being loaded. A gem must depend on zergrush to be
139
+ # considered, but then each system has to add it's own INCLUDE to make sure
140
+ # that only plugins related to it are loaded.
141
+ #
142
+ # An example again comes from Mongrel. In order to load all Mongrel plugins:
143
+ #
144
+ # GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE
145
+ #
146
+ # Which will load all plugins that depend on mongrel AND zergrush. Now, one
147
+ # extra thing we do is we delay loading Rails Mongrel plugins until after rails
148
+ # is configured. Do do this the mongrel_rails script has:
149
+ #
150
+ # GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE, "rails" => GemPlugin::EXCLUDE
151
+ # The only thing to remember is that this is saying "include a plugin if it
152
+ # depends on zergrush, mongrel, but NOT rails". If a plugin also depends on other
153
+ # stuff then it's loaded just fine. Only zergrush, mongrel, and rails are
154
+ # ever used to determine if it should be included.
155
+ #
156
+ # NOTE: Currently RubyGems will run autorequire on gems that get required AND
157
+ # on their dependencies. This really messes with people running edge rails
158
+ # since activerecord or other stuff gets loaded for just touching a gem plugin.
159
+ # To prevent this load requires the full path to the "init.rb" file, which
160
+ # avoids the RubyGems autorequire magic.
161
+ def load(needs = {})
162
+ needs = needs.merge({"zergrush" => INCLUDE})
163
+
164
+ Gem::Specification.each { |gem|
165
+ # don't load gems more than once
166
+ next if @gems.has_key? gem.name
167
+ check = needs.dup
168
+
169
+ # rolls through the depends and inverts anything it finds
170
+ gem.dependencies.each do |dep|
171
+ # this will fail if a gem is depended more than once
172
+ if check.has_key? dep.name
173
+ check[dep.name] = !check[dep.name]
174
+ end
175
+ end
176
+
177
+ # now since excluded gems start as true, inverting them
178
+ # makes them false so we'll skip this gem if any excludes are found
179
+ if (check.select {|name,test| !test}).length == 0
180
+ # looks like no needs were set to false, so it's good
181
+ if gem.metadata["zergrushplugin"] != nil
182
+ require File.join(gem.gem_dir, "lib", gem.name, "init.rb")
183
+ @gems[gem.name] = gem.gem_dir
184
+ end
185
+ end
186
+ }
187
+
188
+ return nil
189
+ end
190
+
191
+
192
+ # Not necessary for you to call directly, but this is
193
+ # how GemPlugin::Base.inherited actually adds a
194
+ # plugin to a category.
195
+ def register(category, name, klass)
196
+ @plugins[category] ||= {}
197
+ @plugins[category][name.downcase] = klass
198
+ end
199
+
200
+
201
+ # Resolves the given name (should include /category/name) to
202
+ # find the plugin class and create an instance. You can
203
+ # pass a second hash option that is then given to the Plugin
204
+ # to configure it.
205
+ def create(name, options = {})
206
+ last_slash = name.rindex("/")
207
+ category = name[0 ... last_slash]
208
+ plugin = name[last_slash .. -1]
209
+
210
+ map = @plugins[category]
211
+ if not map
212
+ raise "Plugin category #{category} does not exist"
213
+ elsif not map.has_key? plugin
214
+ raise "Plugin #{plugin} does not exist in category #{category}"
215
+ else
216
+ map[plugin].new(options)
217
+ end
218
+ end
219
+
220
+
221
+ # Simply says whether the given gem has been loaded yet or not.
222
+ def loaded?(gem_name)
223
+ @gems.has_key? gem_name
224
+ end
225
+
226
+
227
+ # GemPlugins can have a 'resources' directory which is packaged with them
228
+ # and can hold any data resources the plugin may need. The main problem
229
+ # is that it's difficult to figure out where these resources are
230
+ # actually located on the file system. The resource method tries to
231
+ # locate the real path for a given resource path.
232
+ #
233
+ # Let's say you have a 'resources/stylesheets/default.css' file in your
234
+ # gem distribution, then finding where this file really is involves:
235
+ #
236
+ # Manager.instance.resource("mygem", "/stylesheets/default.css")
237
+ #
238
+ # You either get back the full path to the resource or you get a nil
239
+ # if it doesn't exist.
240
+ #
241
+ # If you request a path for a GemPlugin that hasn't been loaded yet
242
+ # then it will throw an PluginNotLoaded exception. The gem may be
243
+ # present on your system in this case, but you just haven't loaded
244
+ # it with Manager.instance.load properly.
245
+ def resource(gem_name, path)
246
+ if not loaded? gem_name
247
+ raise PluginNotLoaded.new("Plugin #{gem_name} not loaded when getting resource #{path}")
248
+ end
249
+
250
+ file = File.join(@gems[gem_name], "resources", path)
251
+
252
+ if File.exist? file
253
+ return file
254
+ else
255
+ return nil
256
+ end
257
+ end
258
+
259
+
260
+ # While Manager.resource will find arbitrary resources, a special
261
+ # case is when you need to load a set of configuration defaults.
262
+ # GemPlugin normalizes this to be if you have a file "resources/defaults.yaml"
263
+ # then you'll be able to load them via Manager.config.
264
+ #
265
+ # How you use the method is you get the options the user wants set, pass
266
+ # them to Manager.instance.config, and what you get back is a new Hash
267
+ # with the user's settings overriding the defaults.
268
+ #
269
+ # opts = Manager.instance.config "mygem", :age => 12, :max_load => .9
270
+ #
271
+ # In the above case, if defaults had {:age => 14} then it would be
272
+ # changed to 12.
273
+ #
274
+ # This loads the .yaml file on the fly every time, so doing it a
275
+ # whole lot is very stupid. If you need to make frequent calls to
276
+ # this, call it once with no options (Manager.instance.config) then
277
+ # use the returned defaults directly from then on.
278
+ def config(gem_name, options={})
279
+ config_file = Manager.instance.resource(gem_name, "/defaults.yaml")
280
+ if config_file
281
+ begin
282
+ defaults = YAML.load_file(config_file)
283
+ return defaults.merge(options)
284
+ rescue
285
+ raise "Error loading config #{config_file} for gem #{gem_name}"
286
+ end
287
+ else
288
+ return options
289
+ end
290
+ end
291
+ end
292
+
293
+ # This base class for plugins really does nothing
294
+ # more than wire up the new class into the right category.
295
+ # It is not thread-safe yet but will be soon.
296
+ class Base
297
+
298
+ attr_reader :options
299
+
300
+
301
+ # See Mongrel::Plugin for an explanation.
302
+ def Base.inherited(klass)
303
+ name = "/" + klass.to_s.downcase
304
+ Manager.instance.register(@@category, name, klass)
305
+ @@category = nil
306
+ end
307
+
308
+ # See Mongrel::Plugin for an explanation.
309
+ def Base.category=(category)
310
+ @@category = category
311
+ end
312
+
313
+ def initialize(options = {})
314
+ @options = options
315
+ end
316
+ end
317
+
318
+ # This nifty function works with the GemPlugin::Base to give you
319
+ # the syntax:
320
+ #
321
+ # class MyThing < GemPlugin::Plugin "/things"
322
+ # ...
323
+ # end
324
+ #
325
+ # What it does is temporarily sets the GemPlugin::Base.category, and then
326
+ # returns GemPlugin::Base. Since the next immediate thing Ruby does is
327
+ # use this returned class to create the new class, GemPlugin::Base.inherited
328
+ # gets called. GemPlugin::Base.inherited then uses the set category, class name,
329
+ # and class to register the plugin in the right way.
330
+ def ZergGemPlugin::Plugin(c)
331
+ Base.category = c
332
+ Base
333
+ end
334
+ end
data/lib/zerg/hive.rb CHANGED
@@ -27,11 +27,12 @@ require 'json-schema'
27
27
  require 'fileutils'
28
28
  require 'singleton'
29
29
  require 'highline/import'
30
+ require_relative 'erbalize'
30
31
 
31
32
  module Zerg
32
33
  class Hive
33
34
  include Singleton
34
- attr_reader :hive
35
+ attr_reader :hive, :load_path
35
36
 
36
37
  def loaded
37
38
  @loaded || false
@@ -42,12 +43,12 @@ module Zerg
42
43
  return
43
44
  end
44
45
 
45
- load_path = (ENV['HIVE_CWD'] == nil) ? File.join("#{Dir.pwd}", ".hive") : File.join("#{ENV['HIVE_CWD']}", ".hive")
46
- abort("ERROR: '.hive' not found at #{load_path}. Run 'zerg init', change HIVE_CWD or run zerg from a different path.") unless File.directory?(load_path)
46
+ @load_path = (ENV['HIVE_CWD'] == nil) ? File.join("#{Dir.pwd}", ".hive") : File.join("#{ENV['HIVE_CWD']}", ".hive")
47
+ abort("ERROR: '.hive' not found at #{@load_path}. Run 'zerg init', change HIVE_CWD or run zerg from a different path.") unless File.directory?(@load_path)
47
48
 
48
49
  # load all .ke files into one big hash
49
50
  @hive = Hash.new
50
- Dir.glob(File.join("#{load_path}", "*.ke")) do |ke_file|
51
+ Dir.glob(File.join("#{@load_path}", "*.ke")) do |ke_file|
51
52
  # do work on files ending in .rb in the desired directory
52
53
  begin
53
54
  ke_file_hash = JSON.parse( IO.read(ke_file) )
@@ -82,20 +83,32 @@ module Zerg
82
83
  end
83
84
 
84
85
  def self.verify
85
- load_path = (ENV['HIVE_CWD'] == nil) ? File.join("#{Dir.pwd}", ".hive") : File.join("#{ENV['HIVE_CWD']}", ".hive")
86
- abort("ERROR: '.hive' not found at #{load_path}. Run 'zerg init', change HIVE_CWD or run zerg from a different path.") unless File.directory?(load_path)
86
+ instance.load
87
87
 
88
- Dir.glob(File.join("#{load_path}", "*.ke")) do |ke_file|
88
+ Dir.glob(File.join("#{instance.load_path}", "*.ke")) do |ke_file|
89
89
  begin
90
- ke_file_hash = JSON.parse( IO.read(ke_file) )
90
+ ke_file_hash = JSON.parse( File.open(ke_file, 'r').read )
91
91
 
92
92
  # verify against schema.
93
- errors = JSON::Validator.fully_validate(File.join("#{File.dirname(__FILE__)}", "../../data/ke.schema"), ke_file_hash, :errors_as_objects => true)
94
- unless errors.empty?
95
- abort("ERROR: #{ke_file} failed validation. Errors: #{errors.ai}")
96
- end
93
+ # first get the tasks schema piece from the driver
94
+ pmgr = ZergGemPlugin::Manager.instance
95
+ pmgr.load
96
+ abort("ERROR: 'drivertype' is missing from #{ke_file}") unless ke_file_hash["vm"]["driver"]["drivertype"] != nil
97
+ driver = pmgr.create("/driver/#{ke_file_hash["vm"]["driver"]["drivertype"]}")
98
+ driver_schema = driver.task_schema
99
+
100
+ schema_template = File.open(File.join("#{File.dirname(__FILE__)}", "..", "..", "data", "ke.schema"), 'r').read
101
+ sources = {
102
+ :driver_tasks_schema => driver_schema
103
+ }
104
+ full_schema = JSON.parse(Erbalize.erbalize_hash(schema_template, sources))
105
+
106
+ errors = JSON::Validator.fully_validate(full_schema, ke_file_hash, :errors_as_objects => true)
107
+ abort("ERROR: #{ke_file} failed validation. Errors: #{errors.ai}") unless errors.empty?
97
108
  rescue JSON::ParserError => err
98
109
  abort("ERROR: Could not parse #{ke_file}. Likely invalid JSON.")
110
+ rescue ZergGemPlugin::PluginNotLoaded
111
+ abort("ERROR: driver #{ke_file_hash["vm"]["driver"]["drivertype"]} not found. Did you install the plugin gem?")
99
112
  end
100
113
  end
101
114
 
@@ -103,18 +116,32 @@ module Zerg
103
116
  end
104
117
 
105
118
  def self.import(file, force)
106
- load_path = (ENV['HIVE_CWD'] == nil) ? File.join("#{Dir.pwd}", ".hive") : File.join("#{ENV['HIVE_CWD']}", ".hive")
107
- abort("ERROR: '.hive' not found at #{load_path}. Run 'zerg init', change HIVE_CWD or run zerg from a different path.") unless File.directory?(load_path)
119
+ instance.load
108
120
  abort("ERROR: '#{file}' not found!") unless File.exist?(file)
109
- abort("ERROR: '#{File.basename(file)}' already exists in hive!") unless !File.exist?(File.join(load_path, File.basename(file))) || force == true
121
+ abort("ERROR: '#{File.basename(file)}' already exists in hive!") unless !File.exist?(File.join(instance.load_path, File.basename(file))) || force == true
110
122
 
111
123
  # check the file against schema.
112
124
  begin
113
- ke_file_hash = JSON.parse( IO.read(file) )
114
- errors = JSON::Validator.fully_validate(File.join("#{File.dirname(__FILE__)}", "../../data/ke.schema"), ke_file_hash, :errors_as_objects => true)
115
- abort("ERROR: #{file} failed validation. Errors: #{errors.ai}") unless errors.empty?
116
125
 
117
- FileUtils.cp(file, File.join(load_path, File.basename(file)))
126
+ ke_file_hash = JSON.parse( File.open(file, 'r').read )
127
+
128
+ # get the tasks schema piece from the driver
129
+ pmgr = ZergGemPlugin::Manager.instance
130
+ pmgr.load
131
+ abort("ERROR: 'drivertype' is missing from #{ke_file}") unless ke_file_hash["vm"]["driver"]["drivertype"] != nil
132
+ driver = pmgr.create("/driver/#{ke_file_hash["vm"]["driver"]["drivertype"]}")
133
+ driver_schema = driver.task_schema
134
+
135
+ schema_template = File.open(File.join("#{File.dirname(__FILE__)}", "..", "..", "data", "ke.schema"), 'r').read
136
+ sources = {
137
+ :driver_tasks_schema => driver_schema
138
+ }
139
+ full_schema = JSON.parse(Erbalize.erbalize_hash(schema_template, sources))
140
+
141
+ errors = JSON::Validator.fully_validate(full_schema, ke_file_hash, :errors_as_objects => true)
142
+ abort("ERROR: #{ke_file} failed validation. Errors: #{errors.ai}") unless errors.empty?
143
+
144
+ FileUtils.cp(file, File.join(instance.load_path, File.basename(file)))
118
145
  rescue JSON::ParserError => err
119
146
  abort("ERROR: Could not parse #{file}. Likely invalid JSON.")
120
147
  end
@@ -122,12 +149,11 @@ module Zerg
122
149
  end
123
150
 
124
151
  def self.remove(taskname, force)
125
- load_path = (ENV['HIVE_CWD'] == nil) ? File.join("#{Dir.pwd}", ".hive") : File.join("#{ENV['HIVE_CWD']}", ".hive")
126
- abort("ERROR: '.hive' not found at #{load_path}. Run 'zerg init', change HIVE_CWD or run zerg from a different path.") unless File.directory?(load_path)
127
- abort("ERROR: '#{taskname}' not found!") unless File.exist?(File.join(load_path, "#{taskname}.ke"))
152
+ instance.load
153
+ abort("ERROR: '#{taskname}' not found!") unless File.exist?(File.join(instance.load_path, "#{taskname}.ke"))
128
154
 
129
155
  # check the file against schema.
130
- taskfile = File.join(load_path, "#{taskname}.ke")
156
+ taskfile = File.join(instance.load_path, "#{taskname}.ke")
131
157
 
132
158
  agreed = true
133
159
  if force != true
@@ -136,8 +162,8 @@ module Zerg
136
162
 
137
163
  abort("Cancelled!") unless agreed == true
138
164
 
139
- FileUtils.rm_rf(File.join(load_path, "driver", taskname))
140
- FileUtils.rm(File.join(load_path, "#{taskname}.ke"))
165
+ FileUtils.rm_rf(File.join(instance.load_path, "driver", taskname))
166
+ FileUtils.rm(File.join(instance.load_path, "#{taskname}.ke"))
141
167
 
142
168
  puts "SUCCESS!"
143
169
  end