palletjack 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +2 -0
- data/.gitignore +53 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +243 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/palletjack.rb +71 -0
- data/lib/palletjack/keytransformer.rb +264 -0
- data/lib/palletjack/pallet.rb +80 -0
- data/lib/palletjack/tool.rb +463 -0
- data/lib/palletjack/version.rb +5 -0
- data/palletjack.gemspec +36 -0
- metadata +164 -0
- metadata.gz.sig +1 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
class PalletJack
|
|
2
|
+
class KeyTransformer
|
|
3
|
+
class Reader < KeyTransformer
|
|
4
|
+
def concatenate(param, context = {})
|
|
5
|
+
context[:value].split(param) if context[:value]
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class Writer < KeyTransformer
|
|
10
|
+
def concatenate(param, context = {})
|
|
11
|
+
context[:value].join(param) if context[:value]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Internal synthesize* helper method
|
|
15
|
+
# N.B. rdoc will not be generated, because method is private.
|
|
16
|
+
#
|
|
17
|
+
# :call-seq:
|
|
18
|
+
# synthesize_internal(param, dictionary) -> string or nil
|
|
19
|
+
#
|
|
20
|
+
# Use the single +String+ or +Enumerable+ containing +String+
|
|
21
|
+
# in +param+ to build and return a substitution value. If any
|
|
22
|
+
# failure occurs while building the new value, return +nil+.
|
|
23
|
+
#
|
|
24
|
+
# Substitutions are made from key-value pairs in +dictionary+
|
|
25
|
+
#
|
|
26
|
+
# YAML structure:
|
|
27
|
+
#
|
|
28
|
+
# - some_rule: "rule"
|
|
29
|
+
#
|
|
30
|
+
# or
|
|
31
|
+
#
|
|
32
|
+
# - some_rule:
|
|
33
|
+
# - "rule"
|
|
34
|
+
# - "rule"
|
|
35
|
+
# ...
|
|
36
|
+
#
|
|
37
|
+
# Rules are strings used to build the new value. The value of
|
|
38
|
+
# another key is inserted by <tt>#[key]</tt>, and all other
|
|
39
|
+
# characters are copied verbatim.
|
|
40
|
+
#
|
|
41
|
+
# Rules are evaluated in order, and the first one to
|
|
42
|
+
# successfully produce a value without failing a key lookup is
|
|
43
|
+
# used.
|
|
44
|
+
#
|
|
45
|
+
|
|
46
|
+
def synthesize_internal(param, dictionary, result=String.new)
|
|
47
|
+
case param
|
|
48
|
+
when String
|
|
49
|
+
rex=/#\[([[:alnum:]._-]+)\]/
|
|
50
|
+
if md=rex.match(param) then
|
|
51
|
+
result << md.pre_match
|
|
52
|
+
return unless lookup = dictionary[md[1]]
|
|
53
|
+
result << lookup.to_s
|
|
54
|
+
synthesize_internal(md.post_match, dictionary, result)
|
|
55
|
+
else
|
|
56
|
+
result << param
|
|
57
|
+
end
|
|
58
|
+
else # Enumerable
|
|
59
|
+
param.reduce(false) do |found_one, alternative|
|
|
60
|
+
found_one || synthesize_internal(alternative, dictionary)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
private :synthesize_internal
|
|
65
|
+
|
|
66
|
+
# Synthesize a pallet value by pasting others together.
|
|
67
|
+
#
|
|
68
|
+
# :call-seq:
|
|
69
|
+
# synthesize(param, context) -> string or nil
|
|
70
|
+
#
|
|
71
|
+
# If +context+ contains a non-nil +:value+, an earlier transform
|
|
72
|
+
# has already produced a value for this key, so do nothing and
|
|
73
|
+
# return +nil+.
|
|
74
|
+
#
|
|
75
|
+
# Otherwise, use the parsed YAML structure in +param+ to build
|
|
76
|
+
# and return a new value. If any failure occurs while building
|
|
77
|
+
# the new value, return +nil+ to let another transform try.
|
|
78
|
+
#
|
|
79
|
+
# YAML structure:
|
|
80
|
+
#
|
|
81
|
+
# - synthesize: "rule"
|
|
82
|
+
#
|
|
83
|
+
# or
|
|
84
|
+
#
|
|
85
|
+
# - synthesize:
|
|
86
|
+
# - "rule"
|
|
87
|
+
# - "rule"
|
|
88
|
+
# ...
|
|
89
|
+
#
|
|
90
|
+
# Rules are strings used to build the new value. The value of
|
|
91
|
+
# another key is inserted by <tt>#[key]</tt>, and all other
|
|
92
|
+
# characters are copied verbatim.
|
|
93
|
+
#
|
|
94
|
+
# Rules are evaluated in order, and the first one to
|
|
95
|
+
# successfully produce a value without failing a key lookup is
|
|
96
|
+
# used.
|
|
97
|
+
#
|
|
98
|
+
# Example:
|
|
99
|
+
#
|
|
100
|
+
# - net.dns.fqdn:
|
|
101
|
+
# - synthesize: "#[net.ip.name].#[domain.name]"
|
|
102
|
+
#
|
|
103
|
+
# - chassis.nic.name:
|
|
104
|
+
# - synthesize:
|
|
105
|
+
# - "p#[chassis.nic.pcislot]p#[chassis.nic.port]"
|
|
106
|
+
# - "em#[chassis.nic.port]"
|
|
107
|
+
|
|
108
|
+
def synthesize(param, context = {})
|
|
109
|
+
return if context[:value]
|
|
110
|
+
|
|
111
|
+
synthesize_internal(param, context[:pallet])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Synthesize a pallet value from others, using regular
|
|
115
|
+
# expressions to pull out parts of values.
|
|
116
|
+
#
|
|
117
|
+
# :call-seq:
|
|
118
|
+
# synthesize_regexp(param, context) -> string or nil
|
|
119
|
+
#
|
|
120
|
+
# If +context+ contains a non-nil +:value+, an earlier transform
|
|
121
|
+
# has already produced a value for this key, so do nothing and
|
|
122
|
+
# return +nil+.
|
|
123
|
+
#
|
|
124
|
+
# Otherwise, use the parsed YAML structure in +param+ to build
|
|
125
|
+
# and return a new value. If any failure occurs while building
|
|
126
|
+
# the new value, return +nil+ to let another transform try.
|
|
127
|
+
#
|
|
128
|
+
# YAML structure:
|
|
129
|
+
#
|
|
130
|
+
# - synthesize_regexp:
|
|
131
|
+
# sources:
|
|
132
|
+
# source0:
|
|
133
|
+
# key: "key"
|
|
134
|
+
# regexp: "regexp"
|
|
135
|
+
# source1:
|
|
136
|
+
# key: "key"
|
|
137
|
+
# regexp: "regexp"
|
|
138
|
+
# ...
|
|
139
|
+
# produce: "recipe"
|
|
140
|
+
#
|
|
141
|
+
# where:
|
|
142
|
+
# [+sourceN+] Arbitrary number of sources for partial values,
|
|
143
|
+
# with arbitrary names
|
|
144
|
+
# [+key+] Name of the key to read a partial value from
|
|
145
|
+
# [+regexp+] Regular expression for parsing the value indicated
|
|
146
|
+
# by +key+, with named captures used to save
|
|
147
|
+
# substrings for producing the final value. Capture
|
|
148
|
+
# names must not be repeated within the same
|
|
149
|
+
# synthesize_regexp block.
|
|
150
|
+
# [+produce+] A recipe for building the new value. Named
|
|
151
|
+
# captures are inserted by <tt>#[capture]</tt>, and
|
|
152
|
+
# all other characters are copied verbatim.
|
|
153
|
+
#
|
|
154
|
+
# Example:
|
|
155
|
+
#
|
|
156
|
+
# Take strings like +192.168.0.0_24+ from +pallet.ipv4_network+
|
|
157
|
+
# and produce strings like +192.168.0.0/24+ in +net.ipv4.cidr+.
|
|
158
|
+
#
|
|
159
|
+
# - net.ipv4.cidr:
|
|
160
|
+
# - synthesize_regexp:
|
|
161
|
+
# sources:
|
|
162
|
+
# ipv4_network:
|
|
163
|
+
# key: "pallet.ipv4_network"
|
|
164
|
+
# regexp: "^(?<network>[0-9.]+)_(?<prefix_length>[0-9]+)$"
|
|
165
|
+
# produce: "#[network]/#[prefix_length]"
|
|
166
|
+
|
|
167
|
+
def synthesize_regexp(param, context = {})
|
|
168
|
+
return if context[:value]
|
|
169
|
+
|
|
170
|
+
captures = {}
|
|
171
|
+
|
|
172
|
+
param["sources"].each do |_, source|
|
|
173
|
+
# Trying to read values from a non-existent key. Return nil
|
|
174
|
+
# and let another transform try.
|
|
175
|
+
return unless lookup = context[:pallet][source["key"]]
|
|
176
|
+
|
|
177
|
+
# Save all named captures
|
|
178
|
+
Regexp.new(source["regexp"]).match(lookup) do |md|
|
|
179
|
+
md.names.each do |name|
|
|
180
|
+
captures[name] = md[name.to_sym]
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
synthesize_internal(param["produce"], captures)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Synthesized value will override an inherited value for a
|
|
189
|
+
# +key+, but in some cases the intent is actually to only
|
|
190
|
+
# synthesize a value when there is no inherited value. This
|
|
191
|
+
# provides early termination of transforms for such keys.
|
|
192
|
+
#
|
|
193
|
+
# Example:
|
|
194
|
+
#
|
|
195
|
+
# - net.layer2.name:
|
|
196
|
+
# - inherit: ~
|
|
197
|
+
# - synthesize: "#[chassis.nic.name]"
|
|
198
|
+
|
|
199
|
+
def inherit(_, context = {})
|
|
200
|
+
throw context[:abort] if context[:pallet][context[:key]]
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def initialize(key_transforms={})
|
|
205
|
+
@key_transforms = key_transforms
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Destructively transform the values in +pallet+ according to the
|
|
209
|
+
# loaded transform rules.
|
|
210
|
+
#
|
|
211
|
+
# YAML structure:
|
|
212
|
+
#
|
|
213
|
+
# - key:
|
|
214
|
+
# - transform1:
|
|
215
|
+
# [transform-specific configuration]
|
|
216
|
+
# - transform2:
|
|
217
|
+
# [transform-specific configuration]
|
|
218
|
+
# [...]
|
|
219
|
+
#
|
|
220
|
+
# Transforms are evaluated in order from top to bottom, and the
|
|
221
|
+
# first one to successfully produce a value is used.
|
|
222
|
+
#
|
|
223
|
+
# Transforms are methods in PalletJack::KeyTransformer::Writer,
|
|
224
|
+
# called by name. They should return the new value, or +false+ if
|
|
225
|
+
# unsuccessful.
|
|
226
|
+
#
|
|
227
|
+
# Transforms are given two parameters, +param+ and +context+:
|
|
228
|
+
# [+param+] transform-specific configuration from transforms.yaml
|
|
229
|
+
# [+context+]
|
|
230
|
+
# [+pallet+] The pallet object being processed
|
|
231
|
+
# [+key+] The key from transforms.yaml being processed
|
|
232
|
+
# [+value+] Current locally assigned value for key in pallet
|
|
233
|
+
# [+abort+] #throw this to abort transforms for current key
|
|
234
|
+
|
|
235
|
+
def transform!(pallet)
|
|
236
|
+
@key_transforms.each do |keytrans_item|
|
|
237
|
+
|
|
238
|
+
# Enable early termination of transforms for a key
|
|
239
|
+
# by wrapping execution in a catch block.
|
|
240
|
+
catch do |abort_tag|
|
|
241
|
+
key, transforms = keytrans_item.flatten
|
|
242
|
+
context = {
|
|
243
|
+
pallet: pallet,
|
|
244
|
+
key: key,
|
|
245
|
+
value: pallet[key, shallow: true],
|
|
246
|
+
abort: abort_tag
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
transforms.each do |t|
|
|
250
|
+
transform, param = t.flatten
|
|
251
|
+
if self.respond_to?(transform.to_sym) then
|
|
252
|
+
if new_value = self.send(transform.to_sym, param, context)
|
|
253
|
+
then
|
|
254
|
+
pallet[key] = new_value
|
|
255
|
+
break
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
@hash
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
class PalletJack
|
|
2
|
+
# PalletJack managed pallet of key boxes inside a warehouse.
|
|
3
|
+
class Pallet < KVDAG::Vertex
|
|
4
|
+
|
|
5
|
+
attr_reader :name
|
|
6
|
+
attr_reader :kind
|
|
7
|
+
|
|
8
|
+
# N.B: A pallet should never be created manually; use
|
|
9
|
+
# +PalletJack::new+ to initialize a complete warehouse.
|
|
10
|
+
#
|
|
11
|
+
# [+jack+] PalletJack that will manage this pallet.
|
|
12
|
+
# [+path+] Filesystem path to pallet data.
|
|
13
|
+
#
|
|
14
|
+
# Create PalletJack managed singletonish pallet.
|
|
15
|
+
#
|
|
16
|
+
# Use relative path inside of warehouse as kind/name for this
|
|
17
|
+
# pallet, and make a singletonish object for that key.
|
|
18
|
+
|
|
19
|
+
def Pallet.new(jack, path) #:doc:
|
|
20
|
+
ppath, name = File.split(path)
|
|
21
|
+
_, kind = File.split(ppath)
|
|
22
|
+
|
|
23
|
+
jack.pallets[kind] ||= Hash.new
|
|
24
|
+
jack.pallets[kind][name] || super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# N.B: A pallet should never be created manually; use
|
|
28
|
+
# +PalletJack::new+ to initialize a complete warehouse.
|
|
29
|
+
#
|
|
30
|
+
# [+jack+] PalletJack that will manage this pallet.
|
|
31
|
+
# [+path+] Filesystem path to pallet data.
|
|
32
|
+
#
|
|
33
|
+
# Loads and merges all YAML files in +path+ into this Vertex.
|
|
34
|
+
#
|
|
35
|
+
# Follows all symlinks in +path+ and creates edges towards
|
|
36
|
+
# the pallet located in the symlink target.
|
|
37
|
+
|
|
38
|
+
private :initialize
|
|
39
|
+
def initialize(jack, path) #:notnew:
|
|
40
|
+
@jack = jack
|
|
41
|
+
@path = path
|
|
42
|
+
ppath, @name = File.split(path)
|
|
43
|
+
_, @kind = File.split(ppath)
|
|
44
|
+
boxes = Array.new
|
|
45
|
+
|
|
46
|
+
super(jack, pallet:{@kind => @name})
|
|
47
|
+
|
|
48
|
+
Dir.foreach(path) do |file|
|
|
49
|
+
next if file[0] == '.'
|
|
50
|
+
filepath = File.join(path, file)
|
|
51
|
+
filestat = File.lstat(filepath)
|
|
52
|
+
case
|
|
53
|
+
when (filestat.file? and file =~ /\.yaml$/)
|
|
54
|
+
merge!(YAML::load_file(filepath))
|
|
55
|
+
boxes << file
|
|
56
|
+
when filestat.symlink?
|
|
57
|
+
link = File.readlink(filepath)
|
|
58
|
+
_, lname = File.split(link)
|
|
59
|
+
|
|
60
|
+
pallet = Pallet.new(jack, File.absolute_path(link, path))
|
|
61
|
+
edge(pallet, pallet:{references:{file => lname}})
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
merge!(pallet:{boxes: boxes})
|
|
65
|
+
@jack.keytrans_writer.transform!(self)
|
|
66
|
+
@jack.pallets[@kind][@name] = self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def inspect
|
|
70
|
+
"#<%s:%x>" % [self.class, self.object_id, @path]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Override standard to_yaml serialization, because pallet objects
|
|
74
|
+
# are ephemeral by nature. The natural serialization is that of
|
|
75
|
+
# their to_hash analogue.
|
|
76
|
+
def to_yaml
|
|
77
|
+
to_hash.to_yaml
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
require 'palletjack'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'singleton'
|
|
5
|
+
require 'rugged'
|
|
6
|
+
|
|
7
|
+
class PalletJack
|
|
8
|
+
|
|
9
|
+
# Superclass for PalletJack tool implementations
|
|
10
|
+
#
|
|
11
|
+
# Provides convenience methods for option parsing, file generation,
|
|
12
|
+
# and warehouse structure management.
|
|
13
|
+
#
|
|
14
|
+
# Example:
|
|
15
|
+
# require 'palletjack/tool'
|
|
16
|
+
#
|
|
17
|
+
# class MyTool < PalletJack::Tool
|
|
18
|
+
# def parse_options(parser)
|
|
19
|
+
# parser.on('-o DIR', '--output DIR',
|
|
20
|
+
# 'output directory',
|
|
21
|
+
# String) {|dir| options[:output] = dir }
|
|
22
|
+
#
|
|
23
|
+
# required_option :output
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# attr_reader :state
|
|
27
|
+
#
|
|
28
|
+
# def process
|
|
29
|
+
# @state = {}
|
|
30
|
+
# jack.each(kind:'system') do |sys|
|
|
31
|
+
# @state[sys.name] = sys
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# def output
|
|
36
|
+
# @state.each do |name, data|
|
|
37
|
+
# config_dir :output, name
|
|
38
|
+
# config_file :output, name, "dump.yaml" do |file|
|
|
39
|
+
# file << data.to_yaml
|
|
40
|
+
# end
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# if __FILE__ == $0
|
|
46
|
+
# MyTool.run
|
|
47
|
+
# end
|
|
48
|
+
|
|
49
|
+
class Tool
|
|
50
|
+
include Singleton
|
|
51
|
+
|
|
52
|
+
# v0.1.1 API:
|
|
53
|
+
#
|
|
54
|
+
# :call-seq:
|
|
55
|
+
# run
|
|
56
|
+
#
|
|
57
|
+
# Main tool framework driver.
|
|
58
|
+
#
|
|
59
|
+
# Run the entire tool; setup, process and output. Actual tools
|
|
60
|
+
# will want to use this function, while testing and other
|
|
61
|
+
# activities that require poking around in internal state will
|
|
62
|
+
# want to run the partial functions instead.
|
|
63
|
+
#
|
|
64
|
+
# Example:
|
|
65
|
+
#
|
|
66
|
+
# if MyTool.standalone?(__FILE__)
|
|
67
|
+
# MyTool.run
|
|
68
|
+
# end
|
|
69
|
+
|
|
70
|
+
# v0.1.0 API, retained until all tools have been updated to
|
|
71
|
+
# v0.1.1:
|
|
72
|
+
#
|
|
73
|
+
# :call-seq:
|
|
74
|
+
# run &block
|
|
75
|
+
#
|
|
76
|
+
# Run the +block+ given in the context of the tool singleton instance
|
|
77
|
+
# as convenience for simple tools.
|
|
78
|
+
#
|
|
79
|
+
# More complex tools probably want to override parse_options to
|
|
80
|
+
# add option parsing, and split functionality into multiple methods.
|
|
81
|
+
#
|
|
82
|
+
# Example:
|
|
83
|
+
#
|
|
84
|
+
# MyTool.run { jack.each(kind:'system') {|sys| puts sys.to_yaml } }
|
|
85
|
+
|
|
86
|
+
def self.run(&block)
|
|
87
|
+
if block
|
|
88
|
+
# v0.1.0 API. When all tools have been ported, remove this and
|
|
89
|
+
# bump the minor version number.
|
|
90
|
+
instance.setup
|
|
91
|
+
instance.instance_eval(&block)
|
|
92
|
+
else
|
|
93
|
+
# v0.1.1 API
|
|
94
|
+
instance.setup
|
|
95
|
+
instance.process
|
|
96
|
+
instance.output
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Predicate for detecting if we are being invoked as a standalone
|
|
101
|
+
# tool, or loaded by e.g. a test framework.
|
|
102
|
+
|
|
103
|
+
def self.standalone?(file)
|
|
104
|
+
File::basename(file) == File::basename($0)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Generate data in an internal format, saving it for later testing
|
|
108
|
+
# or writing to disk by #output.
|
|
109
|
+
#
|
|
110
|
+
# Override this function in specific tool classes.
|
|
111
|
+
#
|
|
112
|
+
# Example:
|
|
113
|
+
#
|
|
114
|
+
# class MyTool < PalletJack::Tool
|
|
115
|
+
# def process
|
|
116
|
+
# @systems = Set.new
|
|
117
|
+
# jack.each(kind:'system') do |s|
|
|
118
|
+
# @systems << s
|
|
119
|
+
# end
|
|
120
|
+
# end
|
|
121
|
+
# end
|
|
122
|
+
|
|
123
|
+
def process
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Output data in its final format, probably to disk or stdout.
|
|
127
|
+
#
|
|
128
|
+
# Example:
|
|
129
|
+
#
|
|
130
|
+
# class MyTool < PalletJack::Tool
|
|
131
|
+
# def output
|
|
132
|
+
# @systems.each do |s|
|
|
133
|
+
# puts s.name
|
|
134
|
+
# end
|
|
135
|
+
# end
|
|
136
|
+
# end
|
|
137
|
+
|
|
138
|
+
def output
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Return the command line argument list to be used. Replace this
|
|
142
|
+
# method when testing.
|
|
143
|
+
|
|
144
|
+
def argv
|
|
145
|
+
ARGV
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Set up the singleton instance
|
|
149
|
+
#
|
|
150
|
+
# Default setup will add options for --warehouse and --help to the
|
|
151
|
+
# OptionParser, and set the banner to something useful.
|
|
152
|
+
#
|
|
153
|
+
# Any exceptions raised during option parsing will abort execution
|
|
154
|
+
# with usage information.
|
|
155
|
+
|
|
156
|
+
def setup
|
|
157
|
+
@parser = OptionParser.new
|
|
158
|
+
@options = {}
|
|
159
|
+
@option_checks = []
|
|
160
|
+
|
|
161
|
+
@parser.banner = "Usage: #{$PROGRAM_NAME} -w <warehouse> [options]"
|
|
162
|
+
@parser.separator ''
|
|
163
|
+
@parser.on('-w DIR', '--warehouse DIR',
|
|
164
|
+
'warehouse directory', String) {|dir|
|
|
165
|
+
@options[:warehouse] = dir }
|
|
166
|
+
@parser.on_tail('-h', '--help', 'display usage information') {
|
|
167
|
+
raise ArgumentError }
|
|
168
|
+
|
|
169
|
+
parse_options(@parser)
|
|
170
|
+
|
|
171
|
+
@parser.parse!(argv)
|
|
172
|
+
@option_checks.each {|check| check.call }
|
|
173
|
+
rescue
|
|
174
|
+
abort(usage)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Additional option parsing
|
|
178
|
+
#
|
|
179
|
+
# The default instance initalization will add option parsing for
|
|
180
|
+
# <tt>-w</tt>/<tt>--warehouse</tt> and <tt>-h</tt>/<tt>--help</tt>,
|
|
181
|
+
# and a simple banner string.
|
|
182
|
+
#
|
|
183
|
+
# Implementations needing more options than the default, a more
|
|
184
|
+
# informative banner, or requirement checks for parsed options should
|
|
185
|
+
# override this empty method.
|
|
186
|
+
#
|
|
187
|
+
# Any exceptions raised will abort execution with usage information.
|
|
188
|
+
#
|
|
189
|
+
# Example:
|
|
190
|
+
#
|
|
191
|
+
# class MyTool < PalletJack::Tool
|
|
192
|
+
# def parse_options(parser)
|
|
193
|
+
# parser.on('-o DIR', '--output DIR',
|
|
194
|
+
# 'output directory',
|
|
195
|
+
# String) {|dir| options[:output] = dir }
|
|
196
|
+
#
|
|
197
|
+
# required_option :output
|
|
198
|
+
# end
|
|
199
|
+
# end
|
|
200
|
+
|
|
201
|
+
def parse_options(parser)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Require the presence of one of the given options.
|
|
205
|
+
#
|
|
206
|
+
# Must not be called outside the scope of the parse_options method.
|
|
207
|
+
#
|
|
208
|
+
# Raises ArgumentError if none exist in options[]
|
|
209
|
+
#
|
|
210
|
+
# Example:
|
|
211
|
+
#
|
|
212
|
+
# def parse_options(parser)
|
|
213
|
+
# ...
|
|
214
|
+
# required_option :output
|
|
215
|
+
# end
|
|
216
|
+
|
|
217
|
+
def required_option(*opts)
|
|
218
|
+
@option_checks << lambda do
|
|
219
|
+
raise ArgumentError unless opts.any? {|opt| options[opt]}
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Require the presence of no more than one of the given options.
|
|
224
|
+
#
|
|
225
|
+
# Must not be called outside the scope of the parse_options method.
|
|
226
|
+
#
|
|
227
|
+
# Raises ArgumentError if more than one exist in options[]
|
|
228
|
+
#
|
|
229
|
+
# Example:
|
|
230
|
+
#
|
|
231
|
+
# def parse_options(parse)
|
|
232
|
+
# ...
|
|
233
|
+
# required_option :output_file, :output_stdout
|
|
234
|
+
# exclusive_options :output_file, :output_stdout
|
|
235
|
+
# end
|
|
236
|
+
|
|
237
|
+
def exclusive_options(*opts)
|
|
238
|
+
@option_checks << lambda do
|
|
239
|
+
raise ArgumentError if opts.count {|opt| options[opt]} > 1
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Usage information from option parser
|
|
244
|
+
#
|
|
245
|
+
# Example:
|
|
246
|
+
#
|
|
247
|
+
# abort(usage) unless options[:warehouse]
|
|
248
|
+
|
|
249
|
+
def usage
|
|
250
|
+
@parser.to_s
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Hash containing all parsed options.
|
|
254
|
+
|
|
255
|
+
attr_reader :options
|
|
256
|
+
|
|
257
|
+
# Pallet containing all warehouse defined configuration options
|
|
258
|
+
#
|
|
259
|
+
# Configuration options for tools can be stored as pallets in
|
|
260
|
+
# the warehouse:
|
|
261
|
+
#
|
|
262
|
+
# _config
|
|
263
|
+
# |
|
|
264
|
+
# +-- MyTool
|
|
265
|
+
# | |
|
|
266
|
+
# | `-- somecfg.yaml
|
|
267
|
+
|
|
268
|
+
def config
|
|
269
|
+
@config ||= jack.fetch(kind:'_config',
|
|
270
|
+
name: self.class.to_s) rescue Hash.new
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Return the PalletJack object for <tt>--warehouse</tt>
|
|
274
|
+
# Aborts execution with usage message if the warehouse was
|
|
275
|
+
# not specified.
|
|
276
|
+
|
|
277
|
+
def jack
|
|
278
|
+
abort(usage) unless options[:warehouse]
|
|
279
|
+
@jack ||= PalletJack.load(options[:warehouse])
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Build a filesystem path from path components
|
|
283
|
+
#
|
|
284
|
+
# Symbols are looked up in the options dictionary.
|
|
285
|
+
# All other types are converted to strings. The
|
|
286
|
+
# resulting list is fed to File#join to produce a
|
|
287
|
+
# local filesystem compliant path.
|
|
288
|
+
#
|
|
289
|
+
# Example:
|
|
290
|
+
# parser.on(...) {|dir| options[:output] = dir }
|
|
291
|
+
# ...
|
|
292
|
+
# config_path :output, 'subdir1'
|
|
293
|
+
# config_path :output, 'subdir2'
|
|
294
|
+
|
|
295
|
+
def config_path(*path)
|
|
296
|
+
File.join(path.map {|item|
|
|
297
|
+
case item
|
|
298
|
+
when Symbol
|
|
299
|
+
options.fetch(item)
|
|
300
|
+
else
|
|
301
|
+
item.to_s
|
|
302
|
+
end
|
|
303
|
+
})
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# :call-seq:
|
|
307
|
+
# config_dir '', 'path', 'name'
|
|
308
|
+
# config_dir :option, 'subdir', ...
|
|
309
|
+
#
|
|
310
|
+
# Creates a directory if it doesn't already exist.
|
|
311
|
+
#
|
|
312
|
+
# Uses config_path to construct the path, so any symbols will
|
|
313
|
+
# be looked up in the options hash.
|
|
314
|
+
#
|
|
315
|
+
# Example:
|
|
316
|
+
#
|
|
317
|
+
# config_dir :output, system.name
|
|
318
|
+
|
|
319
|
+
def config_dir(*path)
|
|
320
|
+
Dir.mkdir(config_path(*path))
|
|
321
|
+
rescue Errno::EEXIST
|
|
322
|
+
nil
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# :call-seq:
|
|
326
|
+
# config_file 'filename' {|file| ... }
|
|
327
|
+
# config_file :option, 'fragment', 'base.ext' {|file| ... }
|
|
328
|
+
# config_file ..., mode:0600 {|file| ...}
|
|
329
|
+
#
|
|
330
|
+
# Creates a configuration file, with default mode:0644
|
|
331
|
+
# and calls the given block with the file as argument.
|
|
332
|
+
#
|
|
333
|
+
# Uses config_path to construct the path, so any symbols will
|
|
334
|
+
# be looked up in the options hash.
|
|
335
|
+
#
|
|
336
|
+
# N.B! If the file already exists, it will be overwritten!
|
|
337
|
+
#
|
|
338
|
+
# Example:
|
|
339
|
+
#
|
|
340
|
+
# config_file :output, system.name, 'dump.yaml' do |file|
|
|
341
|
+
# file << system.to_yaml
|
|
342
|
+
# end
|
|
343
|
+
|
|
344
|
+
def config_file(*path, mode:0644, &block)
|
|
345
|
+
File.open(config_path(*path),
|
|
346
|
+
File::CREAT | File::TRUNC | File::WRONLY, mode) do |file|
|
|
347
|
+
block.call(file)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Create a new pallet directory inside the warehouse
|
|
352
|
+
#
|
|
353
|
+
# Uses config_dir to create the directory, so any symbols will
|
|
354
|
+
# be looked up in the options hash.
|
|
355
|
+
#
|
|
356
|
+
# This is effectively a noop if the pallet already exists.
|
|
357
|
+
#
|
|
358
|
+
# Example:
|
|
359
|
+
#
|
|
360
|
+
# pallet_dir 'system', :system_name
|
|
361
|
+
|
|
362
|
+
def pallet_dir(kind, name)
|
|
363
|
+
config_dir :warehouse, kind
|
|
364
|
+
config_dir :warehouse, kind, name
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Write a key box file inside a pallet
|
|
368
|
+
#
|
|
369
|
+
# The block should return a hash representing the contents of the box.
|
|
370
|
+
# All keys will be stringified, so we can use key: short forms for
|
|
371
|
+
# declaration of the box contents.
|
|
372
|
+
#
|
|
373
|
+
# Uses config_file to create the file, so any symbols will
|
|
374
|
+
# be looked up in the options hash.
|
|
375
|
+
#
|
|
376
|
+
# N.B! If the box already exists, it will be overwritten!
|
|
377
|
+
#--
|
|
378
|
+
# FIXME: should new values be merged with existing box data instead?
|
|
379
|
+
#++
|
|
380
|
+
#
|
|
381
|
+
# Example:
|
|
382
|
+
#
|
|
383
|
+
# pallet_box 'domain', :domain, 'dns' do
|
|
384
|
+
# { dns:{ ns:options[:soa_ns].split(',') } }
|
|
385
|
+
# end
|
|
386
|
+
|
|
387
|
+
def pallet_box(kind, name, box, &block)
|
|
388
|
+
config_file :warehouse, kind, name, "#{box}.yaml" do |box_file|
|
|
389
|
+
box_file << block.call.deep_stringify_keys.to_yaml
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Create links from a pallet to parents
|
|
394
|
+
#
|
|
395
|
+
# Uses config_path to construct paths within the warehouse, so any
|
|
396
|
+
# symbols will be looked up in the options hash.
|
|
397
|
+
#
|
|
398
|
+
# +links+ is a hash containing +link_type+=>[+parent_kind+, +parent_name+]
|
|
399
|
+
#
|
|
400
|
+
# If the link target is empty (e.g. +link_type+=>[]), the link is removed.
|
|
401
|
+
#
|
|
402
|
+
# Example:
|
|
403
|
+
#
|
|
404
|
+
# pallet_links 'system', :system, 'os'=>['os', :os], 'netinstall'=>[]
|
|
405
|
+
|
|
406
|
+
def pallet_links(kind, name, links={})
|
|
407
|
+
links.each do |link_type, parent|
|
|
408
|
+
link_path = config_path(:warehouse, kind, name, link_type)
|
|
409
|
+
|
|
410
|
+
begin
|
|
411
|
+
File.delete(link_path)
|
|
412
|
+
rescue Errno::ENOENT
|
|
413
|
+
nil
|
|
414
|
+
end
|
|
415
|
+
unless parent.empty?
|
|
416
|
+
parent_kind, parent_name = parent
|
|
417
|
+
parent_path = config_path('..', '..', parent_kind, parent_name)
|
|
418
|
+
|
|
419
|
+
File.symlink(parent_path, link_path)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Return a string stating the Git provenance of a warehouse directory,
|
|
425
|
+
# suitable for inclusion at the top of a generated configuration file,
|
|
426
|
+
# with each line prefixed by +comment_char+.
|
|
427
|
+
#
|
|
428
|
+
# If <tt>options[:warehouse]</tt> points within a Git repository,
|
|
429
|
+
# return a string stating its absolute path and active branch. If
|
|
430
|
+
# +include_id+ is true, also include the commit ID of the branch's
|
|
431
|
+
# HEAD.
|
|
432
|
+
#
|
|
433
|
+
# If Git information cannot be found for
|
|
434
|
+
# <tt>options[:warehouse]</tt>, return a string stating its path
|
|
435
|
+
# and print an error message on stderr.
|
|
436
|
+
|
|
437
|
+
def git_header(tool_name, comment_char: '#', include_id: false)
|
|
438
|
+
repo = Rugged::Repository.discover(options[:warehouse])
|
|
439
|
+
workdir = repo.workdir
|
|
440
|
+
branch = repo.head.name
|
|
441
|
+
commit = repo.head.target_id
|
|
442
|
+
|
|
443
|
+
header =
|
|
444
|
+
"#{comment_char}#{comment_char}
|
|
445
|
+
#{comment_char}#{comment_char} Automatically generated by #{tool_name} from
|
|
446
|
+
#{comment_char}#{comment_char} Repository: #{workdir}
|
|
447
|
+
#{comment_char}#{comment_char} Branch: #{branch}\n"
|
|
448
|
+
if include_id
|
|
449
|
+
then
|
|
450
|
+
header +=
|
|
451
|
+
"#{comment_char}#{comment_char} Commit ID: #{repo.head.target_id}\n"
|
|
452
|
+
end
|
|
453
|
+
header += "#{comment_char}#{comment_char}\n"
|
|
454
|
+
return header
|
|
455
|
+
rescue
|
|
456
|
+
STDERR.puts "Error finding Git sourcing information: #{$!}"
|
|
457
|
+
return "#{comment_char}#{comment_char}
|
|
458
|
+
#{comment_char}#{comment_char} Automatically generated by #{tool_name} from
|
|
459
|
+
#{comment_char}#{comment_char} Warehouse: #{warehouse_path}
|
|
460
|
+
#{comment_char}#{comment_char}\n"
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|