blavoshost 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/bin/blavoshost +818 -0
- data/test/helper.rb +10 -0
- data/test/test_blavoshost.rb +7 -0
- metadata +77 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Jerrod Blavos
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
= blavoshost
|
2
|
+
|
3
|
+
Description goes here.
|
4
|
+
|
5
|
+
== Note on Patches/Pull Requests
|
6
|
+
|
7
|
+
* Fork the project.
|
8
|
+
* Make your feature addition or bug fix.
|
9
|
+
* Add tests for it. This is important so I don't break it in a
|
10
|
+
future version unintentionally.
|
11
|
+
* Commit, do not mess with rakefile, version, or history.
|
12
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
13
|
+
* Send me a pull request. Bonus points for topic branches.
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2010 Jerrod Blavos. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
load File.expand_path('../bin/blavoshost', __FILE__)
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |gem|
|
8
|
+
gem.name = "blavoshost"
|
9
|
+
gem.summary = %Q{Add local vhosts}
|
10
|
+
gem.description = %Q{A tool to add local vhosts ot apache/OSX}
|
11
|
+
gem.email = "jerrod@indierockmedia.com"
|
12
|
+
gem.homepage = "http://github.com/jerrod/blavoshost"
|
13
|
+
gem.authors = ["Jerrod Blavos"]
|
14
|
+
gem.executables = ['blavoshost']
|
15
|
+
gem.require_paths = ["bin"] # annoying requirement
|
16
|
+
end
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
20
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
data/bin/blavoshost
ADDED
@@ -0,0 +1,818 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
3
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'json'
|
7
|
+
require 'open3'
|
8
|
+
require 'ostruct'
|
9
|
+
require 'optparse'
|
10
|
+
require 'fileutils'
|
11
|
+
require 'ruby-debug'
|
12
|
+
|
13
|
+
module Hipe
|
14
|
+
module Vhost
|
15
|
+
HardCoded = OpenStruct.new(
|
16
|
+
:execs => ['apachectl','apache2ctl'],
|
17
|
+
:httpd_conf => ['/etc/apache2/apache2.conf',
|
18
|
+
'/etc/apache2/httpd.conf'],
|
19
|
+
:docroots => '~/Sites',
|
20
|
+
:vhost_confs => '/etc/apache2/sites-enabled/',
|
21
|
+
:etc_hosts => '/etc/hosts'
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
module Hipe
|
28
|
+
|
29
|
+
# Every exception thrown directly by this library will be a kind of
|
30
|
+
# Hipe::Fail. Subclasses are things like UserFail, EnvFail and
|
31
|
+
# PermissionFail, which are exceptions resulting from something
|
32
|
+
# the user did wrong, something in the environment that won't permit us
|
33
|
+
# to carry out a request, or a permissions issue with the filesystem
|
34
|
+
# or process space; respectively. The implementing client application will
|
35
|
+
# usually want to catch these and recover gracefully from them.
|
36
|
+
|
37
|
+
# There are corresponding modules for each of these exception classes
|
38
|
+
# (a UserFail is a kind of UserFailey (the adjectivial form) for example),
|
39
|
+
# so we can enhance exceptions thrown from other libraries as well,
|
40
|
+
# and re-throw them as that original exception object with its stack intact.
|
41
|
+
# All exception classes in this library end in 'Fail', and their
|
42
|
+
# corresponding modules end in 'Failey' (the adjective form.)
|
43
|
+
# So note that an exception caught from another library and 'enhanced'
|
44
|
+
# by this one will not be a kind of Hipe::Fail, only a kind of Hipe::Failey.
|
45
|
+
|
46
|
+
# So to have your bases covered, rescue Hipe::Failey or the subset of
|
47
|
+
# Hipe::Failies that you care about rescuing.
|
48
|
+
|
49
|
+
module Failey; end
|
50
|
+
module EnvFailey; include Failey end
|
51
|
+
module UserFailey; include Failey end
|
52
|
+
module PermissionFailey; include UserFailey end
|
53
|
+
|
54
|
+
class Fail < RuntimeError; include Failey end
|
55
|
+
class UserFail < Fail; include UserFailey end
|
56
|
+
class EnvFail < Fail; include EnvFailey end
|
57
|
+
class PermissionFail < UserFail; include PermissionFailey end
|
58
|
+
|
59
|
+
# AsciiFormattey last seen in f2fa2
|
60
|
+
|
61
|
+
# this guy was moved into ack-lite-parse and modified. if you
|
62
|
+
# want any more features, start with that one and note it there
|
63
|
+
module CliLitey
|
64
|
+
# if this thing exceeds 30 lines of code use hipe-core/interfacey
|
65
|
+
def cli_run(argv)
|
66
|
+
return help_text unless argv[0] && @actions[argv[0]]
|
67
|
+
action = argv.shift
|
68
|
+
action = help if (['-h','--help'].include?(action))
|
69
|
+
meth = action
|
70
|
+
send(meth, argv)
|
71
|
+
end
|
72
|
+
|
73
|
+
def program_name
|
74
|
+
File.basename $PROGRAM_NAME
|
75
|
+
end
|
76
|
+
|
77
|
+
def help(args)
|
78
|
+
help_text+"\n\n"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module FileyCoyote
|
83
|
+
def self.writable!(path)
|
84
|
+
if ! File.writable?(path)
|
85
|
+
raise PermissionFail.new("Can't write to #{path}")
|
86
|
+
end
|
87
|
+
@path = path
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
def writable!
|
91
|
+
FileyCoyote.writable!(@path)
|
92
|
+
self
|
93
|
+
end
|
94
|
+
def fileize str
|
95
|
+
FileyCoyote.fileize str
|
96
|
+
end
|
97
|
+
def self.fileize str
|
98
|
+
str.gsub(/[^-\.a-zA-Z0-9]/,'')
|
99
|
+
end
|
100
|
+
def back_that_asset_up! orig_path
|
101
|
+
dry = respond_to?(:dry_run?) ? dry_run? : false
|
102
|
+
append = Time.now.strftime('%Y-%m-%d_%H:%M:%S')
|
103
|
+
new_name = %{#{orig_path}.bak.#{append}}
|
104
|
+
i = 1
|
105
|
+
while File.exist? new_name # very unlikely
|
106
|
+
new_name = %{#{orig_path}.bak.#{append}.#{i}}
|
107
|
+
i += 1
|
108
|
+
end
|
109
|
+
begin
|
110
|
+
FileUtils.copy_file orig_path, new_name unless dry
|
111
|
+
rescue Errno::EACCES => e
|
112
|
+
e.extend OpenStructey
|
113
|
+
e.extend PermissionFailey
|
114
|
+
e.filename = new_name
|
115
|
+
raise e
|
116
|
+
end
|
117
|
+
new_name
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
module DryRunney
|
122
|
+
def dry_run= val
|
123
|
+
@dry_run = val
|
124
|
+
end
|
125
|
+
def dry_run?
|
126
|
+
@dry_run
|
127
|
+
end
|
128
|
+
alias_method :noop, :dry_run?
|
129
|
+
def dry_run!
|
130
|
+
@dry_run = true
|
131
|
+
end
|
132
|
+
def report
|
133
|
+
wtf = @report ||= []
|
134
|
+
wtf
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
module OpenStructey
|
139
|
+
def self.extend_object(base)
|
140
|
+
base.instance_eval do
|
141
|
+
@__info = {}
|
142
|
+
end
|
143
|
+
super
|
144
|
+
end
|
145
|
+
def method_missing name, *args, &blah
|
146
|
+
if /^(.+)=$/ =~ name.to_s
|
147
|
+
@__info[$1.to_sym] = args[0]
|
148
|
+
else
|
149
|
+
@__info[name.to_sym]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
module Systematic
|
155
|
+
def sistum command
|
156
|
+
stdin, stdout, stderr = Open3.popen3 command
|
157
|
+
err = stderr.read; out = stdout.read
|
158
|
+
raise Fail.new(err) if "" != err
|
159
|
+
out.chop
|
160
|
+
end
|
161
|
+
|
162
|
+
def sistum2 command
|
163
|
+
stdin, stdout, stderr = Open3.popen3 command
|
164
|
+
err = stderr.read; out = stdout.read
|
165
|
+
[out.chop,err.chop]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
module Templatesimal
|
170
|
+
# the last thing the world needs is...
|
171
|
+
|
172
|
+
NamePattern = /\{\{([^\}]+)\}\}/ # e.g. "{{title}}"
|
173
|
+
|
174
|
+
def self.[] str
|
175
|
+
str.extend self
|
176
|
+
str.init_templatesimal
|
177
|
+
str
|
178
|
+
end
|
179
|
+
|
180
|
+
def init_templatesimal
|
181
|
+
names = scan(NamePattern).map{|x| x[0]}.uniq
|
182
|
+
meta = class << self; end
|
183
|
+
names.each do |name|
|
184
|
+
unless respond_to?(name)
|
185
|
+
meta = class << self; self; end
|
186
|
+
meta.send(:define_method, :"#{name}=") do |x|
|
187
|
+
template_replace(name, x)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def template_replace name, value
|
194
|
+
gsub! "{{#{name}}}", value
|
195
|
+
end
|
196
|
+
|
197
|
+
def substitute! hash
|
198
|
+
hash.each{|pair| template_replace(pair[0],pair[1]) }
|
199
|
+
self
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
class AsciiTableLite
|
204
|
+
#
|
205
|
+
# For now this is the official home of this here in vhost.
|
206
|
+
# This is a simplified version of hipe-core/struct/table
|
207
|
+
# Note any other projects that copy paste this here:
|
208
|
+
# [none]
|
209
|
+
#
|
210
|
+
|
211
|
+
def initialize(*cols)
|
212
|
+
@cols = cols
|
213
|
+
@pipe = '|'
|
214
|
+
@cona = '+'
|
215
|
+
@rule = '-'
|
216
|
+
@rulesep = @rule * 3
|
217
|
+
@sep = " #{@pipe} "
|
218
|
+
end
|
219
|
+
|
220
|
+
def render rows
|
221
|
+
@rows = rows
|
222
|
+
@maxes = Hash[*@cols.map{|x|[x, x.to_s.length]}.flatten]
|
223
|
+
prerendereds = []
|
224
|
+
rows.each do |info|
|
225
|
+
preprender = OpenStruct.new
|
226
|
+
@cols.each do |col|
|
227
|
+
cel = info.send(col).to_s
|
228
|
+
preprender.send("#{col}=",cel)
|
229
|
+
if (@maxes[col].nil? || cel.length > @maxes[col])
|
230
|
+
@maxes[col] = cel.length
|
231
|
+
end
|
232
|
+
end
|
233
|
+
prerendereds.push preprender
|
234
|
+
end
|
235
|
+
@lines = [go_sep]
|
236
|
+
@lines.push go_headers
|
237
|
+
@lines.push go_sep
|
238
|
+
prerendereds.each do |info|
|
239
|
+
@lines.push go_row info
|
240
|
+
end
|
241
|
+
@lines.push go_sep
|
242
|
+
@lines * "\n"
|
243
|
+
end
|
244
|
+
|
245
|
+
## private
|
246
|
+
|
247
|
+
def go_sep
|
248
|
+
"#{@cona}#{@rule}"<<
|
249
|
+
"#{@cols.map{|x| @rule * @maxes[x]} * @rulesep }"<<
|
250
|
+
"#{@rule}#{@cona}"
|
251
|
+
end
|
252
|
+
|
253
|
+
def go_headers
|
254
|
+
"#{@pipe} "<<
|
255
|
+
(@cols.map{|x|x.to_s.ljust(@maxes[x])}* @sep)<<
|
256
|
+
" #{@pipe}"
|
257
|
+
end
|
258
|
+
|
259
|
+
def go_row row
|
260
|
+
"#{@pipe} "<<
|
261
|
+
(@cols.map{|x|row.send(x).to_s.ljust(@maxes[x])}*@sep)<<
|
262
|
+
" #{@pipe}"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
class UnitOfWork
|
267
|
+
include DryRunney
|
268
|
+
attr_reader :describe
|
269
|
+
def initialize(desc, &block)
|
270
|
+
@describe = desc
|
271
|
+
@block = block
|
272
|
+
end
|
273
|
+
def empty?
|
274
|
+
@block.nil?
|
275
|
+
end
|
276
|
+
def commit
|
277
|
+
@block.call(self) if @block
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
module UnitsOfWork
|
282
|
+
def add_unit_of_work(*args, &block)
|
283
|
+
units_of_work.push UnitOfWork.new(*args,&block)
|
284
|
+
end
|
285
|
+
def units_of_work
|
286
|
+
@units_of_work ||= []
|
287
|
+
end
|
288
|
+
def run_units_of_work dry_run=false
|
289
|
+
num_done = 0
|
290
|
+
units_of_work.each do |unit|
|
291
|
+
if unit.empty?
|
292
|
+
puts unit.describe
|
293
|
+
else
|
294
|
+
unit.dry_run = dry_run
|
295
|
+
unit.commit
|
296
|
+
puts unit.describe
|
297
|
+
unit.report.each do |line|
|
298
|
+
puts " #{line}"
|
299
|
+
end
|
300
|
+
num_done += 1
|
301
|
+
end
|
302
|
+
end
|
303
|
+
num_done
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
module Vhost
|
308
|
+
|
309
|
+
class Fail < RuntimeError; end
|
310
|
+
|
311
|
+
class Cli
|
312
|
+
include Hipe::CliLitey, Hipe::Systematic, Hipe::UnitsOfWork
|
313
|
+
Table = Hipe::AsciiTableLite
|
314
|
+
|
315
|
+
def initialize
|
316
|
+
@actions = {'help'=>1,'explain'=>1,'hosts'=>1,'confs'=>1,
|
317
|
+
'dump' =>1, 'list'=>1, 'add'=>1
|
318
|
+
}
|
319
|
+
@path = {}
|
320
|
+
%w(docroots vhost_confs etc_hosts).each do |guy|
|
321
|
+
@path[guy.to_sym] = File.expand_path HardCoded.send(guy)
|
322
|
+
end
|
323
|
+
these = HardCoded.httpd_conf
|
324
|
+
one = these.detect{ |x| File.exist?(x) }
|
325
|
+
one or fail("not found: #{these.join(', ')}")
|
326
|
+
@path[:httpd_conf] = one
|
327
|
+
get_orientated_with_your_environment
|
328
|
+
@path = OpenStruct.new(@path)
|
329
|
+
end
|
330
|
+
|
331
|
+
def run argv
|
332
|
+
begin
|
333
|
+
cli_run argv
|
334
|
+
rescue PermissionFailey => e
|
335
|
+
return "#{e.message} - do you need to run this with sudo?"
|
336
|
+
rescue EnvFailey, UserFailey => e
|
337
|
+
return "#{e.message}"
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def explain args; HardCoded.explain end
|
342
|
+
|
343
|
+
def help_text
|
344
|
+
HardCoded.help.clone.substitute!(:program_name => program_name)
|
345
|
+
end
|
346
|
+
|
347
|
+
###### command implementations
|
348
|
+
|
349
|
+
#def explain argv; HardCoded.explain end
|
350
|
+
|
351
|
+
def hosts argv
|
352
|
+
rows = parse_hosts.sort!{|x,y| x.name <=> y.name }
|
353
|
+
Table.new(:ip, :name).render(rows)
|
354
|
+
end
|
355
|
+
|
356
|
+
def confs argv
|
357
|
+
rows = parse_confs.sort!{|x,y| x.server_name <=> y.server_name }
|
358
|
+
Table.new(:server_name,:document_root).render(rows)
|
359
|
+
end
|
360
|
+
|
361
|
+
def dump argv
|
362
|
+
parse = parse_dump_vhosts
|
363
|
+
puts Table.new(:server_name, :port, :conf_path).
|
364
|
+
render(parse.entries)
|
365
|
+
puts "\n"
|
366
|
+
puts Table.new(:missing_docroot).
|
367
|
+
render(parse.missing_docroots)
|
368
|
+
''
|
369
|
+
end
|
370
|
+
|
371
|
+
def list argv
|
372
|
+
infos = master.values.sort!{|x,y| x.host <=> y.host}
|
373
|
+
Table.new(
|
374
|
+
:host, :host_ok, :ip, :port, :docroot, :docroot_ok, :conf
|
375
|
+
).render(infos)
|
376
|
+
end
|
377
|
+
|
378
|
+
def add argv
|
379
|
+
opts = OpenStruct.new(
|
380
|
+
:dry_run => false,
|
381
|
+
:docroots => @path.docroots
|
382
|
+
)
|
383
|
+
parser = OptionParser.new do |o|
|
384
|
+
o.on('-n','--dry-run'){ opts.dry_run = true; }
|
385
|
+
o.on('-d','--docroots PATH') do |v|
|
386
|
+
@path.docroots = File.expand_path(v)
|
387
|
+
end
|
388
|
+
o.on('-h','--help') do
|
389
|
+
return "Usage: #{program_name} #{Cli.add_syntax}"
|
390
|
+
end
|
391
|
+
end
|
392
|
+
begin
|
393
|
+
parser.parse! argv
|
394
|
+
rescue OptionParser::ParseError => e
|
395
|
+
puts e.message
|
396
|
+
puts "Usage: #{program_name} #{Cli.add_syntax}"
|
397
|
+
return ''
|
398
|
+
end
|
399
|
+
errs = [];
|
400
|
+
if (argv.length == 0)
|
401
|
+
errs << "too few argments. expecting one."
|
402
|
+
elsif (argv.length > 1)
|
403
|
+
errs << "too many arguments. expecting one."
|
404
|
+
elsif (! (argv[0].to_s =~ EtcHostsProxy::OkNameRe))
|
405
|
+
errs << "invalid name for a domain(?): #{argv[0].inspect}"
|
406
|
+
end
|
407
|
+
if errs.length > 0
|
408
|
+
return ((errs * "\n") << "\nUsage: #{Cli.add_syntax}")
|
409
|
+
end
|
410
|
+
new_hostname = argv[0]
|
411
|
+
_add new_hostname, opts
|
412
|
+
end
|
413
|
+
|
414
|
+
###### top secret implementationz
|
415
|
+
|
416
|
+
def self.add_syntax
|
417
|
+
"add [-n|--dry-run] [-d|--docroots <path>] <host>"
|
418
|
+
end
|
419
|
+
|
420
|
+
def _add host, opts
|
421
|
+
item = master[host] # might be empty object
|
422
|
+
units_of_work.clear
|
423
|
+
go_etc_hosts item, host
|
424
|
+
go_config item, host
|
425
|
+
go_html item, host
|
426
|
+
num = run_units_of_work opts.dry_run
|
427
|
+
if (num==0)
|
428
|
+
puts "nothing to be done for #{host}."
|
429
|
+
else
|
430
|
+
puts "if this wasn't a dry run, you could restart apache with:"
|
431
|
+
puts " #{@path.apachectl} graceful"
|
432
|
+
puts "and go to:"
|
433
|
+
puts " http://#{host}/index.html"
|
434
|
+
end
|
435
|
+
'done.'
|
436
|
+
end
|
437
|
+
|
438
|
+
def go_etc_hosts item, host
|
439
|
+
if 'ok'==item.host_ok
|
440
|
+
add_unit_of_work("#{@path.etc_hosts} ok for #{host}")
|
441
|
+
else
|
442
|
+
hosts = EtcHostsProxy.new(@path.etc_hosts).writable!
|
443
|
+
add_unit_of_work("add line to #{@path.etc_hosts}:") do |unit|
|
444
|
+
hosts.dry_run = unit.dry_run?
|
445
|
+
hosts.add! host
|
446
|
+
unit.report.concat hosts.report
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
def go_config item, host
|
452
|
+
if item.conf
|
453
|
+
add_unit_of_work("config ok for #{host}")
|
454
|
+
else
|
455
|
+
doc_root = File.join(@path.docroots, FileyCoyote.fileize(host))
|
456
|
+
confs = VhostFileMakerPro.new(apache_conf_proxy.sites_enabled_path)
|
457
|
+
add_unit_of_work("add config file for #{host}:") do |unit|
|
458
|
+
confs.dry_run = unit.dry_run?
|
459
|
+
confs.add! host, doc_root
|
460
|
+
unit.report.concat confs.report
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
def go_html item, host
|
466
|
+
if 'ok'==item.docroot_ok
|
467
|
+
add_unit_of_work("#{@path.docroots} ok for #{host}")
|
468
|
+
else
|
469
|
+
docroot = DocRootProxy.new(@path.docroots).writable!
|
470
|
+
add_unit_of_work("add stub website to #{@path.docroots}:")do |unit|
|
471
|
+
docroot.dry_run = unit.dry_run?
|
472
|
+
docroot.make_stub_site(host)
|
473
|
+
unit.report.concat docroot.report
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
def parse_hosts
|
479
|
+
EtcHostsProxy.new(@path.etc_hosts).entries
|
480
|
+
end
|
481
|
+
|
482
|
+
def parse_confs
|
483
|
+
conf = apache_conf_proxy
|
484
|
+
confs = conf.sites_enabled_confs
|
485
|
+
confs.delete_if{|x| x.document_root == '' }
|
486
|
+
confs.sort!{|x,y| x.server_name <=> y.server_name}
|
487
|
+
end
|
488
|
+
|
489
|
+
def apache_conf_proxy
|
490
|
+
@apache_conf_proxy ||= ApacheConfProxy.new @path.httpd_conf
|
491
|
+
end
|
492
|
+
|
493
|
+
def parse_dump_vhosts
|
494
|
+
cmd = "#{@path.apachectl} -t -D DUMP_VHOSTS"
|
495
|
+
out,err = sistum2(cmd)
|
496
|
+
lines = out.split("\n");
|
497
|
+
lines.concat err.split("\n");
|
498
|
+
# on ubuntu, we get 'Syntax OK' in out, and details in err
|
499
|
+
# on osx, we get empty out and everything in err
|
500
|
+
p = DumpParse.new lines.join("\n") # @todo plan on having this crap out a lot
|
501
|
+
p
|
502
|
+
end
|
503
|
+
|
504
|
+
def get_orientated_with_your_environment
|
505
|
+
list = HardCoded.execs
|
506
|
+
infos = list.map do |executable|
|
507
|
+
OpenStruct.new(
|
508
|
+
'which' => sistum("which #{executable}")
|
509
|
+
)
|
510
|
+
end
|
511
|
+
infos.delete_if{|x| ""==x.which}
|
512
|
+
case infos.length
|
513
|
+
when 0:
|
514
|
+
raise Fail.new("#{sistum('whoami')} has neither "<<
|
515
|
+
(list * ' nor ') << "within his or her executable path."<<
|
516
|
+
" I can be of no help to you."
|
517
|
+
)
|
518
|
+
when 1: # fallthru
|
519
|
+
else
|
520
|
+
raise Fail.new("#{sistum('whoami')} has all of these in"<<
|
521
|
+
" his or her path: "<< (infos.map(&:which) * ' and ')<<'.'<<
|
522
|
+
" As this is a zero-configuration jobbie, I have no idea"<<
|
523
|
+
" what to do."
|
524
|
+
)
|
525
|
+
end
|
526
|
+
@path[:apachectl] = infos[0].which
|
527
|
+
end
|
528
|
+
|
529
|
+
def master
|
530
|
+
hosts = parse_hosts
|
531
|
+
confs = parse_confs
|
532
|
+
dumps = parse_dump_vhosts # entries, missing_doctroots
|
533
|
+
missings = Hash[*
|
534
|
+
dumps.missing_docroots.each.map{|x| [x.missing_docroot, 1]}.flatten
|
535
|
+
]
|
536
|
+
master = Hash.new(){|h,k| h[k] = OpenStruct.new(:host=>k) }
|
537
|
+
hosts.each do |host|
|
538
|
+
item = master[host.name]
|
539
|
+
item.ip = host.ip
|
540
|
+
item.host_ok = 'ok'
|
541
|
+
end
|
542
|
+
confs.each do |conf|
|
543
|
+
item = master[conf.server_name]
|
544
|
+
item.docroot = conf.document_root
|
545
|
+
item.docroot_ok = (missings[conf.document_root]) ? 'missing' : 'ok'
|
546
|
+
end
|
547
|
+
dumps.entries.each do |dump|
|
548
|
+
item = master[dump.server_name]
|
549
|
+
item.conf = dump.conf_path
|
550
|
+
item.port = dump.port
|
551
|
+
end
|
552
|
+
master
|
553
|
+
end
|
554
|
+
end # Cli
|
555
|
+
|
556
|
+
class DumpParse
|
557
|
+
attr_reader :missing_docroots, :entries
|
558
|
+
def initialize str
|
559
|
+
lines = str.split("\n")
|
560
|
+
@missing_docroots = []
|
561
|
+
infos = []
|
562
|
+
lines.each do |line|
|
563
|
+
case line
|
564
|
+
when /^Warning: DocumentRoot \[(.+)\] does not exist$/:
|
565
|
+
@missing_docroots.push OpenStruct.new(:missing_docroot => $1)
|
566
|
+
when /^.*is a NameVirtualHost$/: #skip
|
567
|
+
when /^wildcard NameVirtualHosts and _default_ servers: *$/: #skip
|
568
|
+
when "VirtualHost configuration:": # skip
|
569
|
+
when /^ {9}default server (.+) \((.+)\:\d+\)$/:
|
570
|
+
infos.push OpenStruct.new(
|
571
|
+
:server_name => $1, :conf_path => $2, :port => nil
|
572
|
+
)
|
573
|
+
when /^ {9}port ([^ ]+) namevhost (.+) \((.+)\:\d+\)$/:
|
574
|
+
infos.push OpenStruct.new(
|
575
|
+
:server_name => $2, :conf_path => $3, :port => $1
|
576
|
+
)
|
577
|
+
when /^Syntax OK$/: #skip for now end
|
578
|
+
when /^httpd: Could not reliably determine/: #skip
|
579
|
+
when /^\*:(\d{4}) +([^ ]+) +\((.+)\:\d+\)$/
|
580
|
+
infos.push OpenStruct.new(
|
581
|
+
:server_name => $2, :conf_path => $3, :port => $1
|
582
|
+
)
|
583
|
+
else
|
584
|
+
raise Fail.new("we suck: parse fail:\n#{line.inspect}")
|
585
|
+
end
|
586
|
+
end
|
587
|
+
@entries = infos
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
class ApacheConfProxy
|
592
|
+
include DryRunney
|
593
|
+
SitesEnabledRe = %r|^ *Include +([^\n]*sites-enabled/?) *$|m
|
594
|
+
# code for pasring the file itself left behind in bf91c
|
595
|
+
def initialize path
|
596
|
+
@path = path
|
597
|
+
if !File.readable?(@path)
|
598
|
+
raise UserFail.new("no such apache conf file or not readable: #{@path}")
|
599
|
+
end
|
600
|
+
@contents = File.read path
|
601
|
+
end
|
602
|
+
|
603
|
+
# this project it is hard coded to expect that there is
|
604
|
+
# only one such entry in the file
|
605
|
+
def sites_enabled_path
|
606
|
+
arr = @contents.scan(SitesEnabledRe).map{|x| x[0]}
|
607
|
+
case arr.length
|
608
|
+
when 0:
|
609
|
+
`sudo bash -c 'sudo echo "Include /private/etc/apache2/sites-enabled/*.conf" >> /etc/apache2/httpd.conf'`
|
610
|
+
`sudo bash -c 'sudo mkdir /private/etc/apache2/sites-enabled '`
|
611
|
+
return '/private/etc/apache2/sites-enabled/'
|
612
|
+
when 1: arr[0]
|
613
|
+
else arr
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
def sites_enabled_confs
|
618
|
+
path1 = sites_enabled_path
|
619
|
+
paths = Dir[File.join(path1,'*.conf')]
|
620
|
+
paths.map do |path|
|
621
|
+
VhostConfProxy.new(path)
|
622
|
+
end
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
class VhostFileMakerPro
|
627
|
+
include DryRunney
|
628
|
+
def initialize path
|
629
|
+
unless File.directory? path
|
630
|
+
raise EnvFail.new "it's not exist: #{path}"
|
631
|
+
end
|
632
|
+
unless File.writable? path
|
633
|
+
raise PermissionFail.new "not writable: #{path}"
|
634
|
+
end
|
635
|
+
@path = path
|
636
|
+
end
|
637
|
+
def add! host, doc_root
|
638
|
+
path = File.join(@path, "#{host}.conf")
|
639
|
+
if File.exist? path
|
640
|
+
report << "File already existed: #{path}"
|
641
|
+
else
|
642
|
+
contents = HardCoded.vhost_stuff.clone
|
643
|
+
contents.doc_root = doc_root
|
644
|
+
contents.host = host
|
645
|
+
unless dry_run?
|
646
|
+
File.open(path,'a+'){|fh|
|
647
|
+
fh.write contents
|
648
|
+
}
|
649
|
+
end
|
650
|
+
report << "write #{path}"
|
651
|
+
end
|
652
|
+
nil
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
656
|
+
class VhostConfProxy < ApacheConfProxy
|
657
|
+
def initialize *args
|
658
|
+
super(*args)
|
659
|
+
@cache = {}
|
660
|
+
end
|
661
|
+
def document_root
|
662
|
+
_get('DocumentRoot')
|
663
|
+
end
|
664
|
+
def server_name
|
665
|
+
_get('ServerName')
|
666
|
+
end
|
667
|
+
def _get thing
|
668
|
+
@cache[thing] ||= begin
|
669
|
+
re = Regexp.new('^ *'<<Regexp.escape(thing)<<
|
670
|
+
' *"?([^"]+)"? *$')
|
671
|
+
matches = @contents.scan(re)
|
672
|
+
case matches.length
|
673
|
+
when 0: ""
|
674
|
+
when 1: matches[0][0]
|
675
|
+
else matches.map{|x| x[0]}.join(',')
|
676
|
+
end
|
677
|
+
end
|
678
|
+
end
|
679
|
+
end
|
680
|
+
|
681
|
+
class DocRootProxy
|
682
|
+
include FileyCoyote, DryRunney
|
683
|
+
def initialize path
|
684
|
+
@path = path
|
685
|
+
end
|
686
|
+
def writable!
|
687
|
+
unless File.directory? @path
|
688
|
+
raise EnvFail.new(
|
689
|
+
"docroots directory (the directory to hold your docroot "<<
|
690
|
+
"directory) must exist and be directory: #{@path}")
|
691
|
+
end
|
692
|
+
FileyCoyote.writable! @path
|
693
|
+
self
|
694
|
+
end
|
695
|
+
def make_stub_site host
|
696
|
+
@host = host
|
697
|
+
if (doc_root_exists?)
|
698
|
+
raise Fail.new("already exists: #{doc_root}")
|
699
|
+
end
|
700
|
+
report << "mkdir -m 755 #{doc_root}"
|
701
|
+
FileUtils.mkdir(doc_root,:mode=>0755, :verbose=>false,:noop=>dry_run?)
|
702
|
+
index_html = File.join(doc_root,"index.html")
|
703
|
+
my_new_homepage = HardCoded.web_page.clone
|
704
|
+
my_new_homepage.title = host.gsub(/_|-/,' ')
|
705
|
+
unless dry_run?
|
706
|
+
File.open(index_html,'a+'){|fh| fh.write my_new_homepage }
|
707
|
+
end
|
708
|
+
report << "write #{index_html}"
|
709
|
+
nil
|
710
|
+
end
|
711
|
+
protected
|
712
|
+
def doc_root
|
713
|
+
doc_root = File.join(@path, fileize(@host))
|
714
|
+
end
|
715
|
+
def doc_root_exists?
|
716
|
+
File.exist?(doc_root)
|
717
|
+
end
|
718
|
+
end
|
719
|
+
|
720
|
+
class EtcHostsProxy
|
721
|
+
include DryRunney
|
722
|
+
OkNameRe = /^[-a-z_0-9\.]{1,}$/
|
723
|
+
include FileyCoyote, DryRunney
|
724
|
+
def initialize path
|
725
|
+
raise Fail.new("no: #{path}") unless File.exist?(path)
|
726
|
+
@path = path
|
727
|
+
end
|
728
|
+
def entries
|
729
|
+
File.read(@path).scan(
|
730
|
+
%r{(127\.0\.0\.1)\s+([[:print:]]+)[\s\t]*$}
|
731
|
+
).map do |match|
|
732
|
+
OpenStruct.new(:ip => match[0], :name => match[1])
|
733
|
+
end
|
734
|
+
end
|
735
|
+
def add! name
|
736
|
+
writable!
|
737
|
+
raise UserFail.new("i don't like your name: #{name.inspect}") unless
|
738
|
+
(name =~ OkNameRe)
|
739
|
+
info = OpenStruct.new
|
740
|
+
info.backed_up = back_that_asset_up! @path
|
741
|
+
report.push "make backup: #{info.backed_up}"
|
742
|
+
File.open(@path,'a+') do |fh|
|
743
|
+
fh.seek(0,IO::SEEK_END)
|
744
|
+
fh.puts %{127.0.0.1 #{name}}
|
745
|
+
info.hostname = name
|
746
|
+
info.ip = '127.0.0.1'
|
747
|
+
end unless dry_run?
|
748
|
+
report.push %{add "#{name}" to #{@path}}
|
749
|
+
info
|
750
|
+
end
|
751
|
+
end
|
752
|
+
|
753
|
+
|
754
|
+
HardCoded.help = Templatesimal[<<-END.gsub(/^ {6}/,'')
|
755
|
+
do some stuff with your apache server virtualhosts.
|
756
|
+
|
757
|
+
Usage: {{program_name}} COMMAND [ARGUMENTS]
|
758
|
+
|
759
|
+
Commands:
|
760
|
+
help show this screen
|
761
|
+
explain explains the steps involved in adding a virtualhost
|
762
|
+
-- you should understand this before using this script
|
763
|
+
hosts just shows the parsed version of the /etc/hosts file
|
764
|
+
confs just list the known vhosts per the sites-enabled dir.
|
765
|
+
dump a tableized version of `apachectl -t -D DUMP_VHOSTS`
|
766
|
+
list report everything you know about anything
|
767
|
+
#{Cli.add_syntax}
|
768
|
+
add stuff to all 3 places if necessary
|
769
|
+
END
|
770
|
+
]
|
771
|
+
|
772
|
+
HardCoded.explain = Templatesimal[<<-END.gsub(/^ {6}/,'')
|
773
|
+
HOW TO ADD A VIRTUALHOST (the really short version):
|
774
|
+
1) `locate httpd.conf` # then jump to the relevant lines at the end
|
775
|
+
2) copy paste a new virtualhost section into the file -or-
|
776
|
+
add such vhost config file into a folder that gets Include'd
|
777
|
+
3) `locate /etc/hosts` # find the right file, it's usually this one
|
778
|
+
4) copy paste your line
|
779
|
+
5) apache(2?)ctl graceful
|
780
|
+
END
|
781
|
+
]
|
782
|
+
|
783
|
+
HardCoded.web_page = Templatesimal[<<-END.gsub(/^ {6}/,'')
|
784
|
+
<!DOCTYPE html><html><head><title>{{title}}</title>
|
785
|
+
<style type="text/css">
|
786
|
+
body {font-family: sans-serif; background-color: #6cc5c3; }
|
787
|
+
h1,h2,h3,h4,h5 {
|
788
|
+
text-align: center; background-color: #f05921; color: #ffffff;
|
789
|
+
-webkit-border-radius: 8px; /* for Safari */
|
790
|
+
-moz-border-radius: 8px; /* for Firefox */
|
791
|
+
padding: .23em;
|
792
|
+
text-shadow:0 0 10px #000000;
|
793
|
+
}
|
794
|
+
</style>
|
795
|
+
</head><body>
|
796
|
+
<h1>welcome to {{title}}</h1>
|
797
|
+
</body></html>
|
798
|
+
END
|
799
|
+
]
|
800
|
+
|
801
|
+
HardCoded.vhost_stuff = Templatesimal[<<-END.gsub(/^ {6}/,'')
|
802
|
+
<VirtualHost *:80>
|
803
|
+
DocumentRoot "{{doc_root}}"
|
804
|
+
ServerName "{{host}}"
|
805
|
+
<Directory "{{doc_root}}">
|
806
|
+
AllowOverride All
|
807
|
+
Allow from All
|
808
|
+
</Directory>
|
809
|
+
</VirtualHost>
|
810
|
+
END
|
811
|
+
]
|
812
|
+
|
813
|
+
end # Vhost
|
814
|
+
end # Hipe
|
815
|
+
|
816
|
+
if File.basename($PROGRAM_NAME) == File.basename(__FILE__)
|
817
|
+
puts Hipe::Vhost::Cli.new.run(ARGV)
|
818
|
+
end
|
data/test/helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: blavoshost
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 31
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 0.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Jerrod Blavos
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-10-25 00:00:00 -04:00
|
19
|
+
default_executable: blavoshost
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: A tool to add local vhosts ot apache/OSX
|
23
|
+
email: jerrod@indierockmedia.com
|
24
|
+
executables:
|
25
|
+
- blavoshost
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- LICENSE
|
30
|
+
- README.rdoc
|
31
|
+
files:
|
32
|
+
- .document
|
33
|
+
- .gitignore
|
34
|
+
- LICENSE
|
35
|
+
- README.rdoc
|
36
|
+
- Rakefile
|
37
|
+
- VERSION
|
38
|
+
- bin/blavoshost
|
39
|
+
- test/helper.rb
|
40
|
+
- test/test_blavoshost.rb
|
41
|
+
has_rdoc: true
|
42
|
+
homepage: http://github.com/jerrod/blavoshost
|
43
|
+
licenses: []
|
44
|
+
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options:
|
47
|
+
- --charset=UTF-8
|
48
|
+
require_paths:
|
49
|
+
- bin
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
hash: 3
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
hash: 3
|
65
|
+
segments:
|
66
|
+
- 0
|
67
|
+
version: "0"
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.3.7
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: Add local vhosts
|
75
|
+
test_files:
|
76
|
+
- test/helper.rb
|
77
|
+
- test/test_blavoshost.rb
|