#!/usr/bin/env perl
|
|
|
|
=head1 NAME
|
|
|
|
occi - Adafruit Occidentalis Configuration Helper for Raspberry Pi
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
occi is a simple utility for applying configuration settings like
|
|
hostname and WiFi credentials to your Raspberry Pi.
|
|
|
|
Settings are stored in a simple text file, usually
|
|
F</boot/occidentalis.txt>. The format is:
|
|
|
|
hostname=somepi
|
|
|
|
# wifi configuration details:
|
|
wifi_ssid=Your Network Here
|
|
wifi_password=your password here
|
|
|
|
Blank lines and comments starting with C<#> will be ignored. Keys are
|
|
case-insensitive.
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
# apply configuration in /boot/occidentalis.txt
|
|
sudo occi
|
|
|
|
# check current version
|
|
occi --version
|
|
|
|
# apply a different configuration file
|
|
sudo occi --file=/home/alternate_occidentalis.txt
|
|
|
|
=cut
|
|
|
|
package OcciConfig v0.7.0;
|
|
|
|
use warnings;
|
|
use strict;
|
|
use 5.10.0;
|
|
|
|
use File::Basename;
|
|
use File::Copy;
|
|
use Getopt::Long;
|
|
use IPC::Cmd qw(can_run run);
|
|
|
|
# Handle CLI options:
|
|
my $OCCI_CONFIG = "/boot/occidentalis.txt";
|
|
my $show_version_and_exit = 0;
|
|
my $install_and_exit = 0;
|
|
GetOptions(
|
|
"file=s" => \$OCCI_CONFIG,
|
|
"version" => \$show_version_and_exit,
|
|
"install" => \$install_and_exit
|
|
) or die("Error in command line arguments\n");
|
|
|
|
diag("Adafruit Occidentalis configuration helper, ${OcciConfig::VERSION}");
|
|
if ($show_version_and_exit) {
|
|
exit 0;
|
|
}
|
|
|
|
if ($install_and_exit) {
|
|
install();
|
|
exit 0;
|
|
}
|
|
|
|
have_config_or_exit($OCCI_CONFIG);
|
|
my %config = parse_config($OCCI_CONFIG);
|
|
|
|
diag('file', $OCCI_CONFIG);
|
|
|
|
{
|
|
# This bit of magic will find every sub starting with "handle_".
|
|
# It just stands in for explicitly calling:
|
|
#
|
|
# handle_hostname(%config);
|
|
# handle_wifi(%config);
|
|
#
|
|
# and so on down the line.
|
|
|
|
no strict 'refs';
|
|
my (@handlers) = grep { defined &{"OcciConfig\::$_"} && m/^handle_/ } keys %{"OcciConfig\::"};
|
|
|
|
diag_push('run');
|
|
foreach my $handler (@handlers) {
|
|
diag_push($handler);
|
|
&{$handler}(%config);
|
|
diag_pop();
|
|
}
|
|
diag_pop();
|
|
}
|
|
|
|
exit 0;
|
|
|
|
=head1 CONFIGURATION HANDLERS
|
|
|
|
To add a handler, just write a sub that takes the %config hash, like so,
|
|
and returns a list containing one or more log items:
|
|
|
|
sub handle_foo {
|
|
my %config = @_;
|
|
return ('nothing to do here');
|
|
}
|
|
|
|
It will automatically be called every time occi runs.
|
|
|
|
=over
|
|
|
|
=item handle_selftest()
|
|
|
|
Run some basic checks to make sure we have necessary stuff.
|
|
|
|
=cut
|
|
|
|
sub handle_selftest {
|
|
my %config = @_;
|
|
|
|
my %allowed_keys = map { $_ => 1} qw(
|
|
hostname
|
|
wifi_password wifi_ssid
|
|
);
|
|
|
|
diag('Checking configuration integrity.');
|
|
|
|
foreach my $key (sort keys %config) {
|
|
if ($allowed_keys{$key}) {
|
|
diag('valid', $key, $config{$key});
|
|
} else {
|
|
diag('error', $key, 'unrecognized configuration key');
|
|
}
|
|
}
|
|
|
|
# This is just a bit of a sanity check - do we know whether some
|
|
# things we might expect are installed? Currently commented out
|
|
# since we no longer rely on apt for installation of these tools.
|
|
#
|
|
# my @check_packages = qw(occi occidentalis);
|
|
# foreach my $package (@check_packages) {
|
|
# if (is_installed_package($package)) {
|
|
# diag('have package', $package);
|
|
# } else {
|
|
# diag('no package', $package);
|
|
# }
|
|
# }
|
|
|
|
chomp(my $dversion = get_file('/etc/debian_version'));
|
|
diag('debian version', $dversion);
|
|
}
|
|
|
|
=item handle_hostname()
|
|
|
|
Update current hostname and make sure it's set properly at boot.
|
|
|
|
=cut
|
|
|
|
sub handle_hostname {
|
|
my %config = @_;
|
|
|
|
return ('no hostname specified')
|
|
unless defined $config{hostname};
|
|
|
|
my $hostname_changed = 0;
|
|
|
|
# What's the existing configuration?
|
|
chomp(my $existing_etc_hostname = get_file('/etc/hostname'));
|
|
chomp(my $existing_hostname = capture_string('hostname'));
|
|
|
|
unless ($existing_etc_hostname eq $config{hostname}) {
|
|
# Make sure this is set correctly at next boot
|
|
diag('Setting /etc/hostname to ' . $config{hostname});
|
|
put_file('/etc/hostname', $config{hostname});
|
|
$hostname_changed = 1;
|
|
}
|
|
|
|
unless ($existing_hostname eq $config{hostname}) {
|
|
# Make sure this is set correctly right _now_.
|
|
diag('Setting current hostname to ' . $config{hostname});
|
|
system('hostname', $config{hostname});
|
|
$hostname_changed = 1;
|
|
}
|
|
|
|
# Make sure our new hostname is mentioned in /etc/hosts:
|
|
my $etc_hosts = get_file('/etc/hosts');
|
|
my $new_etc_hosts = $etc_hosts;
|
|
my $config_hostline = "127.0.1.1\t$config{hostname}";
|
|
$new_etc_hosts =~ s/^(127[.]0[.]1[.]1\s+${existing_hostname})$/$config_hostline/m;
|
|
if ($new_etc_hosts !~ /$config_hostline/) {
|
|
$new_etc_hosts .= "\n$config_hostline";
|
|
}
|
|
if ($etc_hosts ne $new_etc_hosts) {
|
|
diag('Adding ' . $config_hostline . ' to /etc/hosts');
|
|
put_file('/etc/hosts', $new_etc_hosts);
|
|
$hostname_changed = 1;
|
|
}
|
|
|
|
if ($hostname_changed && (-f '/etc/init.d/avahi-daemon')) {
|
|
diag('restarting avahi-daemon');
|
|
my (@restart_log) = capture_list('service', 'avahi-daemon', 'restart');
|
|
foreach my $logline (@restart_log) {
|
|
diag($logline);
|
|
}
|
|
}
|
|
}
|
|
|
|
=item handle_wifi()
|
|
|
|
Configure a wireless network.
|
|
|
|
=cut
|
|
|
|
sub handle_wifi {
|
|
my %config = @_;
|
|
|
|
my $conf_file = '/etc/wpa_supplicant/wpa_supplicant.conf';
|
|
my $blurb = get_blurb();
|
|
|
|
return ('no wifi_ssid specified')
|
|
unless defined $config{wifi_ssid};
|
|
|
|
diag('Configuring network :: ' . $config{wifi_ssid});
|
|
|
|
my ($ifconfig) = capture_string('ifconfig', '-s');
|
|
if ($ifconfig !~ /wlan/) {
|
|
diag('No wireless hardware found.');
|
|
} elsif (defined $config{wifi_password}) {
|
|
my $wpa_config = capture_string(
|
|
'wpa_passphrase',
|
|
$config{wifi_ssid},
|
|
$config{'wifi_password'}
|
|
);
|
|
|
|
$wpa_config = <<"WPA";
|
|
# $blurb
|
|
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
|
update_config=1
|
|
$wpa_config
|
|
WPA
|
|
|
|
diag("Writing $conf_file");
|
|
|
|
my @log = capture_list(
|
|
'wpa_cli',
|
|
'reconfigure'
|
|
);
|
|
foreach my $logline (@log) {
|
|
diag($logline);
|
|
}
|
|
|
|
put_file($conf_file, $wpa_config, $conf_file . '.backup');
|
|
} else {
|
|
diag("No wifi_password defined, falling back to iwconfig.");
|
|
my $iwlog = capture_string(
|
|
'iwconfig',
|
|
# TODO: this of course is not always going to be the same...
|
|
'wlan0',
|
|
'essid',
|
|
$config{wifi_ssid}
|
|
);
|
|
|
|
}
|
|
}
|
|
|
|
=back
|
|
|
|
=head1 UTILITY SUBROUTINES
|
|
|
|
=over
|
|
|
|
=item capture_string($cmd, @args)
|
|
|
|
Return a string containing the output of a command, or log an
|
|
error.
|
|
|
|
=cut
|
|
|
|
sub capture_string {
|
|
return join "", capture_list(@_);
|
|
}
|
|
|
|
=item capture_list($cmd, @args)
|
|
|
|
Return a list containing the output of a command, or log an
|
|
error.
|
|
|
|
=cut
|
|
|
|
sub capture_list {
|
|
my (@cmd) = @_;
|
|
|
|
my ($success, $error_message, $full_buf, $stdout_buf, $stderr_buf) =
|
|
run(command => \@cmd, verbose => 0);
|
|
if ($success) {
|
|
return @$full_buf;
|
|
} else {
|
|
diag('error', $error_message);
|
|
}
|
|
}
|
|
|
|
=item parse_config($path_to_file)
|
|
|
|
Grab a hash of configuration options out of some text file,
|
|
formatted like so:
|
|
|
|
key1=value
|
|
key2=value2
|
|
|
|
=cut
|
|
|
|
sub parse_config {
|
|
my %config;
|
|
my ($config_path) = @_;
|
|
my $config_str = get_file($config_path);
|
|
|
|
# Crude dos2unix:
|
|
$config_str =~ s/\r\n/\n/g;
|
|
|
|
while ($config_str =~ m{^([a-z_]+) = (.*?)$}ixmg) {
|
|
my $key = lc($1); # normalize to lowercase
|
|
my $value = $2;
|
|
$config{$key} = $value;
|
|
}
|
|
|
|
return %config;
|
|
}
|
|
|
|
=item get_file($path_to_file)
|
|
|
|
Returns the contents of a given file as a string.
|
|
|
|
=cut
|
|
|
|
sub get_file {
|
|
my ($path) = @_;
|
|
|
|
if (! -e $path) {
|
|
die "$path doesn't appear to exist."
|
|
}
|
|
|
|
local $/ = undef;
|
|
open my $fh, '<', $path
|
|
or die "Failed opening $path: $!";
|
|
my $contents = <$fh>;
|
|
close $fh;
|
|
|
|
return $contents;
|
|
}
|
|
|
|
=item put_file($path, $content)
|
|
|
|
Put $content in the file at $path.
|
|
|
|
=cut
|
|
|
|
sub put_file {
|
|
my ($path, $content, $backup_path) = @_;
|
|
|
|
# Handle one-time backups - this could use some rethinking.
|
|
if (defined $backup_path) {
|
|
if (! -e $backup_path) {
|
|
if (-e $path) {
|
|
my $old_contents = get_file($path);
|
|
put_file($backup_path, $old_contents);
|
|
}
|
|
}
|
|
}
|
|
|
|
open my $fh, '>', $path
|
|
or die "Failed opening $path: $!";
|
|
print $fh $content;
|
|
close $fh;
|
|
}
|
|
|
|
=item is_installed_package($package_name)
|
|
|
|
Check whether a given package is installed.
|
|
|
|
=cut
|
|
|
|
sub is_installed_package {
|
|
my ($package_name) = @_;
|
|
my $query_result = capture_string(
|
|
'dpkg-query',
|
|
'-W',
|
|
'-f',
|
|
'${Status}',
|
|
$package_name
|
|
);
|
|
|
|
return ($query_result =~ /install ok installed/);
|
|
}
|
|
|
|
=item install_package($package_name)
|
|
|
|
Ensure that a given package is installed. Should be idempotent.
|
|
|
|
Returns a status string and, if action taken, an install log.
|
|
|
|
=cut
|
|
|
|
sub install_package {
|
|
my ($package_name) = @_;
|
|
|
|
return 'already-installed'
|
|
if is_installed_package($package_name);
|
|
|
|
my @install_log = capture_list(
|
|
'apt-get',
|
|
'-y',
|
|
'install',
|
|
$package_name
|
|
);
|
|
|
|
return ('installed', @install_log);
|
|
}
|
|
|
|
=item uninstall_package($package_name)
|
|
|
|
Ensure that a given package is not installed. Should be idempotent.
|
|
|
|
Returns a status string and, if action taken, an uninstall log.
|
|
|
|
=cut
|
|
|
|
sub uninstall_package {
|
|
my ($package_name) = @_;
|
|
|
|
return 'already-uninstalled'
|
|
unless is_installed_package($package_name);
|
|
|
|
my @install_log = capture_list(
|
|
'apt-get',
|
|
'-y',
|
|
'remove',
|
|
$package_name
|
|
);
|
|
|
|
return ('uninstalled', @install_log);
|
|
}
|
|
|
|
=item get_blurb()
|
|
|
|
Return a useful blurb for inclusion in config file comments.
|
|
|
|
=cut
|
|
|
|
sub get_blurb {
|
|
return "This file is managed by $OCCI_CONFIG";
|
|
}
|
|
|
|
=item diag(@columns)
|
|
|
|
Print columns of diagnostic output. Use diag_push() to add a level of
|
|
indentation, and diag_pop() to remove it.
|
|
|
|
=cut
|
|
|
|
{
|
|
# Cheesy retention of state:
|
|
my @diag_stack = ();
|
|
|
|
sub diag {
|
|
my (@cols) = @_;
|
|
print join " :: ", (@diag_stack, @cols);
|
|
print "\n";
|
|
}
|
|
|
|
sub diag_push {
|
|
my ($value) = @_;
|
|
push @diag_stack, $value;
|
|
}
|
|
|
|
sub diag_pop {
|
|
pop @diag_stack;
|
|
}
|
|
}
|
|
|
|
=item have_config_or_exit($path)
|
|
|
|
Check that a given config file exists, and exit with some documentation if not.
|
|
|
|
=cut
|
|
|
|
sub have_config_or_exit {
|
|
my ($file) = @_;
|
|
|
|
return 1 if -f $file;
|
|
|
|
print STDERR <<"HELPTEXT";
|
|
It looks like you don't have a $OCCI_CONFIG yet.
|
|
|
|
In order to create one:
|
|
|
|
sudo nano $OCCI_CONFIG
|
|
|
|
And then add configuration keys like:
|
|
|
|
hostname=somepi
|
|
|
|
See /usr/share/doc/occi/occidentalis_example.txt for a full example.
|
|
HELPTEXT
|
|
|
|
exit 0;
|
|
}
|
|
|
|
=item install()
|
|
|
|
Install occi systemwide, including systemd service for running at startup.
|
|
|
|
=cut
|
|
|
|
sub install {
|
|
diag_push('install');
|
|
|
|
# Copy this script to /usr/local/bin:
|
|
my $src_path = __FILE__;
|
|
my $install_path = '/usr/local/bin/' . basename($src_path);
|
|
if (copy($src_path, $install_path)) {
|
|
diag("copied $src_path -> /usr/local/bin");
|
|
capture_string("chmod a+x $install_path");
|
|
} else {
|
|
diag(
|
|
"copy of $src_path to /usr/local/bin failed - do you need to use sudo?"
|
|
);
|
|
exit(1);
|
|
}
|
|
|
|
# Create a /boot/occidentalis.txt if it doesn't exist:
|
|
if (-e $OCCI_CONFIG) {
|
|
diag("Found existing $OCCI_CONFIG");
|
|
} else {
|
|
diag("Creating example $OCCI_CONFIG");
|
|
my $occi_text = <<"INI";
|
|
# Lines with a leading "#" are comments. Blank lines are ignored.
|
|
|
|
# Uncomment below to set hostname:
|
|
# hostname=somepi
|
|
|
|
# Uncomment below to configure a wireless network:
|
|
# wifi_ssid=somewifinetwork
|
|
# wifi_password=somepassword
|
|
INI
|
|
put_file($OCCI_CONFIG, $occi_text);
|
|
}
|
|
|
|
# 3. set up systemd to run this file
|
|
my $unit_path = '/etc/systemd/system/occi.service';
|
|
my $unit_text = <<"SYSTEMD";
|
|
[Unit]
|
|
Description=Adafruit Occidentalis Configuration Helper for Raspberry Pi
|
|
|
|
[Service]
|
|
ExecStart=/usr/local/bin/occi
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
SYSTEMD
|
|
|
|
diag("Writing $unit_path");
|
|
put_file($unit_path, $unit_text);
|
|
capture_string("chmod 664 $unit_path");
|
|
|
|
diag('Enabling occi.service');
|
|
capture_string('systemctl daemon-reload');
|
|
capture_string('systemctl enable occi.service');
|
|
|
|
diag_pop();
|
|
}
|
|
|
|
=back
|
|
|
|
=head1 AUTHOR
|
|
|
|
Brennen Bearnes
|
|
Todd Treece
|
|
|
|
=head1 COPYING
|
|
|
|
The MIT License (MIT)
|
|
|
|
Copyright (c) 2015 Adafruit Industries
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
|
|
=cut
|