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