proxmox_cloud-init.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. <?php
  2. /**
  3. * cloudinit script for pfSense on Proxmox VE
  4. *
  5. * The script does a pfSense configuration in that manner:
  6. * 1. looks for an available cloudinit drive and mount it to /etc/cloudinit
  7. * 2. compares the cloudinit files on the cloudinit drive by a local copy placed in /etc/cloudinit
  8. * and ends without changing anything if the files on both location are similar
  9. * 3. parses the cloudinit YAML files and prepare and set the given values to pfSense config array
  10. * 4. write the pfSense configuration and reboot the instance
  11. * HowTo install:
  12. * 1. get a copy of the YAML parse from https://github.com/mustangostang/spyc/ and place it to /usr/local/sbin
  13. * 2. place a copy of proxmox_cloudinit.php also to /usr/local/sbin
  14. * HowTo use:
  15. * 1. attach a cloudinit drive to the pfSense VM
  16. * 2. create a startupscript
  17. *
  18. * @version 0.9
  19. * @author Andre Genrich andre.genrich@thurdata.ch
  20. */
  21. require_once('Spyc.php'); // yaml parser
  22. require_once('config.inc'); // pfSense configuration structures
  23. require_once('system.inc'); // pfSense system utilities
  24. static $cloudInitFiles = array('meta-data', 'network-config', 'user-data'); // provided by Proxmox
  25. // tuneables
  26. static $cloudInitLocalPath = '/etc/cloud'; // place for $cloudInitControlFile, $cloudInitResetRequest and a local copy of $cloudInitFiles
  27. static $cloudInitMountPoint = '/mnt/cloud'; // mountpoint for cloudinit drive
  28. static $cloudInitControl = 'lastrun'; // file contains the timestamp of the last run or "init" after a change run
  29. static $cloudInitResetControl = 'emergency'; // file contains the timestamp of the last emergency run
  30. static $unstableRunTimeout = 300; // after $unstableRunTimeout the script assumes the system is stable
  31. /**
  32. * calcCIDR calculates a netmask in CIDR notation from dotted decimal notation
  33. *
  34. * @param string $mac netmask in dotted decimal notation
  35. * @return int $cidr netmask in CIDR notation
  36. */
  37. function calcCIDR( $mac) {
  38. $cidr = 32 - log((ip2long($mac) ^ ip2long('255.255.255.255')) + 1 ,2);
  39. return $cidr;
  40. }
  41. /**
  42. * createDir creates necessary directories
  43. *
  44. * @param string $path is the full path of the directory
  45. */
  46. function createDir( $path) {
  47. if (!is_dir("$path")) {
  48. if (!mkdir ( "$path", 0770, TRUE)) {
  49. $sys_err= error_get_last();
  50. syslog(LOG_ERR,"cloudinit: failed to create $path, error $sys_err[type] $sys_err[message]");
  51. exit(1);
  52. }
  53. }
  54. }
  55. /**
  56. * restoreConfiguration removes all local cloudinit files and starts a reset to factory defaults
  57. *
  58. * @param string $cloudInitLocalPath
  59. * @param array $cloudInitFiles
  60. * @param string $cloudInitControl
  61. * @param string $cloudInitResetControl
  62. */
  63. function restoreConfiguration( $cloudInitLocalPath, $cloudInitFiles, $cloudInitControl, $cloudInitResetControl) {
  64. unlink("$cloudInitLocalPath/$cloudInitControl");
  65. unlink("$cloudInitLocalPath/$cloudInitResetControl");
  66. foreach ( $cloudInitFiles as $cloudInitFile) {
  67. unlink("$cloudInitLocalPath/$cloudInitFile");
  68. }
  69. reset_factory_defaults();
  70. system_reboot_sync();
  71. }
  72. /**
  73. * controlEmergencyRun check and update run control files
  74. *
  75. * $cloudInitControl contains "init" after a change run -> controlEmergencyRun outs the ciúrrent timestamp into $cloudInitControl
  76. *
  77. * @param string $cloudInitLocalPath
  78. * @param string $cloudInitControl
  79. * @param string $cloudInitResetControl
  80. *
  81. * @return int 0 to trigger a skip run
  82. * @return int 1 to trigger an emergency run (set again all cloudinit settings)
  83. * @return int 3 to trigger a reset to factory defaults run (reset all cloudinit & pfSense changes)
  84. */
  85. function controlEmergencyRun( $cloudInitLocalPath, $cloudInitControl, $cloudInitResetControl) {
  86. if ((file_get_contents( "$cloudInitLocalPath/$cloudInitControl")) == "init") {
  87. file_put_contents( "$cloudInitLocalPath/$cloudInitControl", time());
  88. return 0;
  89. }
  90. if ((file_get_contents( "$cloudInitLocalPath/$cloudInitControl") + 300) > time()) {
  91. if (file_exists( "$cloudInitLocalPath/$cloudInitResetControl")) {
  92. if ((file_get_contents( "$cloudInitLocalPath/$cloudInitResetControl") + 300) > time()) {
  93. unlink( "$cloudInitLocalPath/$cloudInitResetControl");
  94. return 2;
  95. } else {
  96. unlink( "$cloudInitLocalPath/$cloudInitResetControl");
  97. return 1;
  98. }
  99. } else {
  100. file_put_contents( "$cloudInitLocalPath/$cloudInitResetControl", time());
  101. return 1;
  102. }
  103. } else {
  104. unlink( "$cloudInitLocalPath/$cloudInitResetControl");
  105. file_put_contents( "$cloudInitLocalPath/$cloudInitControl", time());
  106. return 0;
  107. }
  108. }
  109. /**
  110. * updateCloudInitFiles compares the cloudinit files
  111. *
  112. * conpares the cloudinit drive by a local copy and
  113. * update the local copy in case of changes
  114. * or if the local copy does not exist
  115. *
  116. * @param string $cloudInitMountPoint
  117. * @param string $cloudInitLocalPath
  118. * @param array $cloudInitFiles
  119. *
  120. * @return bool true in case of updates, false in case of all is up to date
  121. */
  122. function updateCloudInitFiles( $cloudInitMountPoint, $cloudInitLocalPath, $cloudInitFiles) {
  123. $cloudInitFileDiff = false;
  124. // check for updated config files and update the local copy in case of differs
  125. foreach ( $cloudInitFiles as $cloudInitFile ) {
  126. if (!((sha1_file("$cloudInitLocalPath/$cloudInitFile")) == (sha1_file("$cloudInitMountPoint/$cloudInitFile")))) {
  127. if (!copy("$cloudInitMountPoint/$cloudInitFile", "$cloudInitLocalPath/$cloudInitFile")) {
  128. $sys_err = error_get_last();
  129. syslog(LOG_ERR,"cloudinit failed: $sys_err[type] $sys_err[message]");
  130. exit(1);
  131. } else {
  132. $cloudInitFileDiff = true;
  133. }
  134. }
  135. }
  136. return $cloudInitFileDiff;
  137. }
  138. /**
  139. * checkCloudInitFiles probes existence of all necessary cloudinit files
  140. *
  141. * @param string $cloudInitMountPoint
  142. * @param array $cloudInitFiles
  143. *
  144. * @return bool true in case of all files are in place or false if someone missing
  145. */
  146. function checkCloudInitFiles( $cloudInitMountPoint, $cloudInitFiles) {
  147. foreach($cloudInitFiles as $cloudInitFile) {
  148. if (!file_exists("$cloudInitMountPoint/$cloudInitFile")) {
  149. return false;
  150. }
  151. }
  152. return true;
  153. }
  154. /**
  155. * checkCloudInitDevice search for a cloudinit drive
  156. *
  157. * probes any attached cd device for existing cloudinit files
  158. * mounts the drive to /mnt/cloudinit and probes that all neccessary cloudinit files exist
  159. *
  160. * @param string $cloudInitMountPoint
  161. * @param array $cloudInitFiles
  162. *
  163. * @return bool true in case of success, fals in case of no cloudinit drive could found
  164. */
  165. function checkCloudInitDevice( $cloudInitMountPoint, $cloudInitFiles) {
  166. // get attached cd devices
  167. preg_match_all( "/.*cd[0-9] /", file_get_contents('/var/run/dmesg.boot'), $cdDeviceList);
  168. if (empty($cdDeviceList[0])) {
  169. syslog(LOG_ERR,"cloudinit: no cloudinit drive found");
  170. exit(1);
  171. }
  172. // check cloudinit iso is mounted or try to mount the cloudinit medium
  173. foreach($cdDeviceList[0] as $cdDevice) {
  174. // is a cd mounted on /mnt/cloudinit ?
  175. $sys_err_msg = exec("df $cloudInitMountPoint | grep -q /dev/$cdDevice 2>&1", $sys_msg, $sys_err_no);
  176. if ($sys_err_no) { // not mounted
  177. // try to mount the cd
  178. $mount_err = exec("mount_cd9660 /dev/$cdDevice $cloudInitMountPoint", $sys_msg, $sys_err_no);
  179. if (!$sys_err_no) {
  180. if (checkCloudInitFiles( $cloudInitMountPoint, $cloudInitFiles)) {
  181. syslog(LOG_INFO,"cloudinit: found cloud init drive on $cdDevice mounted at $cloudInitMountPoint/");
  182. return true;
  183. } else {
  184. $umount_err = exec("umount $cloudInitMountPoint", $sys_msg, $sys_err_no);
  185. if ($sys_err_no) {
  186. syslog(LOG_ERR,"cloudinit: mounted a wrong device $cdDevice but not able to umount because $sys_msg");
  187. return false;
  188. }
  189. }
  190. }
  191. } else { //already mounted (but not by us)
  192. if (checkCloudInitFiles( $cloudInitMountPoint, $cloudInitFiles)) {
  193. syslog(LOG_INFO,"cloudinit: found cloud init drive on $cdDevice at $cloudInitMountPoint/");
  194. return true;
  195. } else {
  196. syslog(LOG_ERR,"cloudinit: expected files on cloud init drive not found");
  197. return false;
  198. }
  199. }
  200. }
  201. return false;
  202. }
  203. /**
  204. * searchIfDevice does a case insensitive search for a network device by given hardware address
  205. *
  206. * @param string $mac hardware address
  207. * @return string $if[1] device name or false if no device could be found
  208. */
  209. function searchIfDevice( $mac) {
  210. exec("ifconfig -a | awk '/^[a-z]/ { gsub(/\:/,\"\", $1); iface=$1; next } /hwaddr/ { mac=$2; print mac, iface}'", $ifMacList, $sys_err_no);
  211. foreach($ifMacList as $ifMac) {
  212. $if = explode(" ",$ifMac);
  213. if (strcasecmp("$if[0]", "$mac") == 0) { // case insensitive
  214. return $if[1];
  215. }
  216. }
  217. return false;
  218. }
  219. // create mountpoint for cloudinit drive if not exist
  220. createDir( $cloudInitMountPoint);
  221. // create local folder for config & control files
  222. createDir( $cloudInitLocalPath);
  223. // search and mount the cloudinit image or exit 1
  224. if (!checkCloudInitDevice( $cloudInitMountPoint, $cloudInitFiles)) {
  225. syslog(LOG_ERR,"cloudinit: no cloud init drive available, skipping...\n");
  226. exit(1);
  227. }
  228. // probe for special run modes
  229. if (!updateCloudInitFiles( $cloudInitMountPoint, $cloudInitLocalPath, $cloudInitFiles)) {
  230. switch (controlEmergencyRun( $cloudInitLocalPath, $cloudInitControl, $cloudInitResetControl)) {
  231. case 0:
  232. syslog(LOG_INFO,"cloudinit: cloud init files up to date, skipping...\n");
  233. exit(0);
  234. break;
  235. case 1:
  236. syslog(LOG_INFO,"cloudinit: cloud init files up to date, but emergency run triggered...\n");
  237. break;
  238. case 2:
  239. syslog(LOG_INFO,"cloudinit: reset run triggered, restore cloudinit default configuration!\n");
  240. restoreConfiguration( $cloudInitLocalPath, $cloudInitFiles, $cloudInitControl, $cloudInitResetControl);
  241. break;
  242. }
  243. }
  244. // parse cloud init configurations
  245. // $metaData = Spyc::YAMLLoad("$cloudInitLocalPath/$cloudInitFiles[0]"); // meta-data (actually not in use)
  246. $netData = Spyc::YAMLLoad("$cloudInitLocalPath/$cloudInitFiles[1]"); // network-config
  247. $userData = Spyc::YAMLLoad("$cloudInitLocalPath/$cloudInitFiles[2]"); // user-data
  248. // configure nameserver if set
  249. $ifLastNr=(count($netData['config'])-1); // the YAML parser reurns a crappy array like this
  250. if (reset($netData['config'][$ifLastNr]) == 'nameserver') { // (
  251. next($netData['config'][$ifLastNr]); // [type] => nameserver
  252. $dnsServerCount = 0; // [address] =>
  253. while($nameserverIP=next($netData['config'][$ifLastNr])) { // [0] => 1.2.3.4
  254. $config['system']['dnsserver'][$dnsServerCount] = $nameserverIP; // [1] => 4.3.2.1
  255. $dnsServerCount++; // [search] =>
  256. } // [2] => mydomain.local
  257. $config['system']['domain'] = next($netData['config'][$ifLastNr]); // )
  258. }
  259. // configure WAN interface
  260. $wanDevice = searchIfDevice( $netData['config'][0]['mac_address']);
  261. if (!$wanDevice) {
  262. syslog(LOG_ERR,"cloudinit: no WAN device found");
  263. exit(1);
  264. } else {
  265. $config['interfaces']['wan']['if'] = $wanDevice;
  266. }
  267. if ($netData['config'][0][0]['type'] == 'static') {
  268. $config['interfaces']['wan']['ipaddr'] = $netData['config'][0][0]['address'];
  269. $config['interfaces']['wan']['subnet'] = calcCIDR( $netData['config'][0][0]['netmask']);
  270. $config['interfaces']['wan']['gateway'] = $netData['config'][0][0]['gateway'];
  271. } elseif ($netData['config'][0][0]['type'] == 'dhcp4') {
  272. $config['interfaces']['wan']['ipaddr'] = 'dhcp';
  273. unset( $config['interfaces']['wan']['subnet']);
  274. unset( $config['interfaces']['wan']['gateway']);
  275. }
  276. // configure primary LAN device
  277. $lanDevice = searchIfDevice( $netData['config'][1]['mac_address']);
  278. if (!$lanDevice) {
  279. syslog(LOG_ERR,"cloudinit: no LAN device found");
  280. exit(1);
  281. } else {
  282. $config['interfaces']['lan']['if'] = $lanDevice;
  283. }
  284. if ($netData['config'][1][0]['type'] == 'static') {
  285. $config['interfaces']['lan']['ipaddr'] = $netData['config'][1][0]['address'];
  286. $config['interfaces']['lan']['subnet'] = calcCIDR( $netData['config'][1][0]['netmask']);
  287. $config['interfaces']['lan']['gateway'] = $netData['config'][1][0]['gateway'];
  288. }
  289. // configure additional network devices
  290. if ($ifLastNr > 2) {
  291. for ($ifNr=2;$ifNr<$ifLastNr;$ifNr++) {
  292. $optDeviceName = "opt" . strval($ifNr-1);
  293. $optDevice = searchIfDevice( $netData['config'][$ifNr]['mac_address']);
  294. if (!$optDevice) {
  295. syslog(LOG_WARN,"cloudinit: given network device {$netData['config'][$ifNr]['mac_address']} not found");
  296. break;
  297. } else {
  298. $config['interfaces'][$optDeviceName]['if'] = $optDevice;
  299. }
  300. if ($netData['config'][$ifNr][0]['type'] == 'static') {
  301. $config['interfaces'][$optDeviceName]['ipaddr'] = $netData['config'][$ifNr][0]['address'];
  302. $config['interfaces'][$optDeviceName]['subnet'] = calcCIDR( $netData['config'][$ifNr][0]['netmask']);
  303. $config['interfaces'][$optDeviceName]['gateway'] = $netData['config'][$ifNr][0]['gateway'];
  304. }
  305. }
  306. }
  307. // add ssh keys
  308. if (isset($userData['ssh_authorized_keys'])) {
  309. foreach ($userData[ssh_authorized_keys] as $sshKey) {
  310. $sshKeys .= "$sshKey\n";
  311. }
  312. $config['system']['user'][0]['authorizedkeys'] = base64_encode("$sshKeys");
  313. }
  314. $config['system']['hostname'] = $userData['hostname'];
  315. $config['system']['user'][0]['bcrypt-hash'] = $userData['password'];
  316. // write the configuration
  317. write_config();
  318. // update runmode control file
  319. file_put_contents( "$cloudInitLocalPath/$cloudInitControl", "init");
  320. // skip pfSense wizard
  321. unlink("/conf/trigger_initial_wizard");
  322. // finally reboot the system
  323. system_reboot_sync();