bandshell 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/bandshell-control.rb +11 -0
- data/bin/bandshell-daemon.rb +12 -0
- data/bin/concerto_configserver +10 -0
- data/bin/concerto_netsetup +6 -0
- data/lib/config_store.rb +48 -0
- data/lib/live_image.rb +27 -0
- data/lib/netconfig.rb +542 -0
- data/web/app.rb +320 -0
- data/web/config.ru +2 -0
- data/web/public/network.js +27 -0
- data/web/public/problem.js +7 -0
- data/web/public/stylesheet.css +3 -0
- data/web/public/trollface.png +0 -0
- data/web/views/main.haml +6 -0
- data/web/views/netsettings.haml +64 -0
- data/web/views/password.haml +9 -0
- data/web/views/player_status.haml +8 -0
- data/web/views/problem.haml +15 -0
- data/web/views/setup.haml +25 -0
- metadata +145 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#Requires "daemons" gem
|
3
|
+
|
4
|
+
#Starts the Web App Server & verifies that it is still alive
|
5
|
+
#Starts the Browser & verifies that it is still alive
|
6
|
+
#Periodically uses local library methods to check for updates from the server
|
7
|
+
#Controls screen according to rules downloaded from server
|
8
|
+
|
9
|
+
#Start the daemon loop - everything happens in there
|
10
|
+
loop do
|
11
|
+
sleep(5)
|
12
|
+
end
|
data/lib/config_store.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'live_image'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
# A key/value store for strings.
|
5
|
+
# Right now implemented as disk files.
|
6
|
+
module ConcertoConfig
|
7
|
+
module ConfigStore
|
8
|
+
@@path = nil
|
9
|
+
|
10
|
+
def self.read_config(name, default='')
|
11
|
+
initialize_path if not @@path
|
12
|
+
file = File.join(@@path, name)
|
13
|
+
rofile = File.join(@@ropath, name)
|
14
|
+
|
15
|
+
# Check the read/write config location first. If nothing there,
|
16
|
+
# check the read-only location. If nothing is there, return default.
|
17
|
+
# This way writes can be made at runtime on read-only media while
|
18
|
+
# still allowing some settings to be "baked into" the media.
|
19
|
+
if File.exist?(file)
|
20
|
+
IO.read(file)
|
21
|
+
elsif File.exist?(rofile)
|
22
|
+
IO.read(rofile)
|
23
|
+
else
|
24
|
+
default
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Write a config to the read/write configuration location.
|
29
|
+
def self.write_config(name, value)
|
30
|
+
initialize_path if not @@path
|
31
|
+
file = File.join(@@path, name)
|
32
|
+
|
33
|
+
File.open(file, 'w') do |f|
|
34
|
+
f.write value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.initialize_path
|
39
|
+
@@ropath = File.join(LiveImage.mountpoint, 'concerto', 'config')
|
40
|
+
if LiveImage.readonly?
|
41
|
+
@@path = '/tmp/concerto/config'
|
42
|
+
else
|
43
|
+
@@path = @@ropath
|
44
|
+
end
|
45
|
+
FileUtils.mkdir_p @@path
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/live_image.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
# Functions for dealing with the live image
|
4
|
+
# (where it's mounted, if it's read-only, etc)
|
5
|
+
module ConcertoConfig
|
6
|
+
module LiveImage
|
7
|
+
def self.mountpoint
|
8
|
+
'/live/image'
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.readonly?
|
12
|
+
# on a readonly file system this will fail
|
13
|
+
if not File.exist? self.mountpoint
|
14
|
+
true
|
15
|
+
else
|
16
|
+
begin
|
17
|
+
f = Tempfile.new('test', self.mountpoint)
|
18
|
+
f.close!
|
19
|
+
false
|
20
|
+
rescue
|
21
|
+
# if the tempfile creation bombs we assume readonly
|
22
|
+
true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/netconfig.rb
ADDED
@@ -0,0 +1,542 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'json'
|
5
|
+
require 'ipaddress'
|
6
|
+
require 'config_store'
|
7
|
+
|
8
|
+
# The big idea here is that we have connection methods (layer 2)
|
9
|
+
# and addressing methods (layer 3) and by combining that configuration
|
10
|
+
# information we end up with a complete network configuration.
|
11
|
+
#
|
12
|
+
# Each general layer-2 and layer-3 connection method is represented
|
13
|
+
# as a class; adding the specific details creates an instance of the class.
|
14
|
+
# The configuration is serialized as the name of the class plus the details
|
15
|
+
# needed to reconstruct the instance.
|
16
|
+
#
|
17
|
+
# Each instance can contribute lines to /etc/network/interfaces,
|
18
|
+
# the Debian standard network configuration file.
|
19
|
+
# Each instance also has the opportunity to write out other configuration
|
20
|
+
# files such as wpa_supplicant.conf, resolv.conf etc.
|
21
|
+
|
22
|
+
class Module
|
23
|
+
# Get the name of a class/module and strip off any leading modules.
|
24
|
+
# This is useful in determining arguments for Module#const_get.
|
25
|
+
def basename
|
26
|
+
name.gsub(/^.*::/, '')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module ConcertoConfig
|
31
|
+
# The Debian interfaces configuration file we are going to write out.
|
32
|
+
INTERFACES_FILE='/etc/network/interfaces'
|
33
|
+
|
34
|
+
# Some useful interface operations.
|
35
|
+
class Interface
|
36
|
+
# Wrap an interface name (eth0, wlan0 etc) with some useful operations.
|
37
|
+
def initialize(name)
|
38
|
+
@name = name
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get the name of the interface as a string.
|
42
|
+
attr_reader :name
|
43
|
+
|
44
|
+
# Get the (first) IPv4 address assigned to the interface.
|
45
|
+
# Return "0.0.0.0" if we don't have any v4 addresses.
|
46
|
+
def ip
|
47
|
+
if ifconfig =~ /inet addr:([0-9.]+)/
|
48
|
+
$1
|
49
|
+
else
|
50
|
+
"0.0.0.0"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Get the physical (mac, ethernet) address of the interface.
|
55
|
+
def mac
|
56
|
+
File.open("/sys/class/net/#{@name}/address") do |f|
|
57
|
+
f.read.chomp
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def up
|
62
|
+
system("/sbin/ifconfig #{@name} up")
|
63
|
+
end
|
64
|
+
|
65
|
+
def down
|
66
|
+
system("/sbin/ifconfig #{@name} down")
|
67
|
+
end
|
68
|
+
|
69
|
+
def ifup
|
70
|
+
system("/sbin/ifup #{@name}")
|
71
|
+
end
|
72
|
+
|
73
|
+
def ifdown
|
74
|
+
system("/sbin/ifdown #{@name}")
|
75
|
+
end
|
76
|
+
|
77
|
+
def up?
|
78
|
+
if ifconfig =~ /UP/
|
79
|
+
true
|
80
|
+
else
|
81
|
+
false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def medium_present?
|
86
|
+
brought_up = false
|
87
|
+
result = false
|
88
|
+
|
89
|
+
if not up?
|
90
|
+
brought_up = true
|
91
|
+
up
|
92
|
+
sleep 10
|
93
|
+
end
|
94
|
+
|
95
|
+
if ifconfig =~ /RUNNING/
|
96
|
+
result = true
|
97
|
+
end
|
98
|
+
|
99
|
+
if brought_up
|
100
|
+
down
|
101
|
+
end
|
102
|
+
|
103
|
+
result
|
104
|
+
end
|
105
|
+
private
|
106
|
+
def ifconfig
|
107
|
+
`/sbin/ifconfig #{@name}`
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# (Instance) methods that must be defined by all connection and
|
112
|
+
# addressing method classes
|
113
|
+
#
|
114
|
+
# creation and serialization:
|
115
|
+
#
|
116
|
+
# initialize(args={}): Create a new instance. When unserializing the args
|
117
|
+
# hash created during serialization is passed in.
|
118
|
+
#
|
119
|
+
# args: return a hash of data needed to reconstruct the instance. This
|
120
|
+
# hash will be passed to initialize() when unserializing.
|
121
|
+
#
|
122
|
+
# OS-level configuration:
|
123
|
+
#
|
124
|
+
# write_configs: Write out any additional configuration files needed
|
125
|
+
# (e.g. resolv.conf, wpa_supplicant.conf, ...)
|
126
|
+
#
|
127
|
+
# config_interface_name (only for connection methods): return the name of
|
128
|
+
# the physical interface to be used for the connection
|
129
|
+
#
|
130
|
+
# addressing_type (only for addressing methods): return the name of the
|
131
|
+
# addressing method (dhcp, static, manual...) to be used in the Debian
|
132
|
+
# network configuration file /etc/network/interfaces.
|
133
|
+
#
|
134
|
+
# interfaces_lines: an array of strings representing lines to be added
|
135
|
+
# to the /etc/network/interfaces file after the line naming the interface.
|
136
|
+
#
|
137
|
+
# Stuff for the Web interface:
|
138
|
+
#
|
139
|
+
# safe_assign: return an array of symbols representing fields the user
|
140
|
+
# should be allowed to modify.
|
141
|
+
#
|
142
|
+
# validate: check that the internal configuration is at least somewhat
|
143
|
+
# consistent and stands a chance of working; throw exception if not
|
144
|
+
# (FIXME! need a better error handling mechanism)
|
145
|
+
#
|
146
|
+
# and attr_accessors for everything the web interface
|
147
|
+
# should be able to assign to.
|
148
|
+
#
|
149
|
+
# Everyone must define a class method self.description as well.
|
150
|
+
# This returns a string that is displayed in the web interface dropdowns
|
151
|
+
# because using plain class identifiers there doesn't look good.
|
152
|
+
# This should be something short like "Wired Connection".
|
153
|
+
|
154
|
+
# Layer 2 connection via wired media.
|
155
|
+
# We will look for wired interfaces that have media connected,
|
156
|
+
# or use an interface chosen by the user via the args. There's
|
157
|
+
# nothing extra to be contributed to the interfaces file besides
|
158
|
+
# the name of the interface to be used.
|
159
|
+
class WiredConnection
|
160
|
+
def initialize(args={})
|
161
|
+
if args['interface_name']
|
162
|
+
@interface_name = args['interface_name']
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def write_configs
|
167
|
+
# We don't need any.
|
168
|
+
end
|
169
|
+
|
170
|
+
def config_interface_name
|
171
|
+
if @interface_name && @interface_name.length > 0
|
172
|
+
# the user has specified an interface to use
|
173
|
+
@interface_name
|
174
|
+
else
|
175
|
+
# scan for the first wired interface that has media
|
176
|
+
scan_interfaces
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# If interface_name is something other than nil or the empty string,
|
181
|
+
# we will override the automatic detection and use that interface.
|
182
|
+
attr_accessor :interface_name
|
183
|
+
|
184
|
+
def safe_assign
|
185
|
+
[ :interface_name ]
|
186
|
+
end
|
187
|
+
|
188
|
+
def args
|
189
|
+
{
|
190
|
+
'interface_name' => @interface_name
|
191
|
+
}
|
192
|
+
end
|
193
|
+
|
194
|
+
def interfaces_lines
|
195
|
+
[]
|
196
|
+
end
|
197
|
+
|
198
|
+
def validate
|
199
|
+
if @interface_name != ''
|
200
|
+
if self.class.interfaces.find {
|
201
|
+
|iface| iface.name == @interface_name
|
202
|
+
}.nil?
|
203
|
+
fail "The interface doesn't exist on the system"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Try to find all wired interfaces on the system.
|
209
|
+
def self.interfaces
|
210
|
+
# This is somewhat Linux specific and may miss some oddballs.
|
211
|
+
devices = Dir.glob('/sys/class/net/eth*')
|
212
|
+
devices.map { |d| Interface.new(File.basename(d)) }
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.description
|
216
|
+
"Wired connection"
|
217
|
+
end
|
218
|
+
|
219
|
+
private
|
220
|
+
# Find the first wired interface with medium present. If none
|
221
|
+
# is found default to eth0.
|
222
|
+
def scan_interfaces
|
223
|
+
first_with_medium = self.class.interfaces.find {
|
224
|
+
|iface| iface.medium_present?
|
225
|
+
}
|
226
|
+
|
227
|
+
if first_with_medium
|
228
|
+
first_with_medium.name
|
229
|
+
else
|
230
|
+
# if we get here no interface was found with a cable attached
|
231
|
+
# default to eth0 and hope for the best
|
232
|
+
STDERR.puts "warning: no suitable interface found, using eth0"
|
233
|
+
'eth0'
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# 802.11* unencrypted wireless connections.
|
239
|
+
# These are managed by wpa_supplicant on Debian so we need to create its
|
240
|
+
# configuration file and link it to the interfaces file.
|
241
|
+
class WirelessConnection
|
242
|
+
def initialize(args={})
|
243
|
+
@ssid = args['ssid'] || ''
|
244
|
+
@interface_name = args['interface_name'] if args['interface_name']
|
245
|
+
@wpa_config_file = '/tmp/wpa_supplicant.concerto.conf'
|
246
|
+
end
|
247
|
+
|
248
|
+
attr_accessor :ssid, :interface_name
|
249
|
+
|
250
|
+
def config_interface_name
|
251
|
+
# If the user has requested a specific interface, use it.
|
252
|
+
# Otherwise, just pick the first wlan interface, assuming
|
253
|
+
# it works and all wlan interfaces have approximately equal
|
254
|
+
# reception. When this assumption is wrong the user must force.
|
255
|
+
if @interface_name && @interface_name != ''
|
256
|
+
@interface_name
|
257
|
+
else
|
258
|
+
self.class.interfaces[0].name
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def validate
|
263
|
+
if @ssid == ''
|
264
|
+
fail "Need SSID for wireless connection"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def safe_assign
|
269
|
+
[ :ssid, :interface_name ]
|
270
|
+
end
|
271
|
+
|
272
|
+
def write_configs
|
273
|
+
# Write a wpa_supplicant.conf file for an unsecured network.
|
274
|
+
File.open(@wpa_config_file, 'w') do |wpaconf|
|
275
|
+
# long lines, sorry!
|
276
|
+
wpaconf.puts "ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev"
|
277
|
+
wpaconf.puts "network={"
|
278
|
+
wpaconf.puts "ssid=\"#{@ssid}\""
|
279
|
+
wpaconf.puts "scan_ssid=1"
|
280
|
+
wpaconf.puts "key_mgmt=NONE"
|
281
|
+
wpaconf.puts "}"
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def interfaces_lines
|
286
|
+
# This links the wpa config to the interfaces file.
|
287
|
+
["wpa-conf #{@wpa_config_file}"]
|
288
|
+
end
|
289
|
+
|
290
|
+
def args
|
291
|
+
{
|
292
|
+
'interface_name' => @interface_name,
|
293
|
+
'ssid' => @ssid
|
294
|
+
}
|
295
|
+
end
|
296
|
+
|
297
|
+
def self.description
|
298
|
+
"Wireless connection (no encryption)"
|
299
|
+
end
|
300
|
+
|
301
|
+
def self.interfaces
|
302
|
+
# Again this is not guaranteed to be a catch all.
|
303
|
+
devices = Dir.glob('/sys/class/net/{ath,wlan}*')
|
304
|
+
devices.map { |d| Interface.new(File.basename(d)) }
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Static IPv4 addressing.
|
309
|
+
# We use the IPAddress gem to validate that the address information
|
310
|
+
# is vaguely correct (weeding out errors like the gateway
|
311
|
+
# being on another subnet)
|
312
|
+
class StaticAddressing
|
313
|
+
def initialize(args={})
|
314
|
+
@nameservers = args['nameservers']
|
315
|
+
@address = args['address']
|
316
|
+
@netmask = args['netmask']
|
317
|
+
@gateway = args['gateway']
|
318
|
+
end
|
319
|
+
|
320
|
+
def addressing_type
|
321
|
+
'static'
|
322
|
+
end
|
323
|
+
|
324
|
+
def args
|
325
|
+
{
|
326
|
+
'address' => @address,
|
327
|
+
'netmask' => @netmask,
|
328
|
+
'gateway' => @gateway,
|
329
|
+
'nameservers' => @nameservers
|
330
|
+
}
|
331
|
+
end
|
332
|
+
|
333
|
+
def interfaces_lines
|
334
|
+
[
|
335
|
+
"address #{@address}",
|
336
|
+
"netmask #{@netmask}",
|
337
|
+
"gateway #{@gateway}"
|
338
|
+
]
|
339
|
+
end
|
340
|
+
|
341
|
+
|
342
|
+
def write_configs
|
343
|
+
File.open('/etc/resolv.conf','w') do |resolvconf|
|
344
|
+
@nameservers.each do |nameserver|
|
345
|
+
resolvconf.puts("nameserver #{nameserver}");
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def self.description
|
351
|
+
"Static Addressing"
|
352
|
+
end
|
353
|
+
|
354
|
+
attr_accessor :address, :netmask, :gateway, :nameservers
|
355
|
+
|
356
|
+
def safe_assign
|
357
|
+
[ :address, :netmask, :gateway, :nameservers_flat ]
|
358
|
+
end
|
359
|
+
|
360
|
+
def validate
|
361
|
+
@address.strip!
|
362
|
+
@netmask.strip!
|
363
|
+
@gateway.strip!
|
364
|
+
|
365
|
+
if not IPAddress.valid_ipv4?(@address)
|
366
|
+
fail "Static address is invalid"
|
367
|
+
end
|
368
|
+
|
369
|
+
p @netmask
|
370
|
+
if not IPAddress.valid_ipv4_netmask?(@netmask)
|
371
|
+
fail "Static netmask is invalid"
|
372
|
+
end
|
373
|
+
|
374
|
+
p @netmask
|
375
|
+
subnet = IPAddress::IPv4.new(@address)
|
376
|
+
subnet.netmask = @netmask
|
377
|
+
if not subnet.include? IPAddress::IPv4.new(gateway)
|
378
|
+
fail "Gateway provided is unreachable"
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
# These next two methods are for the web interface, where it's
|
383
|
+
# more convenient to enter a bunch of nameservers on one line
|
384
|
+
# than to have to deal with an array of fields.
|
385
|
+
def nameservers_flat=(separated_list)
|
386
|
+
servers = separated_list.strip.split(/\s*[,|:;\s]\s*/)
|
387
|
+
servers.each do |server|
|
388
|
+
server.strip!
|
389
|
+
p server
|
390
|
+
if not IPAddress.valid? server
|
391
|
+
fail "One or more invalid IP addresses in nameserver list"
|
392
|
+
end
|
393
|
+
end
|
394
|
+
@nameservers = servers
|
395
|
+
end
|
396
|
+
|
397
|
+
def nameservers_flat
|
398
|
+
@nameservers.join(',')
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# Dynamic IPv4 addressing via DHCP
|
403
|
+
class DHCPAddressing
|
404
|
+
def initialize(args={})
|
405
|
+
# we accept no args
|
406
|
+
end
|
407
|
+
|
408
|
+
def addressing_type
|
409
|
+
'dhcp'
|
410
|
+
end
|
411
|
+
|
412
|
+
def interfaces_lines
|
413
|
+
# DHCP needs no additional interfaces args
|
414
|
+
# from the addressing side
|
415
|
+
[]
|
416
|
+
end
|
417
|
+
|
418
|
+
def validate
|
419
|
+
# nothing to validate
|
420
|
+
end
|
421
|
+
|
422
|
+
def safe_assign
|
423
|
+
[] # no args
|
424
|
+
end
|
425
|
+
|
426
|
+
def args
|
427
|
+
{ }
|
428
|
+
end
|
429
|
+
|
430
|
+
def write_configs
|
431
|
+
# dhclient will write our resolv.conf so we do not need
|
432
|
+
# to do anything
|
433
|
+
end
|
434
|
+
|
435
|
+
def self.description
|
436
|
+
"Dynamic Addressing - DHCP"
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
# Read a JSON formatted network configuration from the config store.
|
441
|
+
# This instantiates the connection and addressing method classes
|
442
|
+
# and returns the instances i.e. cm, am = read_network_config
|
443
|
+
#
|
444
|
+
# If no configuration is saved or it is corrupt this returns
|
445
|
+
# a default configuration that is somewhat likely to work.
|
446
|
+
def self.read_network_config
|
447
|
+
input = ConfigStore.read_config('network_config', '')
|
448
|
+
|
449
|
+
begin
|
450
|
+
args = JSON.parse(input)
|
451
|
+
rescue
|
452
|
+
# set up some sane defaults if we have no configuration
|
453
|
+
# or it can't be parsed
|
454
|
+
args = {
|
455
|
+
'connection_method' => 'WiredConnection',
|
456
|
+
'addressing_method' => 'DHCPAddressing',
|
457
|
+
'connection_method_args' => { },
|
458
|
+
'addressing_method_args' => { }
|
459
|
+
}
|
460
|
+
end
|
461
|
+
|
462
|
+
connection_method_class = ConcertoConfig.const_get(args['connection_method'])
|
463
|
+
addressing_method_class = ConcertoConfig.const_get(args['addressing_method'])
|
464
|
+
|
465
|
+
connection_method = connection_method_class.new(
|
466
|
+
args['connection_method_args']
|
467
|
+
)
|
468
|
+
|
469
|
+
addressing_method = addressing_method_class.new(
|
470
|
+
args['addressing_method_args']
|
471
|
+
)
|
472
|
+
|
473
|
+
return [connection_method, addressing_method]
|
474
|
+
end
|
475
|
+
|
476
|
+
# Save the network configuration to the configuration store.
|
477
|
+
# Arguments are instances of connection method and addressing
|
478
|
+
# method classes. Throws exception if either one is not valid.
|
479
|
+
def self.write_network_config(cm, am)
|
480
|
+
# Check that everything is consistent. If not, we currently throw
|
481
|
+
# an exception, which probably is not the best long term solution.
|
482
|
+
cm.validate
|
483
|
+
am.validate
|
484
|
+
|
485
|
+
# Serialize our instances as JSON data to be written to the config file.
|
486
|
+
json_data = {
|
487
|
+
'connection_method' => cm.class.basename,
|
488
|
+
'connection_method_args' => cm.args,
|
489
|
+
'addressing_method' => am.class.basename,
|
490
|
+
'addressing_method_args' => am.args
|
491
|
+
}.to_json
|
492
|
+
|
493
|
+
# Save the serialized configuration.
|
494
|
+
ConfigStore.write_config('network_config', json_data)
|
495
|
+
end
|
496
|
+
|
497
|
+
# This reads a JSON configuration file on STDIN and writes the interfaces
|
498
|
+
# file. Also the classes instantiated will have a chance to write
|
499
|
+
# out any auxiliary files needed.
|
500
|
+
def self.configure_system_network
|
501
|
+
connection_method, addressing_method = read_network_config
|
502
|
+
|
503
|
+
ifname = connection_method.config_interface_name
|
504
|
+
|
505
|
+
# squirrel away the name of the interface we are configuring
|
506
|
+
# This will be useful later for getting network status information.
|
507
|
+
ConfigStore.write_config('network_interface', ifname)
|
508
|
+
|
509
|
+
# Write the /etc/network/interfaces file.
|
510
|
+
File.open(INTERFACES_FILE, 'w') do |f|
|
511
|
+
f.puts "# Concerto Live network configuration"
|
512
|
+
f.puts "# Generated by netconfig.rb"
|
513
|
+
f.puts "# Changes will be lost on reboot"
|
514
|
+
f.puts "auto lo"
|
515
|
+
f.puts "iface lo inet loopback"
|
516
|
+
f.puts ""
|
517
|
+
f.puts "auto #{ifname}"
|
518
|
+
f.puts "iface #{ifname} inet #{addressing_method.addressing_type}"
|
519
|
+
|
520
|
+
addressing_method.interfaces_lines.each do |line|
|
521
|
+
f.puts "\t#{line}"
|
522
|
+
end
|
523
|
+
|
524
|
+
connection_method.interfaces_lines.each do |line|
|
525
|
+
f.puts "\t#{line}"
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
# Write auxiliary configuration files.
|
530
|
+
connection_method.write_configs
|
531
|
+
end
|
532
|
+
|
533
|
+
# Get the name of the interface we configured
|
534
|
+
def self.configured_interface
|
535
|
+
ifname = ConfigStore.read_config('network_interface', '')
|
536
|
+
if ifname != ''
|
537
|
+
Interface.new(ifname)
|
538
|
+
else
|
539
|
+
nil
|
540
|
+
end
|
541
|
+
end
|
542
|
+
end
|
data/web/app.rb
ADDED
@@ -0,0 +1,320 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'haml'
|
4
|
+
require 'json'
|
5
|
+
require 'net/http'
|
6
|
+
require 'ipaddress'
|
7
|
+
require 'netconfig'
|
8
|
+
require 'sys/uptime'
|
9
|
+
require 'sys/proctable'
|
10
|
+
include Sys
|
11
|
+
|
12
|
+
class ConcertoConfigServer < Sinatra::Base
|
13
|
+
# push these over to netconfig.rb?
|
14
|
+
# Our list of available physical-layer connection methods...
|
15
|
+
CONNECTION_METHODS = [
|
16
|
+
ConcertoConfig::WiredConnection,
|
17
|
+
ConcertoConfig::WirelessConnection
|
18
|
+
]
|
19
|
+
# ... and available layer-3 addressing methods.
|
20
|
+
ADDRESSING_METHODS = [
|
21
|
+
ConcertoConfig::DHCPAddressing,
|
22
|
+
ConcertoConfig::StaticAddressing
|
23
|
+
]
|
24
|
+
|
25
|
+
# Hosts we allow to access configuration without authenticating.
|
26
|
+
LOCALHOSTS = [
|
27
|
+
IPAddress.parse("127.0.0.1"),
|
28
|
+
IPAddress.parse("::1")
|
29
|
+
]
|
30
|
+
|
31
|
+
set :haml, { :format => :html5, :layout => :main }
|
32
|
+
|
33
|
+
helpers do
|
34
|
+
# Get the return value of the method on obj if obj supports the method.
|
35
|
+
# Otherwise return the empty string.
|
36
|
+
# This is useful in views where arguments may be of diverse types.
|
37
|
+
def value_from(obj, method)
|
38
|
+
if obj.respond_to? method
|
39
|
+
obj.send method
|
40
|
+
else
|
41
|
+
""
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Enforce authentication on actions.
|
46
|
+
# Calling from within an action will check authentication and return
|
47
|
+
# 401 if unauthorized.
|
48
|
+
def protected!
|
49
|
+
unless authorized?
|
50
|
+
response['WWW-Authenticate'] = \
|
51
|
+
%(Basic realm="Concerto Configuration")
|
52
|
+
throw(:halt, [401, "Not authorized\n"])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Check authorization credentials.
|
57
|
+
# Currently configured to check if the REMOTE_ADDR is local and allow
|
58
|
+
# everything if so. This permits someone at local console to configure
|
59
|
+
# without the need for a password. Others must have the correct
|
60
|
+
# password to be considered authorized.
|
61
|
+
def authorized?
|
62
|
+
ip = IPAddress.parse(request.env['REMOTE_ADDR'])
|
63
|
+
password = ConcertoConfig::ConfigStore.read_config(
|
64
|
+
'password', 'default'
|
65
|
+
)
|
66
|
+
if LOCALHOSTS.include? ip
|
67
|
+
# allow all requests from localhost no questions asked
|
68
|
+
true
|
69
|
+
else
|
70
|
+
@auth ||= Rack::Auth::Basic::Request.new(request.env)
|
71
|
+
@auth.provided? && @auth.basic? && @auth.credentials && \
|
72
|
+
@auth.credentials == ['root', password]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Get our base URL from wherever it may be stored.
|
77
|
+
def concerto_url
|
78
|
+
ConcertoConfig::ConfigStore.read_config('concerto_url', '')
|
79
|
+
end
|
80
|
+
|
81
|
+
# Try to figure out what our current IPv4 address is
|
82
|
+
# and return it as a string.
|
83
|
+
def my_ip
|
84
|
+
iface = ConcertoConfig.configured_interface
|
85
|
+
if iface
|
86
|
+
iface.ip
|
87
|
+
else
|
88
|
+
"Network setup failed or bad configuration"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Check if we have something resembling a network connection.
|
93
|
+
# This means we found a usable interface and it has an IPv4 address.
|
94
|
+
def network_ok
|
95
|
+
iface = ConcertoConfig.configured_interface
|
96
|
+
if iface
|
97
|
+
if iface.ip != "0.0.0.0"
|
98
|
+
true
|
99
|
+
else
|
100
|
+
false
|
101
|
+
end
|
102
|
+
else
|
103
|
+
false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Check if we can retrieve a URL and get a 200 status code.
|
108
|
+
def validate_url(url)
|
109
|
+
begin
|
110
|
+
# this will fail with Errno::something if server
|
111
|
+
# can't be reached
|
112
|
+
response = Net::HTTP.get_response(URI(url))
|
113
|
+
if response.code != "200"
|
114
|
+
# also bomb out if we don't get an OK response
|
115
|
+
# maybe demanding 200 is too strict here?
|
116
|
+
fail
|
117
|
+
end
|
118
|
+
|
119
|
+
# if we get here we have a somewhat valid URL to go to
|
120
|
+
true
|
121
|
+
rescue
|
122
|
+
# our request bombed out for some reason
|
123
|
+
false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
get '/' do
|
129
|
+
if concerto_url == ''
|
130
|
+
redirect '/setup'
|
131
|
+
else
|
132
|
+
redirect '/player_status'
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# The local fullscreen browser will go to /screen.
|
137
|
+
# We should redirect to the screen URL if possible.
|
138
|
+
# Otherwise, we need to go to the setup page to show useful information
|
139
|
+
# and allow for local configuration if needed/wanted.
|
140
|
+
get '/screen' do
|
141
|
+
# if we don't have a URL go to setup
|
142
|
+
# if we do, check it out
|
143
|
+
if concerto_url == ''
|
144
|
+
redirect '/setup'
|
145
|
+
else
|
146
|
+
# check if the concerto server is reachable, if so redirect there
|
147
|
+
# if not redirect to a local error message screen
|
148
|
+
if validate_url(concerto_url)
|
149
|
+
redirect concerto_url
|
150
|
+
else
|
151
|
+
redirect '/problem'
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Present a form for entering the base URL.
|
157
|
+
get '/setup' do
|
158
|
+
protected!
|
159
|
+
if network_ok
|
160
|
+
# Everything's up and running, we just don't know what
|
161
|
+
# our URL should be.
|
162
|
+
haml :setup
|
163
|
+
else
|
164
|
+
# The network settings are not sane, we don't have an IP.
|
165
|
+
# Redirect the user to the network configuration page to
|
166
|
+
# take care of this.
|
167
|
+
#redirect '/netconfig'
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Save the Concerto base URL.
|
172
|
+
post '/setup' do
|
173
|
+
protected!
|
174
|
+
url = params[:url]
|
175
|
+
if validate_url(url)
|
176
|
+
# save to the configuration store
|
177
|
+
ConcertoConfig::ConfigStore.write_config('concerto_url', url)
|
178
|
+
|
179
|
+
# root will now redirect to the proper concerto_url
|
180
|
+
redirect '/screen'
|
181
|
+
else
|
182
|
+
# the URL was no good, back to setup!
|
183
|
+
# error handling flash something something something
|
184
|
+
redirect '/setup'
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# render a page indicating that the concerto_url is no good.
|
189
|
+
# this page redirects to / every 5 seconds
|
190
|
+
get '/problem' do
|
191
|
+
haml :problem
|
192
|
+
end
|
193
|
+
|
194
|
+
get '/netconfig' do
|
195
|
+
protected!
|
196
|
+
|
197
|
+
# parse existing config file (if any)
|
198
|
+
# if there isn't one, nil will suffice because our
|
199
|
+
# value_from(...) helper will return the empty string if a method
|
200
|
+
# is not implemented. This is also how we get away with just having
|
201
|
+
# one instance each of the config classes that are currently selected.
|
202
|
+
begin
|
203
|
+
cm, am = ConcertoConfig.read_network_config
|
204
|
+
rescue Errno::ENOENT
|
205
|
+
cm = nil
|
206
|
+
am = nil
|
207
|
+
end
|
208
|
+
|
209
|
+
# view will grab what it can from our existing
|
210
|
+
# connection/addressing methods using value_from().
|
211
|
+
haml :netsettings, :locals => {
|
212
|
+
:connection_method => cm,
|
213
|
+
:addressing_method => am
|
214
|
+
}
|
215
|
+
end
|
216
|
+
|
217
|
+
# Given the name of a class, pick a class out of a list of allowed classes.
|
218
|
+
# This is used for parsing the form input for network configuration.
|
219
|
+
def pick_class(name, list)
|
220
|
+
list.find { |klass| klass.basename == name }
|
221
|
+
end
|
222
|
+
|
223
|
+
# Extract arguments from a set of form data that are intended to go to a
|
224
|
+
# specific network configuration class. These fields have names of the form
|
225
|
+
# 'ClassName/field_name'; this function returns a hash in the form
|
226
|
+
# { 'field_name' => 'value } containing the configuration arguments.
|
227
|
+
def extract_class_args(params, target_class)
|
228
|
+
result = { }
|
229
|
+
params.each do |key, value|
|
230
|
+
klass, arg = key.split('/', 2)
|
231
|
+
if klass == target_class
|
232
|
+
result[arg] = value
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
result
|
237
|
+
end
|
238
|
+
|
239
|
+
# Set the arguments on an instance of a given configuration class.
|
240
|
+
# This uses the safe_assign method that should be present in all
|
241
|
+
# configuration classes to determine which values are allowed to be passed
|
242
|
+
# via form fields (i.e. which ones are subject to validation)
|
243
|
+
def do_assign(params, instance)
|
244
|
+
safe = instance.safe_assign
|
245
|
+
|
246
|
+
params.each do |param, value|
|
247
|
+
if safe.include? param.intern
|
248
|
+
instance.send((param + '=').intern, value)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Process the form fields and generate a JSON network configuration file.
|
254
|
+
post '/netconfig' do
|
255
|
+
protected!
|
256
|
+
|
257
|
+
# First we find the connection-method and addressing-method classes.
|
258
|
+
cmclass = pick_class(params[:connection_type], CONNECTION_METHODS)
|
259
|
+
fail "Connection method not supported" if cmclass.nil?
|
260
|
+
|
261
|
+
amclass = pick_class(params[:addressing_type], ADDRESSING_METHODS)
|
262
|
+
fail "Addressing method not supported" if amclass.nil?
|
263
|
+
|
264
|
+
# ... and create some instances of them.
|
265
|
+
cm = cmclass.new
|
266
|
+
am = amclass.new
|
267
|
+
|
268
|
+
# Now given the names of the specific classes the user has chosen,
|
269
|
+
# extract the corresponding form fields.
|
270
|
+
cmargs = extract_class_args(params, cmclass.basename)
|
271
|
+
amargs = extract_class_args(params, amclass.basename)
|
272
|
+
|
273
|
+
# Set properties on each instance given the form values passed in.
|
274
|
+
do_assign(cmargs, cm)
|
275
|
+
do_assign(amargs, am)
|
276
|
+
|
277
|
+
# Save the configuration file.
|
278
|
+
ConcertoConfig.write_network_config(cm, am)
|
279
|
+
|
280
|
+
# Reload network configuration.
|
281
|
+
STDERR.puts "Trying to bring down the interface"
|
282
|
+
if ConcertoConfig.configured_interface
|
283
|
+
ConcertoConfig.configured_interface.ifdown
|
284
|
+
end
|
285
|
+
STDERR.puts "Rewriting configuration files"
|
286
|
+
ConcertoConfig::configure_system_network
|
287
|
+
STDERR.puts "Bringing interface back up"
|
288
|
+
ConcertoConfig.configured_interface.ifup
|
289
|
+
|
290
|
+
# Back to the network form.
|
291
|
+
redirect '/netconfig' # as a get request
|
292
|
+
end
|
293
|
+
|
294
|
+
get '/password' do
|
295
|
+
protected!
|
296
|
+
haml :password
|
297
|
+
end
|
298
|
+
|
299
|
+
post '/password' do
|
300
|
+
protected!
|
301
|
+
|
302
|
+
if params[:newpass] != params[:newpass_confirm]
|
303
|
+
# something something error handling something
|
304
|
+
redirect '/password'
|
305
|
+
end
|
306
|
+
|
307
|
+
ConcertoConfig::ConfigStore.write_config('password', params[:newpass])
|
308
|
+
redirect '/setup'
|
309
|
+
end
|
310
|
+
|
311
|
+
#Shows uptime,firmware version, and general system and process information
|
312
|
+
#Requires ffi, sys-uptime, and sys-proctable gems
|
313
|
+
get '/player_status' do
|
314
|
+
@proctable = ProcTable.ps
|
315
|
+
haml :player_status
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
319
|
+
|
320
|
+
ConcertoConfigServer.run!
|
data/web/config.ru
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
function show_hide(id_to_show, where) {
|
2
|
+
$(where).each(function(k, el) {
|
3
|
+
if ($(el).attr('id') == id_to_show) {
|
4
|
+
$(el).show( );
|
5
|
+
} else {
|
6
|
+
$(el).hide( );
|
7
|
+
}
|
8
|
+
});
|
9
|
+
}
|
10
|
+
|
11
|
+
function update_connection_type_fields() {
|
12
|
+
var id_to_show = $("#connection_type").val();
|
13
|
+
show_hide(id_to_show, "#connection div");
|
14
|
+
}
|
15
|
+
|
16
|
+
function update_address_type_fields() {
|
17
|
+
var id_to_show = $("#addressing_type").val();
|
18
|
+
show_hide(id_to_show, "#address div");
|
19
|
+
}
|
20
|
+
|
21
|
+
$(function() {
|
22
|
+
update_connection_type_fields( );
|
23
|
+
update_address_type_fields( );
|
24
|
+
|
25
|
+
$("#connection_type").change(update_connection_type_fields);
|
26
|
+
$("#addressing_type").change(update_address_type_fields);
|
27
|
+
});
|
Binary file
|
data/web/views/main.haml
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
%script{:src=>"network.js"}
|
2
|
+
%a{:href=>"/setup"}="Back to setup"
|
3
|
+
%h1="Concerto Player Network Configuration"
|
4
|
+
%form{:method=>"post"}
|
5
|
+
%h2="Connection Type"
|
6
|
+
%select#connection_type{:name=>"connection_type"}
|
7
|
+
/ list out our available connection methods as options
|
8
|
+
/ set the html selected flag on the one that is currently
|
9
|
+
/ selected in the config
|
10
|
+
- CONNECTION_METHODS.each do |cm|
|
11
|
+
%option{:value=>cm.basename,
|
12
|
+
:selected=>(cm==connection_method.class)}=cm.description
|
13
|
+
#connection
|
14
|
+
#WiredConnection
|
15
|
+
%h3="Wired Connection Settings"
|
16
|
+
%p
|
17
|
+
%label{:for=>"WiredConnection_interface_name"}="Interface Name"
|
18
|
+
%select#interface_name{:name=>"WiredConnection/interface_name"}
|
19
|
+
%option{:value=>""}="Auto Select"
|
20
|
+
/ list out available interfaces and their MAC addresses
|
21
|
+
/ preselect one if there was one chosen in config
|
22
|
+
- ConcertoConfig::WiredConnection.interfaces.each do |iface|
|
23
|
+
%option{:value=>iface.name, :selected=>value_from(connection_method, :interface_name)==iface.name}="#{iface.name} - #{iface.mac}"
|
24
|
+
#WirelessConnection
|
25
|
+
%h3="Wireless Connection Settings (no encryption)"
|
26
|
+
%p
|
27
|
+
%label{:for=>"WirelessConnection_interface_name"}="Interface Name"
|
28
|
+
%select#interface_name{:name=>"WirelessConnection/interface_name"}
|
29
|
+
%option{:value=>""}="Auto Select"
|
30
|
+
/ same as above but with the wireless interfaces
|
31
|
+
- ConcertoConfig::WirelessConnection.interfaces.each do |iface|
|
32
|
+
%option{:value=>iface.name, :selected=>value_from(connection_method, :interface_name) == iface.name}="#{iface.name} - #{iface.mac}"
|
33
|
+
%p
|
34
|
+
%label{:for=>"WirelessConnection_ssid"}="SSID"
|
35
|
+
%input{:type=>"text", :name=>"WirelessConnection/ssid",
|
36
|
+
:value=>value_from(connection_method, :ssid)}
|
37
|
+
%h2="IP Address"
|
38
|
+
%select#addressing_type{:name=>"addressing_type"}
|
39
|
+
- ADDRESSING_METHODS.each do |am|
|
40
|
+
%option{:value=>am.basename,
|
41
|
+
:selected=>(am==addressing_method.class)}=am.description
|
42
|
+
#address
|
43
|
+
#DHCPAddressing
|
44
|
+
#StaticAddressing
|
45
|
+
%h3="Static Address Settings"
|
46
|
+
%p
|
47
|
+
%label{:for=>"StaticAddressing_address"}="Address"
|
48
|
+
%input{:type=>"text", :name=>"StaticAddressing/address",
|
49
|
+
:value=>value_from(addressing_method, :address)}
|
50
|
+
%p
|
51
|
+
%label{:for=>"StaticAddressing_netmask"}="Netmask"
|
52
|
+
%input{:type=>"text", :name=>"StaticAddressing/netmask",
|
53
|
+
:value=>value_from(addressing_method, :netmask)}
|
54
|
+
%p
|
55
|
+
%label{:for=>"StaticAddressing_gateway"}="Gateway"
|
56
|
+
%input{:type=>"text", :name=>"StaticAddressing/gateway",
|
57
|
+
:value=>value_from(addressing_method, :gateway)}
|
58
|
+
%p
|
59
|
+
%label{:for=>"StaticAddressing_address"}="Nameservers (separate with commas or spaces)"
|
60
|
+
%input{:type=>"text", :name=>"StaticAddressing/nameservers_flat",
|
61
|
+
:value=>value_from(addressing_method, :nameservers_flat)}
|
62
|
+
|
63
|
+
%input{:type=>"submit"}
|
64
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
%form{:method=>'post'}
|
2
|
+
%p
|
3
|
+
%label{:for=>'newpass'} New Password
|
4
|
+
%input{:type=>'password', :name=>'newpass'}
|
5
|
+
%p
|
6
|
+
%label{:for=>'newpass_confirm'} Confirm Password
|
7
|
+
%input{:type=>'password', :name=>'newpass_confirm'}
|
8
|
+
%p
|
9
|
+
%input{:type=>'submit', :value=>'Change Password'}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
%script{:src=>"problem.js"}
|
2
|
+
.error
|
3
|
+
%p
|
4
|
+
%img{:src=>"trollface.png"}
|
5
|
+
|
6
|
+
%h1 Problem?
|
7
|
+
%p
|
8
|
+
Due to technical difficulties, this Concerto screen is currently not
|
9
|
+
operational. Please check again soon.
|
10
|
+
%p
|
11
|
+
==The Concerto installation at #{concerto_url} could not be reached.
|
12
|
+
%p
|
13
|
+
If the URL needs to be changed, go back to
|
14
|
+
%a{:href=>"/setup"}
|
15
|
+
Player Configuration
|
@@ -0,0 +1,25 @@
|
|
1
|
+
%h1 Welcome to Concerto Player
|
2
|
+
%p
|
3
|
+
You're seeing this because your Concerto player has not yet been configured.
|
4
|
+
We need the URL of your Concerto instance before we can get up and running.
|
5
|
+
|
6
|
+
%form{:method=>'post'}
|
7
|
+
%p
|
8
|
+
%label{:for=>'url'} URL
|
9
|
+
%input{:type=>'text', :name=>'url'}
|
10
|
+
%input{:type=>'submit', :value=>'Here it is!'}
|
11
|
+
|
12
|
+
%p
|
13
|
+
The IPv4 address of this screen appears to be:
|
14
|
+
=my_ip
|
15
|
+
%p
|
16
|
+
==This page can be accessed remotely via http://#{my_ip}/setup
|
17
|
+
%p
|
18
|
+
Username is root, default password is 'default'.
|
19
|
+
%p
|
20
|
+
Also, you may want to
|
21
|
+
%a{:href=>'/netconfig'} change network settings
|
22
|
+
or
|
23
|
+
%a{:href=>'/password'} change the configuration password.
|
24
|
+
|
25
|
+
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bandshell
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.8
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Andrew Armenia
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-30 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: sinatra
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: haml
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: sys-uptime
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: sys-proctable
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: ipaddress
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: Client-side tools for Concerto digital signage
|
95
|
+
email: andrew@asquaredlabs.com
|
96
|
+
executables:
|
97
|
+
- concerto_netsetup
|
98
|
+
- concerto_configserver
|
99
|
+
extensions: []
|
100
|
+
extra_rdoc_files: []
|
101
|
+
files:
|
102
|
+
- lib/live_image.rb
|
103
|
+
- lib/netconfig.rb
|
104
|
+
- lib/config_store.rb
|
105
|
+
- web/config.ru
|
106
|
+
- web/app.rb
|
107
|
+
- web/public/network.js
|
108
|
+
- web/public/stylesheet.css
|
109
|
+
- web/public/problem.js
|
110
|
+
- web/public/trollface.png
|
111
|
+
- web/views/main.haml
|
112
|
+
- web/views/password.haml
|
113
|
+
- web/views/setup.haml
|
114
|
+
- web/views/netsettings.haml
|
115
|
+
- web/views/problem.haml
|
116
|
+
- web/views/player_status.haml
|
117
|
+
- bin/concerto_configserver
|
118
|
+
- bin/bandshell-daemon.rb
|
119
|
+
- bin/concerto_netsetup
|
120
|
+
- bin/bandshell-control.rb
|
121
|
+
homepage:
|
122
|
+
licenses: []
|
123
|
+
post_install_message:
|
124
|
+
rdoc_options: []
|
125
|
+
require_paths:
|
126
|
+
- lib
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
128
|
+
none: false
|
129
|
+
requirements:
|
130
|
+
- - ! '>='
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ! '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
requirements: []
|
140
|
+
rubyforge_project:
|
141
|
+
rubygems_version: 1.8.24
|
142
|
+
signing_key:
|
143
|
+
specification_version: 3
|
144
|
+
summary: Concerto Client Tools
|
145
|
+
test_files: []
|