concerto_client 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|