|
|
- #!/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
|