| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- <?php
- /**
- * cloudinit script for pfSense on Proxmox VE
- *
- * The script does a pfSense configuration in that manner:
- * 1. look for an available cloudinit drive and mount it to /mnt/cloud
- * 2. compares the cloudinit files on the cloudinit drive by a local copy placed in /etc/cloud
- * and ends without changing anything if the files on both location are similar
- * 3. parses the cloudinit YAML files and prepare and set the given values to pfSense config array
- * 4. write the pfSense configuration and reboot the instance
- *
- * HowTo install:
- * 1. get a copy of the YAML parse from https://github.com/mustangostang/spyc/ and place it to /usr/local/sbin
- * 2. place a copy of proxmox_cloudinit.php also to /usr/local/sbin
- * 3. place a startup script in /usr/local/etc/rc.d/
- *
- * Example startup script
- * ----------------------------------------------------------------------------
- * #!/bin/sh
- * # PROVIDE: cloudinit
- * # REQUIRE: FILESYSTEMS netif syslogd
- * # BEFORE: LOGIN
- *
- * . /etc/rc.subr
- *
- * name="cloudinit"
- *
- * start_cmd="${name}_start"
- * stop_cmd=":"
- *
- * load_rc_config $name
- * : ${cloudinit_enable:=yes}
- * : ${cloudinit_msg="Starting CloudInit."}
- *
- * cloudinit_start()
- * {
- * /usr/local/bin/php /usr/local/sbin/proxmox_cloud-init.php
- * }
- *
- * run_rc_command "$1"
- * ----------------------------------------------------------------------------
- *
- * HowTo use:
- * 1. attach a cloudinit cdrom to the pfSense VM an boot
- * 2. boot twice within 5 minutes to force a change run (don't count the automated reboots)
- * 3. boot thrice within 5 minutes between each reboot to force a restore to factory defaults (don't count the automated reboots)
- *
- * @version 0.9.1
- * @author Andre Genrich andre.genrich@thurdata.ch
- */
- require_once('Spyc.php'); // yaml parser
- require_once('config.inc'); // pfSense configuration structures
- require_once('system.inc'); // pfSense system utilities
- static $cloudInitFiles = array('meta-data', 'network-config', 'user-data'); // provided by Proxmox
- // tuneables
- static $cloudInitLocalPath = '/etc/cloud'; // place for $cloudInitControlFile, $cloudInitResetRequest and a local copy of $cloudInitFiles
- static $cloudInitMountPoint = '/mnt/cloud'; // mountpoint for cloudinit drive
- static $cloudInitControl = 'lastrun'; // file contains the timestamp of the last run or "init" after a change run
- static $cloudInitResetControl = 'emergency'; // file contains the timestamp of the last emergency run
- static $unstableRunTimeout = 300; // after $unstableRunTimeout the script assumes the system is stable
- /**
- * calcCIDR calculates a netmask in CIDR notation from dotted decimal notation
- *
- * @param string $mac netmask in dotted decimal notation
- * @return int $cidr netmask in CIDR notation
- */
- function calcCIDR( $mac) {
- $cidr = 32 - log((ip2long($mac) ^ ip2long('255.255.255.255')) + 1 ,2);
- return $cidr;
- }
- /**
- * createDir creates necessary directories
- *
- * @param string $path is the full path of the directory
- */
- function createDir( $path) {
- if (!is_dir("$path")) {
- if (!mkdir ( "$path", 0770, TRUE)) {
- $sys_err= error_get_last();
- syslog(LOG_ERR,"cloudinit: failed to create $path, error $sys_err[type] $sys_err[message]");
- exit(1);
- }
- }
- }
- /**
- * restoreConfiguration removes all local cloudinit files and starts a reset to factory defaults
- *
- */
- function restoreConfiguration() {
- global $cloudInitLocalPath, $cloudInitFiles, $cloudInitControl, $cloudInitResetControl;
- unlink("$cloudInitLocalPath/$cloudInitControl");
- unlink("$cloudInitLocalPath/$cloudInitResetControl");
- foreach ( $cloudInitFiles as $cloudInitFile) {
- unlink("$cloudInitLocalPath/$cloudInitFile");
- }
- reset_factory_defaults();
- system_reboot_sync();
- }
- /**
- * controlEmergencyRun check and update run control files
- *
- * $cloudInitControl contains "init" after a change run -> controlEmergencyRun outs the ciúrrent timestamp into $cloudInitControl
- *
- * @return int 0 to trigger a skip run
- * @return int 1 to trigger an emergency run (set again all cloudinit settings)
- * @return int 3 to trigger a reset to factory defaults run (reset all cloudinit & pfSense changes)
- */
- function controlEmergencyRun() {
- global $cloudInitLocalPath, $cloudInitControl, $cloudInitResetControl;
- if ((file_get_contents( "$cloudInitLocalPath/$cloudInitControl")) == "init") {
- file_put_contents( "$cloudInitLocalPath/$cloudInitControl", time());
- return 0;
- }
- if ((file_get_contents( "$cloudInitLocalPath/$cloudInitControl") + 300) > time()) {
- if (file_exists( "$cloudInitLocalPath/$cloudInitResetControl")) {
- if ((file_get_contents( "$cloudInitLocalPath/$cloudInitResetControl") + 300) > time()) {
- unlink( "$cloudInitLocalPath/$cloudInitResetControl");
- return 2;
- } else {
- unlink( "$cloudInitLocalPath/$cloudInitResetControl");
- return 1;
- }
- } else {
- file_put_contents( "$cloudInitLocalPath/$cloudInitResetControl", time());
- return 1;
- }
- } else {
- unlink( "$cloudInitLocalPath/$cloudInitResetControl");
- file_put_contents( "$cloudInitLocalPath/$cloudInitControl", time());
- return 0;
- }
- }
- /**
- * updateCloudInitFiles compares the cloudinit files
- *
- * conpares the cloudinit drive by a local copy and
- * update the local copy in case of changes
- * or if the local copy does not exist
- *
- * @return bool true in case of updates, false in case of all is up to date
- */
- function updateCloudInitFiles() {
- global $cloudInitMountPoint, $cloudInitLocalPath, $cloudInitFiles;
- $cloudInitFileDiff = false;
- // check for updated config files and update the local copy in case of differs
- foreach ( $cloudInitFiles as $cloudInitFile ) {
- if (!((sha1_file("$cloudInitLocalPath/$cloudInitFile")) == (sha1_file("$cloudInitMountPoint/$cloudInitFile")))) {
- if (!copy("$cloudInitMountPoint/$cloudInitFile", "$cloudInitLocalPath/$cloudInitFile")) {
- $sys_err = error_get_last();
- syslog(LOG_ERR,"cloudinit failed: $sys_err[type] $sys_err[message]");
- exit(1);
- } else {
- $cloudInitFileDiff = true;
- }
- }
- }
- return $cloudInitFileDiff;
- }
- /**
- * checkCloudInitFiles probes existence of all necessary cloudinit files
- *
- * @return bool true in case of all files are in place or false if someone missing
- */
- function checkCloudInitFiles() {
- global $cloudInitMountPoint, $cloudInitFiles;
- foreach($cloudInitFiles as $cloudInitFile) {
- if (!file_exists("$cloudInitMountPoint/$cloudInitFile")) {
- return false;
- }
- }
- return true;
- }
- /**
- * checkCloudInitDevice search for a cloudinit drive
- *
- * probes any attached cd device for existing cloudinit files
- * mounts the drive to /mnt/cloudinit and probes that all neccessary cloudinit files exist
- *
- * @return bool true in case of success, fals in case of no cloudinit drive could found
- */
- function checkCloudInitDevice() {
- global $cloudInitMountPoint, $cloudInitFiles;
- // get attached cd devices
- preg_match_all( "/.*cd[0-9] /", file_get_contents('/var/run/dmesg.boot'), $cdDeviceList);
- if (empty($cdDeviceList[0])) {
- syslog(LOG_ERR,"cloudinit: no cloudinit drive found");
- exit(1);
- }
- // check cloudinit iso is mounted or try to mount the cloudinit medium
- foreach($cdDeviceList[0] as $cdDevice) {
- // is a cd mounted on /mnt/cloudinit ?
- $sys_err_msg = exec("df $cloudInitMountPoint | grep -q /dev/$cdDevice 2>&1", $sys_msg, $sys_err_no);
- if ($sys_err_no) { // not mounted
- // try to mount the cd
- $mount_err = exec("mount_cd9660 /dev/$cdDevice $cloudInitMountPoint", $sys_msg, $sys_err_no);
- if (!$sys_err_no) {
- if (checkCloudInitFiles( $cloudInitMountPoint, $cloudInitFiles)) {
- syslog(LOG_INFO,"cloudinit: found cloud init drive on $cdDevice mounted at $cloudInitMountPoint/");
- return true;
- } else {
- $umount_err = exec("umount $cloudInitMountPoint", $sys_msg, $sys_err_no);
- if ($sys_err_no) {
- syslog(LOG_ERR,"cloudinit: mounted a wrong device $cdDevice but not able to umount because $sys_msg");
- return false;
- }
- }
- }
- } else { //already mounted (but not by us)
- if (checkCloudInitFiles( $cloudInitMountPoint, $cloudInitFiles)) {
- syslog(LOG_INFO,"cloudinit: found cloud init drive on $cdDevice at $cloudInitMountPoint/");
- return true;
- } else {
- syslog(LOG_ERR,"cloudinit: expected files on cloud init drive not found");
- return false;
- }
- }
- }
- return false;
- }
- /**
- * searchIfDevice does a case insensitive search for a network device by given hardware address
- *
- * @param string $mac hardware address
- * @return string $if[1] device name or false if no device could be found
- */
- function searchIfDevice( $mac) {
- exec("ifconfig -a | awk '/^[a-z]/ { gsub(/\:/,\"\", $1); iface=$1; next } /hwaddr/ { mac=$2; print mac, iface}'", $ifMacList, $sys_err_no);
- foreach($ifMacList as $ifMac) {
- $if = explode(" ",$ifMac);
- if (strcasecmp("$if[0]", "$mac") == 0) { // case insensitive
- return $if[1];
- }
- }
- return false;
- }
- // create mountpoint for cloudinit drive if not exist
- createDir( $cloudInitMountPoint);
- // create local folder for config & control files
- createDir( $cloudInitLocalPath);
- // search and mount the cloudinit image or exit 1
- if (!checkCloudInitDevice()) {
- syslog(LOG_ERR,"cloudinit: no cloud init drive available, skipping...\n");
- exit(1);
- }
- // probe for special run modes
- if (!updateCloudInitFiles()) {
- switch (controlEmergencyRun()) {
- case 0:
- syslog(LOG_INFO,"cloudinit: cloud init files up to date, skipping...\n");
- exit(0);
- break;
- case 1:
- syslog(LOG_INFO,"cloudinit: cloud init files up to date, but emergency run triggered...\n");
- break;
- case 2:
- syslog(LOG_INFO,"cloudinit: reset run triggered, restore cloudinit default configuration!\n");
- restoreConfiguration();
- break;
- }
- }
- // parse cloud init configurations
- // $metaData = Spyc::YAMLLoad("$cloudInitLocalPath/$cloudInitFiles[0]"); // meta-data (actually not in use)
- $netData = Spyc::YAMLLoad("$cloudInitLocalPath/$cloudInitFiles[1]"); // network-config
- $userData = Spyc::YAMLLoad("$cloudInitLocalPath/$cloudInitFiles[2]"); // user-data
- // configure nameserver if set
- $ifLastNr=(count($netData['config'])-1); // the YAML parser reurns a crappy array like this
- if (reset($netData['config'][$ifLastNr]) == 'nameserver') { // (
- next($netData['config'][$ifLastNr]); // [type] => nameserver
- $dnsServerCount = 0; // [address] =>
- while($nameserverIP=next($netData['config'][$ifLastNr])) { // [0] => 1.2.3.4
- $config['system']['dnsserver'][$dnsServerCount] = $nameserverIP; // [1] => 4.3.2.1
- $dnsServerCount++; // [search] =>
- } // [2] => mydomain.local
- $config['system']['domain'] = next($netData['config'][$ifLastNr]); // )
- }
- // configure WAN interface
- $wanDevice = searchIfDevice( $netData['config'][0]['mac_address']);
- if (!$wanDevice) {
- syslog(LOG_ERR,"cloudinit: no WAN device found");
- exit(1);
- } else {
- $config['interfaces']['wan']['if'] = $wanDevice;
- }
- if ($netData['config'][0][0]['type'] == 'static') {
- $config['interfaces']['wan']['ipaddr'] = $netData['config'][0][0]['address'];
- $config['interfaces']['wan']['subnet'] = calcCIDR( $netData['config'][0][0]['netmask']);
- $config['interfaces']['wan']['gateway'] = $netData['config'][0][0]['gateway'];
- } elseif ($netData['config'][0][0]['type'] == 'dhcp4') {
- $config['interfaces']['wan']['ipaddr'] = 'dhcp';
- unset( $config['interfaces']['wan']['subnet']);
- unset( $config['interfaces']['wan']['gateway']);
- }
- // configure primary LAN device
- $lanDevice = searchIfDevice( $netData['config'][1]['mac_address']);
- if (!$lanDevice) {
- syslog(LOG_ERR,"cloudinit: no LAN device found");
- exit(1);
- } else {
- $config['interfaces']['lan']['if'] = $lanDevice;
- }
- if ($netData['config'][1][0]['type'] == 'static') {
- $config['interfaces']['lan']['ipaddr'] = $netData['config'][1][0]['address'];
- $config['interfaces']['lan']['subnet'] = calcCIDR( $netData['config'][1][0]['netmask']);
- $config['interfaces']['lan']['gateway'] = $netData['config'][1][0]['gateway'];
- }
- // configure additional network devices
- if ($ifLastNr > 2) {
- for ($ifNr=2;$ifNr<$ifLastNr;$ifNr++) {
- $optDeviceName = "opt" . strval($ifNr-1);
- $optDevice = searchIfDevice( $netData['config'][$ifNr]['mac_address']);
- if (!$optDevice) {
- syslog(LOG_WARN,"cloudinit: given network device {$netData['config'][$ifNr]['mac_address']} not found");
- break;
- } else {
- $config['interfaces'][$optDeviceName]['if'] = $optDevice;
- }
- if ($netData['config'][$ifNr][0]['type'] == 'static') {
- $config['interfaces'][$optDeviceName]['ipaddr'] = $netData['config'][$ifNr][0]['address'];
- $config['interfaces'][$optDeviceName]['subnet'] = calcCIDR( $netData['config'][$ifNr][0]['netmask']);
- $config['interfaces'][$optDeviceName]['gateway'] = $netData['config'][$ifNr][0]['gateway'];
- }
- }
- }
- // add ssh keys
- if (isset($userData['ssh_authorized_keys'])) {
- foreach ($userData[ssh_authorized_keys] as $sshKey) {
- $sshKeys .= "$sshKey\n";
- }
- $config['system']['user'][0]['authorizedkeys'] = base64_encode("$sshKeys");
- }
- $config['system']['hostname'] = $userData['hostname'];
- $config['system']['user'][0]['bcrypt-hash'] = $userData['password'];
- // write the configuration
- write_config();
- // update runmode control file
- file_put_contents( "$cloudInitLocalPath/$cloudInitControl", "init");
- // skip pfSense wizard
- unlink("/conf/trigger_initial_wizard");
- // finally reboot the system
- system_reboot_sync();
|