proxmox_cloud-init.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. <?php
  2. /**
  3. * cloud-init script for pfSense on Proxmox VE
  4. *
  5. * The script does a pfSense configuration in that manner:
  6. * 1. looks for an available cloud-init drive and mount it to /etc/cloud-init
  7. * 2. compares the cloud-init files on the cloud-init drive by a local copy placed in /etc/cloud-init
  8. * and ends without changing anything if the files on both location are similar
  9. * 3. parses the cloud-init 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_cloud-init.php also to /usr/local/sbin
  14. * HowTo use:
  15. * 1. attach a cloud-init 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. $cloudInitFiles = array('meta-data', 'network-config', 'user-data'); // provided by Proxmox
  25. $cloudInitLocalPath = '/etc/cloud-init';
  26. $cloudInitMountPoint = '/mnt/cloud-init';
  27. $changesDetected = false;
  28. /**
  29. * compares the cloud-init files
  30. *
  31. * conpares the cloud-init drive by a local copy and
  32. * update the local copy in case of changes
  33. * or if the local copy does not exist
  34. *
  35. * @param array $cloudInitFiles an array of filenames created by Proxmox
  36. * @param string $cloudInitLocalPath the local path to place the copy of cloud-init files
  37. * @param string $cloudInitMountPoint the location where the cloud-init image is mounted
  38. * @return bool true in case of updates, false in case of all is up to date
  39. */
  40. function updateCloudInitFiles( $cloudInitFiles, $cloudInitLocalPath, $cloudInitMountPoint) {
  41. $cloudInitFileDiff = false;
  42. // create /etc/cloud-init if not exist
  43. if (!is_dir("$cloudInitLocalPath")) {
  44. if (!mkdir ( "$cloudInitLocalPath", 0770, TRUE)) {
  45. $sys_err= error_get_last();
  46. echo "cloud-init failed: $sys_err[type] $sys_err[message]\n";
  47. syslog(LOG_ERR,"cloud-init failed: $sys_err[type] $sys_err[message]");
  48. exit(1);
  49. }
  50. }
  51. // check for updated config files and update the local copy in case of differs
  52. foreach ( $cloudInitFiles as $cloudInitFile ) {
  53. if (!((sha1_file("$cloudInitLocalPath/$cloudInitFile")) == (sha1_file("$cloudInitMountPoint/$cloudInitFile")))) {
  54. if (!copy("$cloudInitMountPoint/$cloudInitFile", "$cloudInitLocalPath/$cloudInitFile")) {
  55. $sys_err = error_get_last();
  56. syslog(LOG_ERR,"cloud-init failed: $sys_err[type] $sys_err[message]");
  57. exit(1);
  58. } else {
  59. $cloudInitFileDiff = true;
  60. }
  61. }
  62. }
  63. return $cloudInitFileDiff;
  64. }
  65. /**
  66. * probes existence of all necessary cloud-init files
  67. *
  68. * @param array $cloudInitFiles an array of filenames created by Proxmox
  69. * @param string $cloudInitPath the location where the cloud-init files should be
  70. * @return bool true in case of all files are in place or false if something missing
  71. */
  72. function checkCloudInitFiles( $cloudInitFiles, $cloudInitPath) {
  73. foreach($cloudInitFiles as $cloudInitFile) {
  74. if (!file_exists("$cloudInitPath/$cloudInitFile")) {
  75. return false;
  76. }
  77. }
  78. return true;
  79. }
  80. /**
  81. * search for a cloud-init drive
  82. *
  83. * probes any attached cd device for existing cloud-init files
  84. * mounts the drive to /mnt/cloud-init and probes that all neccessary cloud-init files exist
  85. *
  86. * @param array $cloudInitFiles an array of filenames created by Proxmox
  87. * @param string $cloudInitMountPoint the mountpoint for cloud-init drive
  88. * @return bool true in case of success, fals in case of no cloud-init drive could found
  89. */
  90. function checkCloudInitDevice( $cloudInitFiles, $cloudInitMountPoint) {
  91. // get attached cd devices
  92. preg_match_all( "/.*cd[0-9] /", file_get_contents('/var/run/dmesg.boot'), $cdDeviceList);
  93. if (empty($cdDeviceList[0])) {
  94. syslog(LOG_ERR,"cloud-init failed: No cloud-init drive found");
  95. exit(1);
  96. }
  97. if(!is_dir($cloudInitMountPoint)) {
  98. if(!mkdir($cloudInitMountPoint)) {
  99. syslog(LOG_ERR,"cloud-init failed: Cloud not create mountpoint $cloudInitMountPoint");
  100. exit(1);
  101. }
  102. }
  103. // check cloud-init iso is mounted or try to mount the cloud-init medium
  104. foreach($cdDeviceList[0] as $cdDevice) {
  105. // is a cd mounted on /mnt/cloud-init ?
  106. $sys_err_msg = exec("df $cloudInitMountPoint | grep -q /dev/$cdDevice 2>&1", $sys_msg, $sys_err_no);
  107. if ($sys_err_no) { // not mounted
  108. // try to mount the cd
  109. $mount_err = exec("mount_cd9660 /dev/$cdDevice $cloudInitMountPoint", $sys_msg, $sys_err_no);
  110. if (!$sys_err_no) {
  111. if (checkCloudInitFiles( $cloudInitFiles, $cloudInitMountPoint)) {
  112. syslog(LOG_INFO,"cloud-init: found cloud init drive on $cdDevice mounted at $cloudInitMountPoint/");
  113. return true;
  114. } else {
  115. $umount_err = exec("umount $cloudInitMountPoint", $sys_msg, $sys_err_no);
  116. if ($sys_err_no) {
  117. syslog(LOG_ERR,"cloud-init: mounted a wrong device $cdDevice but not able to umount because $sys_msg");
  118. return false;
  119. }
  120. }
  121. }
  122. } else { //already mounted (but not by us)
  123. if (checkCloudInitFiles( $cloudInitFiles, $cloudInitMountPoint)) {
  124. syslog(LOG_INFO,"cloud-init: found cloud init drive on $cdDevice at $cloudInitMountPoint/");
  125. return true;
  126. } else {
  127. syslog(LOG_ERR,"cloud-init: expected files on cloud init drive not found");
  128. return false;
  129. }
  130. }
  131. }
  132. return false;
  133. }
  134. /**
  135. * does a case insensitive search for a network device by given hardware address
  136. *
  137. * @param string $mac hardware address
  138. * @return string $if[1] device name or false if no device could be found
  139. */
  140. function searchIfDevice( $mac) {
  141. exec("ifconfig -a | awk '/^[a-z]/ { gsub(/\:/,\"\", $1); iface=$1; next } /hwaddr/ { mac=$2; print mac, iface}'", $ifMacList, $sys_err_no);
  142. foreach($ifMacList as $ifMac) {
  143. $if = explode(" ",$ifMac);
  144. if (strcasecmp("$if[0]", "$mac") == 0) { // case insensitive
  145. return $if[1];
  146. }
  147. }
  148. return false;
  149. }
  150. // search and mount the cloud-init image or exit 1
  151. if (!checkCloudInitDevice( $cloudInitFiles, $cloudInitMountPoint)) {
  152. syslog(LOG_ERR,"cloud-init: no cloud init drive available, skipping");
  153. exit(1);
  154. }
  155. // update the local copy of cloud-init files if there are any changes
  156. if (updateCloudInitFiles( $cloudInitFiles, $cloudInitLocalPath, $cloudInitMountPoint)) {
  157. syslog(LOG_INFO,"cloud-init: cloud init files updated");
  158. $changesDetected = true;
  159. }
  160. // parse cloud init configurations
  161. // $metaData = Spyc::YAMLLoad("$cloudInitLocalPath/$cloudInitFiles[0]"); // meta-data (actually not in use)
  162. $netData = Spyc::YAMLLoad("$cloudInitLocalPath/$cloudInitFiles[1]"); // network-config
  163. $userData = Spyc::YAMLLoad("$cloudInitLocalPath/$cloudInitFiles[2]"); // user-data
  164. // configure nameserver if set
  165. $ifLastNr=(count($netData['config'])-1); // the YAML parser reurns a crappy array like this
  166. if (reset($netData['config'][$ifLastNr]) == 'nameserver') { // (
  167. next($netData['config'][$ifLastNr]); // [type] => nameserver
  168. $dnsServerCount = 0; // [address] =>
  169. while($nameserverIP=next($netData['config'][$ifLastNr])) { // [0] => 1.2.3.4
  170. if ($nameserverIP != $config['system']['dnsserver'][$dnsServerCount]) { // [1] => 4.3.2.1
  171. $config['system']['dnsserver'][$dnsServerCount] = $nameserverIP; // [search] =>
  172. $changesDetected = true; // [2] => mydomain.local
  173. } // )
  174. $dnsServerCount++;
  175. }
  176. if (next($netData['config'][$ifLastNr]) != $config['system']['domain']) {
  177. $config['system']['domain'] = current($netData['config'][$ifLastNr]);
  178. $changesDetected = true;
  179. }
  180. }
  181. // configure WAN interface
  182. $wanDevice = searchIfDevice( $netData['config'][0]['mac_address']);
  183. if (!$wanDevice) {
  184. syslog(LOG_ERR,"cloud-init: no WAN device found");
  185. exit(1);
  186. } else {
  187. if ($wanDevice != $config['interfaces']['wan']['if']) {
  188. $config['interfaces']['wan']['if'] = $wanDevice;
  189. $changesDetected = true;
  190. }
  191. }
  192. if ($netData['config'][0][0]['type'] == 'static') {
  193. if ($netData['config'][0][0]['address'] != $config['interfaces']['wan']['ipaddr']) {
  194. $config['interfaces']['wan']['ipaddr'] = $netData['config'][0][0]['address'];
  195. $changesDetected = true;
  196. }
  197. if ((32 - log((ip2long($netData['config'][0][0]['netmask']) ^ ip2long('255.255.255.255')) + 1 ,2)) != $config['interfaces']['wan']['subnet']) {
  198. $config['interfaces']['wan']['subnet'] = 32 - log((ip2long($netData['config'][0][0]['netmask']) ^ ip2long('255.255.255.255')) + 1 ,2);
  199. $changesDetected = true;
  200. }
  201. if ($netData['config'][0][0]['gateway'] != $config['interfaces']['wan']['gateway']) {
  202. $config['interfaces']['wan']['gateway'] = $netData['config'][0][0]['gateway'];
  203. $changesDetected = true;
  204. }
  205. }
  206. // configure primary LAN device
  207. $lanDevice = searchIfDevice( $netData['config'][1]['mac_address']);
  208. if (!$lanDevice) {
  209. syslog(LOG_ERR,"cloud-init: no LAN device found");
  210. exit(1);
  211. } else {
  212. if ($lanDevice != $config['interfaces']['lan']['if']) {
  213. $config['interfaces']['lan']['if'] = $lanDevice;
  214. $changesDetected = true;
  215. }
  216. }
  217. if ($netData['config'][1][0]['type'] == 'static') {
  218. if ($netData['config'][1][0]['address'] != $config['interfaces']['lan']['ipaddr']) {
  219. $config['interfaces']['lan']['ipaddr'] = $netData['config'][1][0]['address'];
  220. $changesDetected = true;
  221. }
  222. if ((32 - log((ip2long($netData['config'][1][0]['netmask']) ^ ip2long('255.255.255.255')) + 1 ,2)) != $config['interfaces']['lan']['subnet']) {
  223. $config['interfaces']['lan']['subnet'] = 32 - log((ip2long($netData['config'][1][0]['netmask']) ^ ip2long('255.255.255.255')) + 1 ,2);
  224. $changesDetected = true;
  225. }
  226. if ($netData['config'][1][0]['gateway'] != $config['interfaces']['lan']['gateway']) {
  227. $config['interfaces']['lan']['gateway'] = $netData['config'][1][0]['gateway'];
  228. $changesDetected = true;
  229. }
  230. }
  231. // configure additional network devices
  232. if ($ifLastNr > 2) {
  233. for ($ifNr=2;$ifNr<$ifLastNr;$ifNr++) {
  234. $optDeviceName = "opt" . strval($ifNr-1);
  235. $optDevice = searchIfDevice( $netData['config'][$ifNr]['mac_address']);
  236. if (!$optDevice) {
  237. syslog(LOG_WARN,"cloud-init: given network device {$netData['config'][$ifNr]['mac_address']} not found");
  238. break;
  239. } else {
  240. if ($optDevice != $config['interfaces'][$optDeviceName]['if']) {
  241. $config['interfaces'][$optDeviceName]['if'] = $optDevice;
  242. $changesDetected = true;
  243. }
  244. }
  245. if ($netData['config'][$ifNr][0]['type'] == 'static') {
  246. if ($netData['config'][$ifNr][0]['address'] != $config['interfaces'][$optDeviceName]['ipaddr']) {
  247. $config['interfaces'][$optDeviceName]['ipaddr'] = $netData['config'][$ifNr][0]['address'];
  248. $changesDetected = true;
  249. }
  250. if ((32 - log((ip2long($netData['config'][$ifNr][0]['netmask']) ^ ip2long('255.255.255.255')) + 1 ,2)) != $config['interfaces'][$optDeviceName]['subnet']) {
  251. $config['interfaces'][$optDeviceName]['subnet'] = 32 - log((ip2long($netData['config'][$ifNr][0]['netmask']) ^ ip2long('255.255.255.255')) + 1 ,2);
  252. $changesDetected = true;
  253. }
  254. if ($netData['config'][$ifNr][0]['gateway'] != $config['interfaces'][$optDeviceName]['gateway']) {
  255. $config['interfaces'][$optDeviceName]['gateway'] = $netData['config'][$ifNr][0]['gateway'];
  256. $changesDetected = true;
  257. }
  258. }
  259. }
  260. }
  261. // add ssh keys
  262. if (isset($userData['ssh_authorized_keys'])) {
  263. foreach ($userData[ssh_authorized_keys] as $sshKey) {
  264. $sshKeys .= "$sshKey\n";
  265. }
  266. if ((base64_encode("$sshKeys")) != $config['system']['user'][0]['authorizedkeys']) {
  267. $config['system']['user'][0]['authorizedkeys'] = base64_encode("$sshKeys");
  268. $changesDetected = true;
  269. }
  270. }
  271. if ($userData['hostname'] != $config['system']['hostname']) {
  272. $config['system']['hostname'] = $userData['hostname'];
  273. $changesDetected = true;
  274. }
  275. if ($userData['password'] != $config['system']['user'][0]['bcrypt-hash']) {
  276. $config['system']['user'][0]['bcrypt-hash'] = $userData['password'];
  277. $changesDetected = true;
  278. }
  279. if ($changesDetected == true) {
  280. // write the configuration
  281. write_config();
  282. // finally reboot the system
  283. system_reboot_sync();
  284. } else {
  285. syslog(LOG_INFO,"cloud-init: no changes detected");
  286. }