proxmox_cloud-init.php 14 KB

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