├── .gitignore ├── DEBIAN ├── changelog ├── conffiles ├── control └── postinst ├── README.md ├── etc └── init.d │ └── occi └── usr ├── bin └── occi └── share ├── doc └── occi │ └── occidentalis_example.txt └── man └── man1 └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | .DS_Store 4 | 5 | # generated man pages - see packages/Makefile 6 | usr/share/man/man1/*.gz 7 | usr/share/doc/occi/changelog.gz 8 | -------------------------------------------------------------------------------- /DEBIAN/changelog: -------------------------------------------------------------------------------- 1 | occi (0.6.0) unstable; urgency=low 2 | 3 | * Don't fail on absence of /boot/occidentalis.txt 4 | 5 | -- Brennen Bearnes Fri, 06 Mar 2015 22:35:18 +0000 6 | 7 | occi (0.5.1) unstable; urgency=low 8 | 9 | * Adds --version flag 10 | * Adds --file flag 11 | * Adds Getopt::Long for CLI option parsing 12 | 13 | -- Brennen Bearnes Mon, 02 Feb 2015 12:38:00 -0700 14 | 15 | occi (0.5.0) unstable; urgency=low 16 | 17 | * Removes handling of Adafruit WebIDE installs 18 | 19 | -- Brennen Bearnes Tue, 27 Jan 2015 14:07:00 -0700 20 | 21 | occi (0.4.0) unstable; urgency=low 22 | 23 | * Adds handling of Adafruit WebIDE installs based on webide=yes 24 | * Refactors some configuration testing and debug output 25 | 26 | -- Brennen Bearnes Mon, 26 Jan 2015 08:24:00 -0700 27 | 28 | occi (0.3.5) unstable; urgency=low 29 | 30 | * Cleans up example occidentalis.txt in POD and man page 31 | 32 | -- Brennen Bearnes Mon, 26 Jan 2015 08:24:00 -0700 33 | 34 | occi (0.3.4) unstable; urgency=low 35 | 36 | * Adds avahi-daemon restart to handle_hostname() 37 | 38 | -- Brennen Bearnes Thu, 22 Jan 2015 14:44:00 -0700 39 | 40 | occi (0.3.3) unstable; urgency=low 41 | 42 | * Adds wpa_supplicant restart to handle_wifi() 43 | 44 | -- Brennen Bearnes Thu, 22 Jan 2015 13:58:00 -0700 45 | 46 | occi (0.3.2) unstable; urgency=low 47 | 48 | * Improve documentation 49 | 50 | -- Brennen Bearnes Tue, 20 Jan 2015 15:35:00 -0700 51 | 52 | occi (0.3.1) unstable; urgency=low 53 | 54 | * Removes occi run from postinst 55 | 56 | -- Todd Treece Tue, 20 Jan 2015 14:23:00 -0500 57 | 58 | occi (0.1.0) unstable; urgency=low 59 | 60 | * Initial release. 61 | * First version of the occi config utility. 62 | 63 | -- Brennen Bearnes Tue, 13 Jan 2015 13:01:00 -0700 64 | -------------------------------------------------------------------------------- /DEBIAN/conffiles: -------------------------------------------------------------------------------- 1 | /etc/init.d/occi 2 | -------------------------------------------------------------------------------- /DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: occi 2 | Version: 0.6.0 3 | Section: base 4 | Priority: optional 5 | Architecture: all 6 | Depends: perl (>= 5.10.0), libipc-system-simple-perl (>= 1.21-1), wpasupplicant (>= 1.0) 7 | Maintainer: Brennen Bearnes 8 | Description: OCcidentalis Config for raspberry pI 9 | A simple utility to apply configuration options from 10 | /boot/occidentalis.txt. 11 | -------------------------------------------------------------------------------- /DEBIAN/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | update-rc.d -f occi defaults 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Occi 2 | 3 | A small configuration helper for the Raspberry Pi. 4 | 5 | See the [Adafruit Pi Finder](https://github.com/adafruit/Adafruit-Pi-Finder) and 6 | [Occidentalis](https://github.com/adafruit/Adafruit-Occidentalis) for more info. 7 | -------------------------------------------------------------------------------- /etc/init.d/occi: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: occi 5 | # Required-Start: $all 6 | # Required-Stop: 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: Occidentalis Configuration Utility 10 | ### END INIT INFO 11 | 12 | set -e 13 | 14 | # /etc/init.d/occi: run the occi configuration utility 15 | test -x /usr/bin/occi || exit 1 16 | 17 | # Carry out specific functions when asked to by the system 18 | case "$1" in 19 | start) 20 | echo Starting occi configuration 21 | occi 22 | ;; 23 | stop) 24 | ;; 25 | restart) 26 | echo Re-applying occi configuration 27 | occi 28 | ;; 29 | force-reload) 30 | echo Re-applying occi configuration 31 | occi 32 | ;; 33 | *) 34 | echo "Usage: /etc/init.d/occi {start|stop}" 35 | exit 1 36 | ;; 37 | esac 38 | 39 | exit 0 40 | -------------------------------------------------------------------------------- /usr/bin/occi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | =head1 NAME 4 | 5 | occi - Adafruit Occidentalis Configuration Helper for Raspberry Pi 6 | 7 | =head1 DESCRIPTION 8 | 9 | occi is a simple utility for applying configuration settings like 10 | hostname and WiFi credentials to your Raspberry Pi. 11 | 12 | Settings are stored in a simple text file, usually 13 | F. The format is: 14 | 15 | hostname=somepi 16 | 17 | # wifi configuration details: 18 | wifi_ssid=Your Network Here 19 | wifi_password=your password here 20 | # optionally add this if you don't broadcast SSID 21 | wifi_scan_ssid=1 22 | # optionally specify the key management 23 | wifi_key_mgmt=WPA_PSK 24 | 25 | Blank lines and comments starting with C<#> will be ignored. Keys are 26 | case-insensitive. 27 | 28 | See F for a full example. 29 | 30 | =head1 SYNOPSIS 31 | 32 | # apply configuration in /boot/occidentalis.txt 33 | sudo occi 34 | 35 | # check current version 36 | occi --version 37 | 38 | # apply a different configuration file 39 | sudo occi --file=/home/alternate_occidentalis.txt 40 | 41 | =cut 42 | 43 | package OcciConfig v0.6.0; 44 | 45 | use warnings; 46 | use strict; 47 | use 5.10.0; 48 | 49 | use Getopt::Long; 50 | use IPC::System::Simple qw(capturex); 51 | 52 | # Handle CLI options: 53 | my $OCCI_CONFIG = "/boot/occidentalis.txt"; 54 | my $show_version_and_exit = 0; 55 | GetOptions( 56 | "file=s" => \$OCCI_CONFIG, 57 | "version" => \$show_version_and_exit 58 | ) or die("Error in command line arguments\n"); 59 | 60 | diag("Adafruit Occidentalis configuration helper, ${OcciConfig::VERSION}"); 61 | if ($show_version_and_exit) { 62 | exit 0; 63 | } 64 | 65 | have_config_or_exit($OCCI_CONFIG); 66 | my %config = parse_config($OCCI_CONFIG); 67 | 68 | diag('file', $OCCI_CONFIG); 69 | 70 | { 71 | # This bit of magic will find every sub starting with "handle_". 72 | # It just stands in for explicitly calling: 73 | # 74 | # handle_hostname(%config); 75 | # handle_wifi(%config); 76 | # 77 | # and so on down the line. 78 | 79 | no strict 'refs'; 80 | my (@handlers) = grep { defined &{"OcciConfig\::$_"} && m/^handle_/ } keys %{"OcciConfig\::"}; 81 | 82 | diag_push('run'); 83 | foreach my $handler (@handlers) { 84 | diag_push($handler); 85 | &{$handler}(%config); 86 | diag_pop(); 87 | } 88 | diag_pop(); 89 | } 90 | 91 | exit 0; 92 | 93 | =head1 CONFIGURATION HANDLERS 94 | 95 | To add a handler, just write a sub that takes the %config hash, like so, 96 | and returns a list containing one or more log items: 97 | 98 | sub handle_foo { 99 | my %config = @_; 100 | return ('nothing to do here'); 101 | } 102 | 103 | It will automatically be called every time occi runs. 104 | 105 | =over 106 | 107 | =item handle_selftest() 108 | 109 | Run some basic sanity checks. 110 | 111 | =cut 112 | 113 | sub handle_selftest { 114 | my %config = @_; 115 | 116 | my %allowed_keys = map { $_ => 1} qw( 117 | hostname 118 | wifi_password wifi_ssid wifi_key_mgmt wifi_scan_ssid 119 | ); 120 | 121 | diag('Checking configuration for basic sanity.'); 122 | 123 | foreach my $key (sort keys %config) { 124 | if ($allowed_keys{$key}) { 125 | diag('valid', $key, $config{$key}); 126 | } else { 127 | diag('error', $key, 'unrecognized configuration key'); 128 | } 129 | } 130 | 131 | # This is just a bit of a sanity check - do we know whether some 132 | # things we might expect are installed? 133 | my @check_packages = qw(occi occidentalis); 134 | foreach my $package (@check_packages) { 135 | if (is_installed_package($package)) { 136 | diag('have package', $package); 137 | } else { 138 | diag('no package', $package); 139 | } 140 | } 141 | 142 | chomp(my $dversion = get_file('/etc/debian_version')); 143 | diag('debian version', $dversion); 144 | } 145 | 146 | =item handle_hostname() 147 | 148 | Update current hostname and make sure it's set properly at boot. 149 | 150 | =cut 151 | 152 | sub handle_hostname { 153 | my %config = @_; 154 | 155 | return ('no hostname specified') 156 | unless defined $config{hostname}; 157 | 158 | my $hostname_changed = 0; 159 | 160 | # What's the existing configuration? 161 | chomp(my $existing_etc_hostname = get_file('/etc/hostname')); 162 | chomp(my $existing_hostname = capture_string('hostname')); 163 | 164 | unless ($existing_etc_hostname eq $config{hostname}) { 165 | # Make sure this is set correctly at next boot 166 | diag('Setting /etc/hostname to ' . $config{hostname}); 167 | put_file('/etc/hostname', $config{hostname}); 168 | $hostname_changed = 1; 169 | } 170 | 171 | unless ($existing_hostname eq $config{hostname}) { 172 | # Make sure this is set correctly right _now_. 173 | diag('Setting current hostname to ' . $config{hostname}); 174 | system('hostname', $config{hostname}); 175 | $hostname_changed = 1; 176 | } 177 | 178 | # Make sure our new hostname is mentioned in /etc/hosts: 179 | my $etc_hosts = get_file('/etc/hosts'); 180 | my $new_etc_hosts = $etc_hosts; 181 | my $config_hostline = "127.0.1.1\t$config{hostname}"; 182 | $new_etc_hosts =~ s/^(127[.]0[.]1[.]1\s+${existing_hostname})$/$config_hostline/m; 183 | if ($new_etc_hosts !~ /$config_hostline/) { 184 | $new_etc_hosts .= "\n$config_hostline"; 185 | } 186 | if ($etc_hosts ne $new_etc_hosts) { 187 | diag('Adding ' . $config_hostline . ' to /etc/hosts'); 188 | put_file('/etc/hosts', $new_etc_hosts); 189 | $hostname_changed = 1; 190 | } 191 | 192 | if ($hostname_changed && (-f '/etc/init.d/avahi-daemon')) { 193 | diag('restarting avahi-daemon'); 194 | my (@restart_log) = capture_list('service', 'avahi-daemon', 'restart'); 195 | foreach my $logline (@restart_log) { 196 | diag($logline); 197 | } 198 | } 199 | } 200 | 201 | =item handle_wifi() 202 | 203 | Configure a wireless network. 204 | 205 | =cut 206 | 207 | sub handle_wifi { 208 | my %config = @_; 209 | 210 | my $conf_file = '/etc/wpa_supplicant/wpa_supplicant.conf'; 211 | my $blurb = get_blurb(); 212 | 213 | return ('no wifi_ssid specified') 214 | unless defined $config{wifi_ssid}; 215 | 216 | diag('Configuring network :: ' . $config{wifi_ssid}); 217 | 218 | my ($ifconfig) = capture_string('ifconfig', '-a'); 219 | if ($ifconfig !~ /wlan/) { 220 | diag('No wireless hardware found.'); 221 | } elsif (defined $config{wifi_password}) { 222 | my $wpa_config = capture_string( 223 | 'wpa_passphrase', 224 | $config{wifi_ssid}, 225 | $config{'wifi_password'} 226 | ); 227 | 228 | my $extraConfig = ""; 229 | if (defined $config{wifi_scan_ssid}) { 230 | $extraConfig = " scan_ssid=$config{wifi_scan_ssid}\n"; 231 | } 232 | 233 | if (defined $config{wifi_key_mgmt}) { 234 | $extraConfig = "$extraConfig key_mgmt=$config{wifi_key_mgmt}\n"; 235 | } 236 | $wpa_config =~ s/\}/$extraConfig\}/; 237 | 238 | $wpa_config = <<"WPA"; 239 | # $blurb 240 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 241 | update_config=1 242 | $wpa_config 243 | WPA 244 | 245 | diag("Writing $conf_file"); 246 | 247 | my @log = capture_list( 248 | 'wpa_cli', 249 | 'reconfigure' 250 | ); 251 | foreach my $logline (@log) { 252 | diag($logline); 253 | } 254 | 255 | put_file($conf_file, $wpa_config, $conf_file . '.backup'); 256 | } else { 257 | diag("No wifi_password defined, falling back to iwconfig."); 258 | my $iwlog = capture_string( 259 | 'iwconfig', 260 | # TODO: this of course is not always going to be the same... 261 | 'wlan0', 262 | 'essid', 263 | $config{wifi_ssid} 264 | ); 265 | 266 | } 267 | } 268 | 269 | 270 | =back 271 | 272 | =head1 UTILITY SUBROUTINES 273 | 274 | =over 275 | 276 | =item capture_string($cmd, @args) 277 | 278 | Return a string containing the output of a command, or log an 279 | error. 280 | 281 | =cut 282 | 283 | sub capture_string { 284 | my ($cmd, @args) = @_; 285 | 286 | my $output; 287 | 288 | eval { 289 | $output = capturex($cmd, @args); 290 | }; 291 | 292 | if ($@) { 293 | diag(1, 'error', $@); 294 | } 295 | 296 | return $output; 297 | } 298 | 299 | =item capture_list($cmd, @args) 300 | 301 | Return a list containing the output of a command, or log an 302 | error. 303 | 304 | =cut 305 | 306 | sub capture_list { 307 | my ($cmd, @args) = @_; 308 | 309 | my @output; 310 | 311 | eval { 312 | @output = capturex($cmd, @args); 313 | }; 314 | 315 | if ($@) { 316 | diag(1, 'error', $@); 317 | } 318 | 319 | return @output; 320 | } 321 | 322 | 323 | =item parse_config($path_to_file) 324 | 325 | Grab a hash of configuration options out of some text file, 326 | formatted like so: 327 | 328 | key1=value 329 | key2=value2 330 | 331 | =cut 332 | 333 | sub parse_config { 334 | my %config; 335 | my ($config_path) = @_; 336 | my $config_str = get_file($config_path); 337 | 338 | # Crude dos2unix: 339 | $config_str =~ s/\r\n/\n/g; 340 | 341 | while ($config_str =~ m{^([a-z_]+) = (.*?)$}ixmg) { 342 | my $key = lc($1); # normalize to lowercase 343 | my $value = $2; 344 | $config{$key} = $value; 345 | } 346 | 347 | return %config; 348 | } 349 | 350 | =item get_file($path_to_file) 351 | 352 | Returns the contents of a given file as a string. 353 | 354 | =cut 355 | 356 | sub get_file { 357 | my ($path) = @_; 358 | 359 | if (! -e $path) { 360 | die "$path doesn't appear to exist." 361 | } 362 | 363 | local $/ = undef; 364 | open my $fh, '<', $path 365 | or die "Failed opening $path: $!"; 366 | my $contents = <$fh>; 367 | close $fh; 368 | 369 | return $contents; 370 | } 371 | 372 | =item put_file($path, $content) 373 | 374 | Put $content in the file at $path. 375 | 376 | =cut 377 | 378 | sub put_file { 379 | my ($path, $content, $backup_path) = @_; 380 | 381 | # Handle one-time backups - this could use some rethinking. 382 | if (defined $backup_path) { 383 | if (! -e $backup_path) { 384 | if (-e $path) { 385 | my $old_contents = get_file($path); 386 | put_file($backup_path, $old_contents); 387 | } 388 | } 389 | } 390 | 391 | open my $fh, '>', $path 392 | or die "Failed opening $path: $!"; 393 | print $fh $content; 394 | close $fh; 395 | } 396 | 397 | =item is_installed_package($package_name) 398 | 399 | Check whether a given package is installed. 400 | 401 | =cut 402 | 403 | sub is_installed_package { 404 | my ($package_name) = @_; 405 | my $query_result = capture_string( 406 | 'dpkg-query', 407 | '-W', 408 | '-f', 409 | '${Status}', 410 | $package_name 411 | ); 412 | 413 | return ($query_result =~ /install ok installed/); 414 | } 415 | 416 | =item install_package($package_name) 417 | 418 | Ensure that a given package is installed. Should be idempotent. 419 | 420 | Returns a status string and, if action taken, an install log. 421 | 422 | =cut 423 | 424 | sub install_package { 425 | my ($package_name) = @_; 426 | 427 | return 'already-installed' 428 | if is_installed_package($package_name); 429 | 430 | my @install_log = capture_list( 431 | 'apt-get', 432 | '-y', 433 | 'install', 434 | $package_name 435 | ); 436 | 437 | return ('installed', @install_log); 438 | } 439 | 440 | =item uninstall_package($package_name) 441 | 442 | Ensure that a given package is not installed. Should be idempotent. 443 | 444 | Returns a status string and, if action taken, an uninstall log. 445 | 446 | =cut 447 | 448 | sub uninstall_package { 449 | my ($package_name) = @_; 450 | 451 | return 'already-uninstalled' 452 | unless is_installed_package($package_name); 453 | 454 | my @install_log = capture_list( 455 | 'apt-get', 456 | '-y', 457 | 'remove', 458 | $package_name 459 | ); 460 | 461 | return ('uninstalled', @install_log); 462 | } 463 | 464 | =item get_blurb() 465 | 466 | Return a useful blurb for inclusion in config file comments. 467 | 468 | =cut 469 | 470 | sub get_blurb { 471 | return "This file is managed by $OCCI_CONFIG"; 472 | } 473 | 474 | =item diag(@columns) 475 | 476 | Print columns of diagnostic output. 477 | 478 | =cut 479 | 480 | { 481 | # Cheesy retention of state: 482 | my @diag_stack = (); 483 | 484 | sub diag { 485 | my (@cols) = @_; 486 | # print "\t" x $depth; 487 | print join " :: ", (@diag_stack, @cols); 488 | print "\n"; 489 | } 490 | 491 | sub diag_push { 492 | my ($value) = @_; 493 | push @diag_stack, $value; 494 | } 495 | 496 | sub diag_pop { 497 | pop @diag_stack; 498 | } 499 | } 500 | 501 | =item have_config_or_exit($path) 502 | 503 | Check that a given config file exists, and exit with some documentation if not. 504 | 505 | =cut 506 | 507 | sub have_config_or_exit { 508 | my ($file) = @_; 509 | 510 | return 1 if -f $file; 511 | 512 | print STDERR <<"HELPTEXT"; 513 | It looks like you don't have a $OCCI_CONFIG yet. 514 | 515 | In order to create one: 516 | 517 | sudo nano $OCCI_CONFIG 518 | 519 | And then add configuration keys like: 520 | 521 | hostname=somepi 522 | 523 | See /usr/share/doc/occi/occidentalis_example.txt for a full example. 524 | HELPTEXT 525 | 526 | exit 0; 527 | } 528 | 529 | 530 | =back 531 | 532 | =head1 AUTHOR 533 | 534 | Brennen Bearnes 535 | Todd Treece 536 | 537 | =head1 COPYING 538 | 539 | The MIT License (MIT) 540 | 541 | Copyright (c) 2015 Adafruit Industries 542 | 543 | Permission is hereby granted, free of charge, to any person obtaining a copy 544 | of this software and associated documentation files (the "Software"), to deal 545 | in the Software without restriction, including without limitation the rights 546 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 547 | copies of the Software, and to permit persons to whom the Software is 548 | furnished to do so, subject to the following conditions: 549 | 550 | The above copyright notice and this permission notice shall be included in 551 | all copies or substantial portions of the Software. 552 | 553 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 554 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 555 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 556 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 557 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 558 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 559 | THE SOFTWARE. 560 | 561 | =cut 562 | -------------------------------------------------------------------------------- /usr/share/doc/occi/occidentalis_example.txt: -------------------------------------------------------------------------------- 1 | # In order to use this file, you can copy it to /boot/occidentalis.txt: 2 | # 3 | # sudo cp occidentalis_example.txt /boot/occidentalis.txt 4 | # 5 | # and change the options on each line. 6 | # 7 | # Lines with a leading "#" are comments. Blank lines are ignored. 8 | 9 | # set hostname 10 | hostname=somepi 11 | 12 | # configure a wireless network 13 | wifi_ssid=somewifinetwork 14 | wifi_password=somepassword 15 | -------------------------------------------------------------------------------- /usr/share/man/man1/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Adafruit-Occi/a4d5c50a45e2c1079692ac2a52907935f34836eb/usr/share/man/man1/.gitkeep --------------------------------------------------------------------------------