├── COPYING ├── PURPOSE.md ├── README.md ├── bsdpserver.py └── docker ├── Dockerfile ├── Readme.md ├── bsdpy.service ├── nginx.conf ├── start.sh ├── storage └── Dockerfile ├── tftp.conf ├── tftpd.service └── unfs3.service /COPYING: -------------------------------------------------------------------------------- 1 | Apache License, Version 2.0 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 13 | 14 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 15 | 16 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 17 | 18 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 19 | 20 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 21 | 22 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 23 | 24 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 25 | 26 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 27 | 28 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 29 | 30 | 2. Grant of Copyright License. 31 | 32 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 33 | 34 | 3. Grant of Patent License. 35 | 36 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 37 | 38 | 4. Redistribution. 39 | 40 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 41 | 42 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 43 | You must cause any modified files to carry prominent notices stating that You changed the files; and 44 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 45 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 46 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 47 | 48 | 5. Submission of Contributions. 49 | 50 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 51 | 52 | 6. Trademarks. 53 | 54 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 55 | 56 | 7. Disclaimer of Warranty. 57 | 58 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 59 | 60 | 8. Limitation of Liability. 61 | 62 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 63 | 64 | 9. Accepting Warranty or Additional Liability. 65 | 66 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 67 | 68 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /PURPOSE.md: -------------------------------------------------------------------------------- 1 | **BSDPy** 2 | ========= 3 | 4 | ### A BSDP/Apple NetBoot server implemented in Python 5 | 6 | 7 | 8 | ### Purpose 9 | 10 | The purpose of this project is to implement a feature-complete NetBoot (BSDP) 11 | server using Python. It relies on the pydhcplib module (http://bit.ly/IGtv67) to 12 | handle BSDP requests from Apple Mac clients. 13 | 14 | Apple has long been the only game in town when it comes to providing NetBoot 15 | service. Other projects have been able to replicate partial functionality 16 | through ISC DHCPd and custom configurations, most notably JAMF’s NetSUS 17 | (https://github.com/jamf/NetSUS/). Some of the DHCPd-based solutions also 18 | require patching of the DHCPd source to work properly. What they have in common 19 | is that none are compatible with the OS X Startup Disk preference pane because 20 | it uses a randomized reply port instead of the standard port (68). 21 | 22 | **Note: **Instructions regarding installation as a system daemon will be added 23 | at a later date. Currently the service can be tested by running it from a CLI 24 | prompt. Some basic logging is written to STDOUT in this case. More complete 25 | logging to a logging facility is planned but not yet implemented. 26 | 27 | **WARNING:** As with any BSDP service, proper DNS functionality is required in 28 | order for it to work - the same requirements as Apple’s NetInstall Server. At a 29 | minimum this means having working forward lookups for the TFTP server which in 30 | the example below is the same as the BSDPy and NFS server. It is possible to 31 | separate these roles, which will be up to the individual admin to implement. 32 | Additionally you really want to have a static IP assignment for the BSDPy host, 33 | either through DHCP reservation or directly set. Things are guaranteed to be 34 | flakey or broken if either one of these is not working. 35 | 36 | ### Sample** **setup 37 | 38 | The following walkthrough assumes a base CentOS 6.4 (32 or 64-bit) install. I 39 | created mine using Vagrant. 40 | 41 | Install and start the TFTP and NFS services and clone the required repositories: 42 | 43 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 44 | $ sudo yum -y install gcc xinetd tftp-server nfs-utils nfs-utils-lib git-core python python-devel 45 | $ sudo sed -i 's/\/var\/lib\/tftpboot/\/nbi/' /etc/xinetd.d/tftp 46 | $ sudo sed -i 's/\-s//' /etc/xinetd.d/tftp 47 | $ sudo sed -i 's/SELINUX=.*/SELINUX=disabled/' /etc/sysconfig/selinux 48 | $ sudo sh -c 'echo "/nbi *(async,ro,no_root_squash,insecure)" >> /etc/exports' 49 | $ sudo mkdir /nbi 50 | $ sudo chkconfig --levels 235 nfs on 51 | $ sudo chkconfig --levels 235 xinetd on 52 | $ sudo chkconfig --levels 235 tftp on 53 | $ sudo chkconfig --levels 235 iptables off 54 | $ sudo service nfs start 55 | $ sudo service xinetd start 56 | $ git clone https://bitbucket.org/bruienne/bsdpy.git 57 | $ git clone https://github.com/bruienne/pydhcplib.git 58 | $ cd pydhcplib 59 | $ sudo python setup.py install 60 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 61 | 62 | Once the above is complete, one or more NBI bundles must be transferred to the 63 | server’s NetBoot service root path, **/nbi**. Assuming SSH is active, using scp 64 | to copy one or more images over would be straightforward: 65 | 66 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 67 | $ scp -r /Path/To/MyNetBoot.nbi user@bsdpyhost:/nbi 68 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 69 | 70 | Once one or more boot images have been transferred, verify that both TFTP and 71 | NFS work by testing their connectivity from a client that can reach the BSDPy 72 | server: 73 | 74 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 75 | $ showmount -e 76 | Export list for : 77 | /nbi * 78 | $ cd ~/; mkdir nbimount 79 | $ mount -t nfs :/nbi ~/nbimount 80 | $ ls ~/nbimount 81 | #Sample output 82 | DSR-1090.nbi NI2.nbi NI.nbi 83 | 84 | $ umount ~/nbimount 85 | $ tftp 86 | #Sample get command 87 | tftp> get /nbi/MyNetBoot.nbi/i386/booter 88 | Received 174997 bytes in 0.2 seconds 89 | tftp> quit 90 | $ ls -l booter 91 | #Sample output 92 | -rwxr-xr-x 1 root root 994464 May 15 2013 booter 93 | 94 | $ rm booter 95 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 96 | 97 | If TFTP and NFS check out successfully the BSDPy service can be started: 98 | 99 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 100 | $ cd bsdpy 101 | $ sudo bsdpserver.py 102 | #Sample output 103 | Using /nbi as root path 104 | ******************************************************** 105 | Got BSDP INFORM[LIST] packet: 106 | ================================================================= 107 | Return ACK[LIST] to 10.0.2.5 on 68 108 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 109 | 110 | By default BSDPy will assume the service root path is /nbi. If it is not, you 111 | can specify it in the CLI: 112 | 113 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 114 | $ sudo bsdpserver.py /mynbiroot 115 | #Sample output 116 | Using /mynbiroot as root path 117 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 118 | 119 | 120 | 121 | ### In depth 122 | 123 | BSDPy aims to offer the same functionality as Apple’s NetBoot server without 124 | relying on (Mac) OS X as its host OS. It is compatible with NetBoot Image (NBI) 125 | bundles as created by Apple’s System Image Utility, DeployStudio Server 126 | Assistant and those created by AutoNBI (https://bitbucket.org/bruienne/autonbi/) 127 | with other tools likely to be compatible too. A checklist of features follows 128 | below, with those not currently implemented in italic type: 129 | 130 | 131 | 132 | - Receive and process BSDP INFORM[LIST] requests from clients (broadcast) 133 | 134 | - Send BSDP ACK[LIST] responses to clients (unicast) containing: 135 | 136 | - One or more NBI sources 137 | 138 | - The default boot image’s identifier 139 | 140 | - The optional identifier of a boot image that was previously selected by 141 | the client 142 | 143 | - Receive and process BSDP INFORM[SELECT] requests from clients (broadcast) 144 | 145 | - Send BSDP ACK[SELECT] responses to clients (unicast) containing: 146 | 147 | - The booter’s TFTP server and its path on the server 148 | 149 | - The selected NBI’s boot image URL using NFS or *HTTP* 150 | 151 | - **Apply available NBI filtering based on Mac model ID or MAC address entries 152 | in NBImageInfo.plist** This feature is now implemented. Please test it out. 153 | 154 | - BSDP Option Codes: 155 | 156 | 1. **BSDP Message Type** (List, Select, Failed) 157 | 158 | 2. **BSDP Version** (Currently 1.1) 159 | 160 | 3. **BSDP Server Identifier** (IPv4 address) 161 | 162 | 4. *BSDP Server Priority - allows clients to pick the least busy server if 163 | more than one BSDP server replied with an ACK[LIST] packet containing 164 | the same NBI identifier (5000+)* 165 | 166 | 5. **BSDP Reply Port** (Used by OS X Startup Disk) 167 | 168 | 6. *BSDP Boot Image List Path (Unused in current BSDP implementation)* 169 | 170 | 7. **BSDP Default Boot Image** (Sent when client is booted using N-key) 171 | 172 | 8. **BSDP Selected Boot Image** (Sent by client to indicate requested 173 | image, *by server to indicate it has a record of an image previously 174 | selected by the client*) 175 | 176 | 9. **BSDP Boot Image List** (Sent by server to indicate available boot 177 | images) 178 | 179 | 10. *BSDP NetBoot 1.0 Firmware (Unused)* 180 | 181 | 11. *BSDP Boot Image Attributes Filter List (Sent by client to request a 182 | list filtered by the server based on defined attributes: 183 | Install/Non-install image, Mac OS 9/Mac OS X/Mac OS X Server/Hardware 184 | Diagnostics)* 185 | 186 | 12. **BSDP Maximum Message Size** (Sent by client to indicate the largest 187 | packet size it can interpret in addition to the size set by DHCP option 188 | 57) 189 | 190 | 191 | 192 | ### Copyright and licensing 193 | 194 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 195 | Copyright 2014 The Regents of the University of Michigan 196 | 197 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 198 | 199 | http://www.apache.org/licenses/LICENSE-2.0 200 | 201 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 202 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 203 | 204 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 205 | This software relies on a modified version of PyDhcpLib by Mathieu Ignacio (http://pydhcplib.tuxfamily.org/pmwiki/index.php) which was released under the GPL 3 license. 206 | 207 | The modified source may be obtained from the project page here: https://github.com/bruienne/pydhcplib and used and modified under the same terms as set forth in the full GNU General Public License 3. 208 | 209 | For the full GPL 3 license text see this link: http://www.gnu.org/licenses/gpl-3.0.txt 210 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 211 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BSDPy 1.0 2 | ========= 3 | 4 | BSDPy is a platform-independent Apple NetBoot (BSDP) service for organizations 5 | that have a need for Apple Mac NetBoot functionality but that lack the ability 6 | to support OS X server in order to implement it. 7 | 8 |   9 | 10 | General Functionality 11 | --------------------- 12 | 13 | The BSDPy service provides the same NetInstall feature set provided by Apple's 14 | OS X Server, which depending on the OS X version is either called "NetBoot" or 15 | "NetInstall". Management tools like DeployStudio and JAMF Casper which rely on a 16 | NetInstall-style image to launch an imaging client that writes a disk image to a 17 | local HD or SSD and performs post-imaging configuration are fully compatible 18 | with BSDPy. NetInstall-style images created by Apple's System Image Utility or 19 | any other tools that create a NetInstall-style NBI are also fully compatible. 20 | BSDPy does not currently support the less frequently used diskless NetBoot mode 21 | which relies on a shadow disk image to be mounted from an AFP share. Shadowing 22 | using a RAM disk or local storage is fully supported. 23 | 24 |   25 | 26 | Configuration 27 | ------------- 28 | 29 | To function, BSDPy needs to be given a valid network interface to listen on, the 30 | boot image network protocol to use and the boot image root path on the host. By 31 | default it uses **"eth0"**, **"HTTP"** and **"/nbi"** for these settings. 32 | Configuration of BSDPy is mainly done through environment variables due to its 33 | Docker-leaning deployment preference. A few basic items like aforementioned 34 | required network interface, boot image protocol and NBI root path can also be 35 | set using command line flags. The complete set of configuration items is as 36 | follows, with defaults in square brackets: 37 | 38 |   39 | 40 | ### Supported environment variables 41 | 42 | - **BSDPY\_IFACE** - Interface to listen on - *['eth0']* 43 | 44 | - **BSDPY\_PROTO** - Protocol to serve boot image - *['http']* 45 | 46 | - **BSDPY\_NBI\_PATH** - Root path to NBIs - *['/nbi']* 47 | 48 | - **BSDPY\_IP** - Public IP BSDPy listens on - (optional) 49 | 50 | - **BSDPY\_NBI\_URL** - Alternate base URL for boot images (HTTP/NFS) - 51 | (optional) 52 | 53 | - **BSDPY\_API\_URL** - API endpoint to obtain NBI entitlements - (optional) 54 | 55 | - **BSDPY\_API\_KEY** - API key to use in conjunction with BSDPY\_API\_URL - 56 | (required with API URL) 57 | 58 |   59 | 60 | ### Supported runtime flags 61 | 62 | - **-i** *['eth0']* - Interface to listen on 63 | 64 | - **-r** *['http']* - Protocol to serve boot image 65 | 66 | - **-p** *['/nbi']* - Root path to NBIs 67 | 68 |   69 | 70 | Modes of Operation 71 | ------------------ 72 | 73 |   74 | 75 | BSDPy has a few distinct modes of operation: 76 | 77 | 1. Self-contained, single-host mode 78 | 79 | 2. Separate NBI repository mode 80 | 81 | 3. API-connected mode 82 | 83 |   84 | 85 | Regardless of the mode of operation a functional TFTP service must be running on 86 | the same host as BSDPy. In addition to TFTP the self-contained mode also 87 | requires a properly configured HTTP or NFS service to be running. Specific 88 | configuration details regarding the TFTP, HTTP and NFS services will be covered 89 | in the **"Deployment methods"** section later in this document. 90 | 91 |   92 | 93 | ### Running self-contained 94 | 95 | When running in self-contained mode BSDPy and its NBI content and supporting 96 | services are located on the same host. This means that BSDP, TFTP and HTTP/NFS 97 | services are all running on the host with a local directory containing one or 98 | more NBI bundles. By default this directory is `/nbi`. 99 | 100 | Required settings (defaults in brackets): 101 | 102 | - **BSDPY\_IFACE** or **-i** *['eth0']* 103 | 104 | - **BSDPY\_PROTO** or **-r** *['http']* 105 | 106 | - **BSDPY\_NBI\_PATH** or **-p** *['/nbi']* 107 | 108 |   109 | 110 | Required **local** services: 111 | 112 | - **BSDPy** listening on port **67 UDP** 113 | 114 | - **TFTPD** listening on port **69 UDP** 115 | 116 | - **HTTPD** listening on port **80 TCP** or **NFSD** listening on ports 117 | **110**, **2049 UDP**/**TCP** 118 | 119 |   120 | 121 | ### Running with a separate NBI repository 122 | 123 | When running in this mode BSDPy will poll a filesystem location on the host it 124 | is running on for NBI bundles to offer. All files that are to be served by TFTP 125 | (booter, kernelcache) will also originate from this host. Once the client is 126 | ready to load the boot image (NetInstall.dmg) it will do so through a URI 127 | located on another host using either HTTP or NFS. The layout of the NBI bundle 128 | is expected to be the same between the BSDPy host and the remote host(s) since 129 | the URI as provided through the `BSDPY_NBI_URL` setting will be combined with 130 | the path of the NBI on the BSDPy host. 131 | 132 | Required settings (defaults in brackets): 133 | 134 | - **BSDPY\_IFACE** or **-i** *[eth0]* 135 | 136 | - **BSDPY\_PROTO** or **-r** *[http]* 137 | 138 | - **BSDPY\_NBI\_PATH** or **-p** *[/nbi]* 139 | 140 | - **BSDPY\_NBI\_URL** ("http://mynbirepo.org/netboot") - *Must be HTTP/port 141 | 80* 142 | 143 |   144 | 145 | Required **local** services: 146 | 147 | - **BSDPy** listening on port **67 UDP** 148 | 149 | - **TFTPD** listening on port **69 UDP** 150 | 151 |   152 | 153 | Required **remote** services: 154 | 155 | - **HTTPD** listening on port **80 TCP** or **NFSD** listening on ports 156 | **110**, **2049 UDP**/**TCP** 157 | 158 |   159 | 160 | In this example, the BSDPy host has NBI bundles stored at /nbi and parses them 161 | to offer to clients: 162 | 163 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 164 | bsdpy-host$ ls /nbi 165 | bsdpy-host$ 109-13E28.nbi 166 | bsdpy-host$ ls /nbi/109-13E28.nbi 167 | bsdpy-host$ NBImageInfo.plist NetInstall.dmg i386 168 | bsdpy-host$ ls /nbi/109-13E28.nbi/i386 169 | bsdpy-host$ PlatformSupport.plist booter com.apple.Boot.plist x86_64 170 | 171 | DEBUG: BSDPY_IP: 10.0.1.2 172 | DEBUG: BSDPY_NBI_PATH: /nbi 173 | DEBUG: BSDPY_IFACE: eth0 174 | DEBUG: BSDPY_PROTO: http 175 | DEBUG: BSDPY_NBI_URL: http://mynbirepo.org/netboot 176 | DEBUG: [========= Updating boot images list =========] 177 | DEBUG: Considering NBI source at /nbi/109-13E28.nbi 178 | DEBUG: /nbi/109-13E28.nbi 179 | DEBUG: [=========      End updated list     =========] 180 | DEBUG: [===== Using the following boot images =======] 181 | DEBUG: /nbi/109-13E28.nbi 182 | DEBUG: [======     End boot image listing      ======] 183 | DEBUG: -=============================================- 184 | 185 | 186 | 187 | DEBUG: Resolving BSDPY_NBI_URL to IP - mynbirepo.org -> 10.0.1.100 188 | DEBUG: Found BSDPY_NBI_URL - using basedmgpath http://10.0.1.100/netboot/ 189 | DEBUG: -========================================================================- 190 | DEBUG: Return ACK[SELECT] to 3c:1:de:ad:be:ef - 10.0.1.5 on port 68 191 | DEBUG: --> TFTP URI: tftp://10.0.1.2/nbi/109-13E28.nbi/i386/booter 192 | DEBUG: --> Boot Image URI: http://10.0.1.100/netboot/109-13E28.nbi/NetInstall.dmg 193 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 194 | 195 |   196 | 197 | The contents of the `i386` directory will be offered to the client via TFTP from 198 | the BSDPy host at `10.0.1.2`. Once the client completes loading the booter, 199 | com.apple.Boot.plist, PlatformSupport.plist and kernelcache files it will send a 200 | request to mount `NetInstall.dmg`. With `BSDPY_NBI_URL` set to 201 | `http://mynbirepo.org/netboot`, the request for `NetInstall.dmg` will now be 202 | sent as a URI made up of the contents of the environment variable (optionally 203 | converted to an IP address if a DNS hostname was used) and the relative path to 204 | NetInstall.dmg from the root directory set by `BSDPY_NBI_PATH` on the BSDPy 205 | host: 206 | 207 | `BSDPY_NBI_URL` = `http://10.0.1.100/netboot` 208 | 209 | \+ 210 | 211 | `BSDPY_NBI_PATH` = `109-13E28.nbi/NetInstall.dmg` 212 | 213 | = 214 | 215 | `http://10.0.1.100/netboot/109-13E28.nbi/NetInstall.dmg` 216 | 217 | The NBI repository host has the same NBI bundle(s) stored on disk and a **HTTP** 218 | service has been configured to serve their parent location at the URL set with 219 | `BSDPY_NBI_PATH`: `http://mynbirepo.org/netboot`. Now, when a request for 220 | `http://10.0.1.100/netboot/OSX109-13E28.nbi/NetInstall.dmg` is received from a 221 | client it is successfully handled by the HTTP service on the NBI repository 222 | server and the client mounts the image and boots. The same general 223 | behind-the-scenes process applies if BSDPy was configured to use **NFS** as boot 224 | image protocol instead, but the URI will instead look similar to this: 225 | `nfs:10.0.1.100/netboot/OSX109-13E28.nbi/NetInstall.dmg` - the NFS service 226 | should be configured to allow all connections to `mynbirepo.org:/netboot` in 227 | `/etc/exports`. 228 | 229 |   230 | 231 | ### Running with an API endpoint 232 | 233 | If BSDPy is run with `BSDPY_API_URL` set it will not poll the local filesystem 234 | for valid NBI bundles but instead send a **HTTP GET** request to the provided 235 | API endpoint for NBI entitlements. An API call is made for each client that 236 | sends a **BSDP LIST** request. At startup BSDPy will also make an API call to 237 | retrieve all available NBIs and cache all TFTP content (`booter`, 238 | `com.apple.Boot.plist`, `PlatformSupport.plist` and `kernelcache`) to the local 239 | TFTP root directory via HTTP or NFS, depending on `BSDPY_PROTO`. It is important 240 | that permissions for the NBI bundles on the remote server are set to allow 241 | downloading of all files below the `ImageName.nbi` directory level. A TFTP 242 | service running alongside BSDPy will serve the contents from this TFTP root 243 | directory to clients. Unless `BSDPY_NBI_PATH` is set the default location of the 244 | TFTP root directory is `/nbi`. 245 | 246 |   247 | 248 | Required settings (defaults in brackets): 249 | 250 | - **BSDPY\_IFACE** or **-i** *[eth0]* 251 | 252 | - **BSDPY\_API\_URL** ("http(s)://myapiserver.org:port/{SOME\_ENDPOINT}") 253 | 254 | - **BSDPY\_API\_KEY** ("SOMEAPIKEYHERE") 255 | 256 |   257 | 258 | Required **local** services: 259 | 260 | - **BSDPy** listening on port **67 UDP** 261 | 262 | - **TFTPD** listening on port **69 UDP** 263 | 264 |   265 | 266 | Required **remote** services: 267 | 268 | - **HTTPD** listening on port **80 TCP** or **NFSD** listening on ports 269 | **110**, **2049 UDP**/**TCP** 270 | 271 | - **HTTPD** listening on **any available port**, either **HTTP** or **HTTPS** 272 | 273 |   274 | 275 | ### API requests and responses 276 | 277 | When making the "all images" call the following parameter is sent in the **HTTP 278 | GET** request: 279 | 280 | - `all=True` - (boolean) 281 | 282 |   283 | 284 | When making the per-client API call the following parameters are sent in the 285 | **HTTP GET** request: 286 | 287 | - `ip_address` - A valid IP address (string) 288 | 289 | - `mac_address` - A valid MAC address, colon-separated (string) 290 | 291 | - `model_name` - A valid Apple model ID, e.g. `MacBookPro10,1` (string) 292 | 293 |   294 | 295 | The API response should be in JSON format, with a content type of 296 | `application/json`. The root object should be an array of objects named 297 | `images`. The following object keys are required for each valid image and should 298 | have a non-empty value: 299 | 300 | - `booter_url` - A valid absolute path to the booter (string) 301 | 302 | - `name` - A valid unique image name (string) 303 | 304 | - `priority` - A valid unique (1-1024/1025-4096) image ID (integer) 305 | 306 | - `root_dmg_url` - A valid HTTP or NFS URL to the boot image (string) 307 | 308 |   309 | 310 | A sample JSON response might look something like this: 311 | 312 |   313 | 314 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 315 | { 316 | "images":[ 317 | { 318 | "booter_url": "/nbi/10.10-14A389.nbi/i386/booter", 319 | "created_at": "2015-03-11T15:20:10-04:00", 320 | "id": 1, 321 | "name": "NetBoot 10.10", 322 | "priority": 2000, 323 | "root_dmg_url": "http://mynbirepo.org/netboot/10.10-14A389.nbi/NetInstall.dmg", 324 | "updated_at": "2015-03-11T15:25:46-04:00" 325 | }, 326 | { 327 | "booter_url": "/nbi/10.9-13E28.nbi/i386/booter", 328 | "created_at": "2015-03-11T15:20:10-04:00", 329 | "id": 2, 330 | "name": "NetBoot 10.9", 331 | "priority": 2001, 332 | "root_dmg_url": "http://mynbirepo.org/netboot/10.9-13E28.nbi/NetInstall.dmg", 333 | "updated_at": "2015-03-11T15:21:16-04:00" 334 | } 335 | ] 336 | } 337 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 338 | 339 |   340 | 341 | BSDPy currently uses the `name`, `booter_url`, `root_dmg_url` and `priority` 342 | keys to send a reply back to the client. Other fields may be added for your 343 | organization's needs as well but will be ignored by BSDPy. The `priority` key 344 | should be unique and additionally should be greater than **1024** if more than 345 | one BSDPy instance will be running on the same network segment for compatibility 346 | with the BSDP client's load balancing host selection mechanism. 347 | 348 | ![]() 349 | 350 | Linux run mode 351 | -------------- 352 | 353 | As of version 1.0 the recommended way to run the BSDPy service is as a Docker 354 | container, with supporting containers for the TFTP, HTTP and NFS services. It is 355 | also still possible to run the service directly on most modern Linux 356 | distributions but support for any distribution-specific implemenation is up to 357 | the individual sys admin. That said, the service does not have a very 358 | complicated set of requirements if run this way. 359 | 360 |   361 | 362 | ### Linux single host 363 | 364 | To run the BSDPy service from a single Linux host the required BSDPy settings, 365 | services and ports as outlined in the **Running self-contained **section earlier 366 | in this document can be used. Settings can be provided either via command line 367 | flags or environment variables. To recap, the `BSDPy` service, `TFTP` service 368 | and either `HTTP` or `NFS` services will all be running on the same host and 369 | connecting clients will be sent the same IP address or host name for all 370 | requests. 371 | 372 | A sample setup for CentOS 6.4 from an earlier version of this README follows. 373 | 374 |   375 | 376 | Install and start the TFTP and NFS services and clone the required repositories: 377 | 378 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 379 | $ sudo yum -y install gcc xinetd tftp-server nfs-utils nfs-utils-lib git-core python python-devel 380 | $ sudo sed -i 's/\/var\/lib\/tftpboot/\/nbi/' /etc/xinetd.d/tftp 381 | $ sudo sed -i 's/\-s//' /etc/xinetd.d/tftp 382 | $ sudo sed -i 's/SELINUX=.*/SELINUX=disabled/' /etc/sysconfig/selinux 383 | $ sudo sh -c 'echo "/nbi *(async,ro,no_root_squash,insecure)" >> /etc/exports' 384 | $ sudo mkdir /nbi 385 | $ sudo chkconfig --levels 235 nfs on 386 | $ sudo chkconfig --levels 235 xinetd on 387 | $ sudo chkconfig --levels 235 tftp on 388 | $ sudo chkconfig --levels 235 iptables off 389 | $ sudo service nfs start 390 | $ sudo service xinetd start 391 | $ git clone https://bitbucket.org/bruienne/bsdpy.git 392 | $ cd /bsdpy 393 | $ sudo pip install -r requirements.txt 394 | 395 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 396 | 397 | Once the above is complete, one or more NBI bundles must be transferred to the 398 | server’s NetBoot service root path, **/nbi**. Assuming SSH is active, using scp 399 | to copy one or more images over would be straightforward: 400 | 401 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 402 | $ scp -r /Path/To/MyNetBoot.nbi user@bsdpyhost:/nbi 403 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 404 | 405 | Once one or more boot images have been transferred, verify that both TFTP and 406 | NFS work by testing their connectivity from a client that can reach the BSDPy 407 | server: 408 | 409 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 410 | $ showmount -e 411 | Export list for : 412 | /nbi * 413 | $ cd ~/; mkdir nbimount 414 | $ mount -t nfs :/nbi ~/nbimount 415 | $ ls ~/nbimount 416 | #Sample output 417 | DSR-1090.nbi NI2.nbi NI.nbi 418 | 419 | $ umount ~/nbimount 420 | $ tftp 421 | #Sample get command 422 | tftp> get /nbi/MyNetBoot.nbi/i386/booter 423 | Received 174997 bytes in 0.2 seconds 424 | tftp> quit 425 | $ ls -l booter 426 | #Sample output 427 | -rwxr-xr-x 1 root root 994464 May 15 2013 booter 428 | 429 | $ rm booter 430 | 431 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 432 | 433 | If TFTP and NFS check out successfully the BSDPy service can be started: 434 | 435 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 436 | $ cd bsdpy 437 | $ sudo bsdpserver.py 438 | #Sample output 439 | 440 | DEBUG: BSDPY_NBI_PATH: /nbi 441 | DEBUG: BSDPY_IFACE: eth0 442 | DEBUG: BSDPY_PROTO: http 443 | DEBUG: [========= Updating boot images list =========] 444 | DEBUG: Considering NBI source at /nbi/109-13E28.nbi 445 | DEBUG: /nbi/109-13E28.nbi 446 | DEBUG: [=========      End updated list     =========] 447 | DEBUG: [===== Using the following boot images =======] 448 | DEBUG: /nbi/109-13E28.nbi 449 | DEBUG: [======     End boot image listing      ======] 450 | DEBUG: -=============================================- 451 | 452 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 453 | 454 | By default BSDPy will assume the service root path is /nbi. If it is not, you 455 | can specify it in the CLI: 456 | 457 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 458 | $ sudo bsdpserver.py -p /usr/share/netboot 459 | #Sample output 460 | 461 | DEBUG: BSDPY_NBI_PATH: /usr/share/netboot 462 | DEBUG: BSDPY_IFACE: eth0 463 | DEBUG: BSDPY_PROTO: http 464 | DEBUG: [========= Updating boot images list =========] 465 | DEBUG: Considering NBI source at /mynbiroot/109-13E28.nbi 466 | DEBUG: /usr/share/netboot/109-13E28.nbi 467 | DEBUG: [=========      End updated list     =========] 468 | DEBUG: [===== Using the following boot images =======] 469 | DEBUG: /usr/share/netboot/109-13E28.nbi 470 | DEBUG: [======     End boot image listing      ======] 471 | DEBUG: -=============================================- 472 | 473 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 474 | 475 |   476 | 477 | ### Linux multi-host 478 | 479 | To run the BSDPy service using multiple Linux hosts, for example to serve boot 480 | images from a dedicated file server or content delivery network, the required 481 | BSDPy settings, services and ports as outlined in the **Running with a separate 482 | NBI repository** section earlier in this document can be used. Settings can be 483 | provided either via command line flags or environment variables. To recap, the 484 | `BSDPy` service and `TFTP` service will be running on one host while one or more 485 | separate hosts will be running the `HTTP` or `NFS` service. The latter may 486 | implement whatever load balancing methodology is appropriate for the 487 | organization, keeping in mind that when the boot image is requested it will be 488 | done using an IP address that was resolved from a hostname, if configured 489 | through `BSDPY_NBI_URL`. 490 | 491 | Docker run mode 492 | --------------- 493 | 494 | ### Single Docker host 495 | 496 | ### Multiple Docker hosts 497 | 498 | -------------------------------------------------------------------------------- /bsdpserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ################################################################################ 3 | # Copyright 2015 The Regents of the University of Michigan 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | # use this file except in compliance with the License. You may obtain a copy of 7 | # the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations under 15 | # the License. 16 | # ############################################################################## 17 | # 18 | # BSDPy - A BSDP NetBoot server implemented in Python - v0.5b 19 | # 20 | # Author: Pepijn Bruienne - University of Michigan - bruienne@umich.edu 21 | # 22 | # Reasonably stable, test before using in production - you know the drill... 23 | # 24 | # Requirements: 25 | # 26 | # - Python 2.5 or later. Not tested with Python 3. 27 | # 28 | # - A Linux distribution that supports: 29 | # Python 2.5 or later 30 | # NFS service 31 | # TFTP service 32 | # 33 | # Tested on CentOS 6.4 and Ubuntu Precise 34 | # 35 | # - Working installation of this fork of the pydhcplib project: 36 | # 37 | # $ git clone https://github.com/bruienne/pydhcplib.git 38 | # $ cd pydhcplib 39 | # $ sudo python setup.py install 40 | # 41 | # The fork contains changes made to the names of DHCP options 43 and 60 which 42 | # are used in BSDP packets. The original library used duplicate names for 43 | # options other than 43 and 60 which breaks our script when they are looked 44 | # up through dhcp_constants.py. 45 | # 46 | # - Working DNS: 47 | # TFTP uses the DHCP 'sname' option to download the booter (kernel) - having 48 | # at least functioning forward DNS for the hostname in 'sname' is therefore 49 | # required. In a typical situation this would be the same server running the 50 | # BSDPy process, but this is not required. Just make sure that wherever TFTP 51 | # is running has working DNS lookup. 52 | # 53 | # - Root permissions. Due to its need to write to use raw sockets elevated 54 | # privileges are required. When run as a typical system service through init 55 | # or upstart this should not be an issue. 56 | # 57 | 58 | from pydhcplib.dhcp_packet import * 59 | from pydhcplib.dhcp_network import * 60 | from urlparse import urlparse 61 | 62 | import socket, struct, fcntl 63 | import os, fnmatch 64 | import plistlib 65 | import logging, optparse 66 | import signal, errno 67 | from docopt import docopt 68 | 69 | platform = sys.platform 70 | 71 | usage = """Usage: bsdpyserver.py [-p ] [-r ] [-i ] 72 | 73 | Run the BSDP server and handle requests from client. Optional parameters are 74 | the root path to serve NBIs from, the protocol to serve them with and the 75 | interface to run on. 76 | 77 | Options: 78 | -h --help This screen. 79 | -p --path The path to serve NBIs from. [default: /nbi] 80 | -r --proto The protocol to serve NBIs with. [default: http] 81 | -i --iface The interface to bind to. [default: eth0] 82 | """ 83 | 84 | logging.basicConfig(format='%(asctime)s - %(levelname)s: %(message)s', 85 | level=logging.DEBUG, 86 | filename='/var/log/bsdpserver.log', 87 | datefmt='%m/%d/%Y %I:%M:%S %p') 88 | 89 | 90 | # A dict that holds mappings of the BSDP option codes for lookup later on 91 | bsdpoptioncodes = {1: 'message_type', 92 | 2: 'version', 93 | 3: 'server_identifier', 94 | 4: 'server_priority', 95 | 5: 'reply_port', 96 | 6: 'image_icon_unused', 97 | 7: 'default_boot_image', 98 | 8: 'selected_boot_image', 99 | 9: 'boot_image_list', 100 | 10: 'netboot_v1', 101 | 11: 'boot_image_attributes', 102 | 12: 'max_message_size'} 103 | 104 | # Some standard DHCP/BSDP options to set listen, server ports and what IP 105 | # address to listen on. Default is 0.0.0.0 which means all requests are 106 | # replied to if they are a BSDP[LIST] or BSDP[SELECT] packet. 107 | netopt = {'client_listen_port':"68", 108 | 'server_listen_port':"67", 109 | 'listen_address':"0.0.0.0"} 110 | 111 | 112 | def get_ip(iface=''): 113 | """ 114 | The get_ip function retrieves the IP for the network interface BSDPY 115 | is running on. 116 | """ 117 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 118 | sockfd = sock.fileno() 119 | SIOCGIFADDR = 0x8915 120 | 121 | ifreq = struct.pack('16sH14s', iface, socket.AF_INET, '\x00' * 14) 122 | try: 123 | res = fcntl.ioctl(sockfd, SIOCGIFADDR, ifreq) 124 | except: 125 | return None 126 | ip = struct.unpack('16sH2x4s8x', res)[2] 127 | return socket.inet_ntoa(ip) 128 | 129 | arguments = docopt(usage, version='0.0.1') 130 | 131 | # Set the root path that NBIs will be served out of, either provided at 132 | # runtime or using a default if none was given. Defaults to /nbi. 133 | 134 | tftprootpath = arguments['--path'] 135 | bootproto = arguments['--proto'] 136 | serverinterface = arguments['--iface'] 137 | 138 | # Get the server IP and hostname for use in in BSDP calls later on. 139 | try: 140 | if os.environ.get('DOCKER_BSDPY_IP'): 141 | externalip = os.environ.get('DOCKER_BSDPY_IP') 142 | serverhostname = externalip 143 | serverip = map(int, externalip.split('.')) 144 | serverip_str = externalip 145 | logging.debug('Found $DOCKER_BSDPY_IP - using custom external IP %s' 146 | % externalip) 147 | elif 'darwin' in platform: 148 | from netifaces import ifaddresses 149 | logging.debug('Running on OS X, using alternate netifaces method') 150 | myip = ifaddresses(serverinterface)[2][0]['addr'] 151 | serverhostname = myip 152 | serverip = map(int, myip.split('.')) 153 | serverip_str = myip 154 | else: 155 | myip = get_ip(serverinterface) 156 | serverhostname = myip 157 | serverip = map(int, myip.split('.')) 158 | serverip_str = myip 159 | logging.debug('No BSDPY_IP env var found, using IP from %s interface' 160 | % serverinterface) 161 | if 'http' in bootproto: 162 | if os.environ.get('DOCKER_BSDPY_NBI_URL'): 163 | nbiurl = urlparse(os.environ.get('DOCKER_BSDPY_NBI_URL')) 164 | nbiurlhostname = nbiurl.hostname 165 | 166 | # EFI bsdp client doesn't do DNS lookup, so we must do it 167 | try: 168 | socket.inet_aton(nbiurlhostname) 169 | except socket.error: 170 | nbiurlhostname = socket.gethostbyname(nbiurlhostname) 171 | logging.debug('Resolving hostname to IP - %s -> %s' % (nbiurl.hostname, nbiurlhostname)) 172 | 173 | basedmgpath = 'http://%s%s/' % (nbiurlhostname, nbiurl.path) 174 | logging.debug('Found DOCKER_BSDPY_NBI_URL - using basedmgpath %s' % basedmgpath) 175 | else: 176 | basedmgpath = 'http://' + serverip_str + '/' 177 | logging.debug('Using HTTP basedmgpath %s' % basedmgpath) 178 | if 'nfs' in bootproto: 179 | basedmgpath = 'nfs:' + serverip_str + ':' + tftprootpath + ':' 180 | logging.debug('Using NFS basedmgpath %s' % basedmgpath) 181 | logging.debug('Server IP: ' + serverip_str + '\n' + 182 | 'Server FQDN: ' + serverhostname + '\n' + 183 | 'Serving on ' + serverinterface + '\n' + 184 | 'Using ' + bootproto + ' to serve boot image.\n') 185 | except: 186 | logging.debug('Error setting serverip, serverhostname or basedmgpath %s' % 187 | sys.exc_info()[1]) 188 | raise 189 | 190 | 191 | def getBaseDmgPath(nbiurl) : 192 | 193 | logging.debug('*********\nRefreshing basedmgpath because DOCKER_BSDPY_NBI_URL uses hostname, not IP') 194 | if 'http' in bootproto: 195 | if os.environ.get('DOCKER_BSDPY_NBI_URL'): 196 | nbiurlhostname = nbiurl.hostname 197 | 198 | # EFI bsdp client doesn't do DNS lookup, so we must do it 199 | try: 200 | socket.inet_aton(nbiurlhostname) 201 | except socket.error: 202 | nbiurlhostname = socket.gethostbyname(nbiurlhostname) 203 | logging.debug('Resolving hostname to IP - %s -> %s' % (nbiurl.hostname, nbiurlhostname)) 204 | 205 | basedmgpath = 'http://%s%s/' % (nbiurlhostname, nbiurl.path) 206 | logging.debug('Found DOCKER_BSDPY_NBI_URL - using basedmgpath %s\n*********\n' % basedmgpath) 207 | else: 208 | basedmgpath = 'http://' + serverip_str + '/' 209 | logging.debug('Using HTTP basedmgpath %s\n*********\n' % basedmgpath) 210 | 211 | if 'nfs' in bootproto: 212 | basedmgpath = 'nfs:' + serverip_str + ':' + tftprootpath + ':' 213 | logging.debug('Using NFS basedmgpath %s' % basedmgpath) 214 | 215 | return basedmgpath 216 | 217 | # Invoke the DhcpServer class from pydhcplib and configure it, overloading the 218 | # available class functions to only listen to DHCP INFORM packets, which is 219 | # what BSDP uses to do its thing - HandleDhcpInform(). 220 | # http://www.opensource.apple.com/source/bootp/bootp-268/Documentation/BSDP.doc 221 | 222 | 223 | class DhcpServer(DhcpNetwork) : 224 | def __init__(self, listen_address="0.0.0.0", 225 | client_listen_port=68,server_listen_port=67) : 226 | 227 | DhcpNetwork.__init__(self, 228 | listen_address, 229 | server_listen_port, 230 | client_listen_port) 231 | 232 | self.EnableBroadcast() 233 | if 'darwin' in platform: 234 | self.EnableReuseaddr() 235 | else: 236 | self.DisableReuseaddr() 237 | 238 | self.CreateSocket() 239 | self.BindToAddress() 240 | 241 | 242 | class Server(DhcpServer): 243 | def __init__(self, options): 244 | DhcpServer.__init__(self,options["listen_address"], 245 | options["client_listen_port"], 246 | options["server_listen_port"]) 247 | 248 | def HandleDhcpInform(self, packet): 249 | return packet 250 | 251 | 252 | def find(pattern, path): 253 | """ 254 | The find() function provides some basic file searching, used later 255 | to look for available NBIs. 256 | """ 257 | result = [] 258 | for root, dirs, files in os.walk(path): 259 | for name in files: 260 | if fnmatch.fnmatch(name, pattern): 261 | result.append(os.path.join(root, name)) 262 | return result 263 | 264 | 265 | def chaddr_to_mac(chaddr): 266 | """Convert the chaddr data from a DhcpPacket Option to a hex string 267 | of the form '12:34:56:ab:cd:ef'""" 268 | return ":".join(hex(i)[2:] for i in chaddr[0:6]) 269 | 270 | 271 | def getNbiOptions(incoming): 272 | """ 273 | The getNbiOptions() function walks through a given directory and 274 | finds and parses compatible NBIs by looking for NBImageInfo.plist 275 | files which are then processed with plistlib to extract an NBI's 276 | configuration items that are needed later on to send to BSDP clients. 277 | 278 | It is assumed that the NBI root directory is laid out as follows: 279 | /nbi/MyGreatImage.nbi 280 | /nbi/AnotherNetBootImage.nbi 281 | """ 282 | # Initialize lists to store NBIs and their options 283 | nbioptions = [] 284 | nbisources = [] 285 | try: 286 | for path, dirs, files in os.walk(incoming): 287 | # Create an empty dict that will hold an NBI's settings 288 | thisnbi = {} 289 | if os.path.splitext(path)[1] == '.nbi': 290 | del dirs[:] 291 | 292 | # Search the path for an NBImageInfo.plist and parse it. 293 | logging.debug('Considering NBI source at ' + str(path)) 294 | nbimageinfoplist = find('NBImageInfo.plist', path)[0] 295 | nbimageinfo = plistlib.readPlist(nbimageinfoplist) 296 | 297 | # Pull NBI settings out of the plist for use later on: 298 | # booter = The kernel which is loaded with tftp 299 | # disabledsysids = System IDs to blacklist, optional 300 | # dmg = The actual OS image loaded after the booter 301 | # enabledsysids = System IDs to whitelist, optional 302 | # enabledmacaddrs = Enabled MAC addresses to whitelist, optional 303 | # (and for which a key may not exist in) 304 | # id = The NBI Identifier, must be unique 305 | # isdefault = Indicates the NBI is the default 306 | # length = Length of the NBI name, needed for BSDP packet 307 | # name = The name of the NBI 308 | 309 | if nbimageinfo['Index'] == 0: 310 | logging.debug('Image "%s" Index is NULL (0), skipping!' 311 | % nbimageinfo['Name']) 312 | continue 313 | elif nbimageinfo['IsEnabled'] is False: 314 | logging.debug('Image "%s" is disabled, skipping.' 315 | % nbimageinfo['Name']) 316 | continue 317 | else: 318 | thisnbi['id'] = nbimageinfo['Index'] 319 | 320 | thisnbi['booter'] = \ 321 | find('booter', path)[0] 322 | thisnbi['description'] = \ 323 | nbimageinfo['Description'] 324 | thisnbi['disabledsysids'] = \ 325 | nbimageinfo['DisabledSystemIdentifiers'] 326 | thisnbi['dmg'] = \ 327 | '/'.join(find('*.dmg', path)[0].split('/')[2:]) 328 | 329 | thisnbi['enabledmacaddrs'] = \ 330 | nbimageinfo.get('EnabledMACAddresses', []) 331 | # EnabledMACAddresses must be lower-case - Apple's tools create them 332 | # as such, but in case they aren't.. 333 | thisnbi['enabledmacaddrs'] = [mac.lower() for mac in 334 | thisnbi['enabledmacaddrs']] 335 | 336 | thisnbi['enabledsysids'] = \ 337 | nbimageinfo['EnabledSystemIdentifiers'] 338 | thisnbi['isdefault'] = \ 339 | nbimageinfo['IsDefault'] 340 | thisnbi['length'] = \ 341 | len(nbimageinfo['Name']) 342 | thisnbi['name'] = \ 343 | nbimageinfo['Name'] 344 | thisnbi['proto'] = \ 345 | nbimageinfo['Type'] 346 | 347 | 348 | # Add the parameters for the current NBI to nbioptions 349 | nbioptions.append(thisnbi) 350 | # Found an eligible NBI source, add it to our nbisources list 351 | nbisources.append(path) 352 | except: 353 | logging.debug("Unexpected error getNbiOptions: %s" % 354 | sys.exc_info()[1]) 355 | raise 356 | 357 | return nbioptions, nbisources 358 | 359 | 360 | def getSysIdEntitlement(nbisources, clientsysid, clientmacaddr, bsdpmsgtype): 361 | """ 362 | The getSysIdEntitlement function takes a list of previously compiled NBI 363 | sources and a clientsysid parameter to determine which of the entries in 364 | nbisources the clientsysid is entitled to. 365 | 366 | The function: 367 | - Initializes the 'hasdupes' variable as False. 368 | - Checks for an enabledmacaddrs value: 369 | - If an empty list, no filtering is performed 370 | - It will otherwise contain one or more MAC addresses, and thisnbi 371 | will be skipped if the client's MAC address is not in this list. 372 | - Apple's NetInstall service also may create a "DisabledMACAddresses" 373 | blacklist, but this never seems to be used. 374 | - Checks for duplicate clientsysid entries in enabled/disabledsysids: 375 | - If found, there is a configuration issue with 376 | NBImageInfo.plist and thisnbi is skipped; a warning 377 | is thrown for the admin to act on. The hasdupes variable will be 378 | set to True. 379 | - Checks if hasdupes is False: 380 | - If True, continue with the tests below, otherwise iterate next. 381 | - Checks for empty disabledsysids and enabledsysids lists: 382 | - If both lists are zero length thisnbi is added to nbientitlements. 383 | - Checks for a missing clientsysid entry in enabledsysids OR a matching 384 | clientsysid entry in disabledsysids: 385 | - If if either is True thisnbi is skipped. 386 | - Checks for matching clientsysid entry in enabledsysids AND a missing 387 | clientsysid entry in disabledsysids: 388 | - If both are True thisnbi is added to nbientitlements. 389 | """ 390 | 391 | # Globals are used to give other functions access to these later 392 | global defaultnbi 393 | global imagenameslist 394 | global hasdefault 395 | 396 | logging.debug('Determining image list for system ID ' + clientsysid) 397 | 398 | # Initialize lists for nbientitlements and imagenameslist, both will 399 | # contain a series of dicts 400 | nbientitlements = [] 401 | imagenameslist = [] 402 | 403 | try: 404 | # Iterate over the NBI list 405 | for thisnbi in nbisources: 406 | 407 | # First a sanity check for duplicate system ID entries 408 | hasdupes = False 409 | 410 | if clientsysid in thisnbi['disabledsysids'] and \ 411 | clientsysid in thisnbi['enabledsysids']: 412 | 413 | # Duplicate entries are bad mkay, so skip this NBI and warn 414 | logging.debug('!!! Image "' + thisnbi['description'] + 415 | '" has duplicate system ID entries ' 416 | 'for model "' + clientsysid + '" - skipping !!!') 417 | hasdupes = True 418 | 419 | # Check whether both disabledsysids and enabledsysids are empty and 420 | # if so add the NBI to the list, there are no restrictions. 421 | if not hasdupes: 422 | # If the NBI had a non-empty EnabledMACAddresses array present, 423 | # skip this image if this client's MAC is not in the list. 424 | if thisnbi['enabledmacaddrs'] and \ 425 | clientmacaddr not in thisnbi['enabledmacaddrs']: 426 | logging.debug('MAC address ' + clientmacaddr + ' is not ' 427 | 'in the enabled MAC list - skipping "' + 428 | thisnbi['description'] + '"') 429 | continue 430 | 431 | if len(thisnbi['disabledsysids']) == 0 and \ 432 | len(thisnbi['enabledsysids']) == 0: 433 | logging.debug('Image "' + thisnbi['description'] + 434 | '" has no restrictions, adding to list') 435 | nbientitlements.append(thisnbi) 436 | 437 | # Check for a missing entry in enabledsysids, this means we skip 438 | elif clientsysid in thisnbi['disabledsysids']: 439 | logging.debug('System ID "' + clientsysid + '" is disabled' 440 | ' - skipping "' + thisnbi['description'] + '"') 441 | 442 | # Check for an entry in enabledsysids 443 | elif clientsysid not in thisnbi['enabledsysids'] or \ 444 | (clientsysid in thisnbi['enabledsysids'] and 445 | clientsysid not in thisnbi['disabledsysids']): 446 | logging.debug('Found enabled system ID ' + clientsysid + 447 | ' - adding "' + thisnbi['description'] + '" to list') 448 | nbientitlements.append(thisnbi) 449 | 450 | except: 451 | logging.debug("Unexpected error filtering image entitlements: %s" % 452 | sys.exc_info()[1]) 453 | raise 454 | 455 | try: 456 | # Now we iterate through the entitled NBIs in search of a default 457 | # image, as determined by its "IsDefault" key 458 | for image in nbientitlements: 459 | 460 | # Check for an isdefault entry in the current NBI 461 | if image['isdefault'] is True: 462 | logging.debug('Found default image ID ' + str(image['id'])) 463 | 464 | # By default defaultnbi is 0, so change it to the matched NBI's 465 | # id. If more than one is found (shouldn't) we use the highest 466 | # id found. This behavior may be changed if it proves to be 467 | # problematic, such as breaking out of the for loop instead. 468 | if defaultnbi < image['id']: 469 | defaultnbi = image['id'] 470 | hasdefault = True 471 | # logging.debug('Setting default image ID ' + str(defaultnbi)) 472 | # logging.debug('hasdefault is: ' + str(hasdefault)) 473 | 474 | # This is to match cases where there is no default image found, 475 | # a possibility. In that case we use the highest found id as the 476 | # default. This too could be changed at a later time. 477 | elif not hasdefault: 478 | if defaultnbi < image['id']: 479 | defaultnbi = image['id'] 480 | # logging.debug('Changing default image ID ' + str(defaultnbi)) 481 | 482 | # Next we construct our imagenameslist which is a list of ints that 483 | # encodes the image id, total name length and its name for use 484 | # by the packet encoder 485 | 486 | # The imageid should be a zero-padded 4 byte string represented as 487 | # ints 488 | imageid = '%04X' % image['id'] 489 | 490 | # Our skip interval within the list; the "[129,0]" header each image 491 | # ID requires, we don't want to count it for the length 492 | n = 2 493 | 494 | # Construct the list by iterating over the imageid, converting to a 495 | # 16 bit string as we go, for proper packet encoding 496 | imageid = [int(imageid[i:i+n], 16) \ 497 | for i in range(0, len(imageid), n)] 498 | imagenameslist += [129,0] + imageid + [image['length']] + \ 499 | strlist(image['name']).list() 500 | except: 501 | logging.debug("Unexpected error setting default image: %s" % 502 | sys.exc_info()[1]) 503 | raise 504 | 505 | # print 'Entitlements: ' + str(len(nbientitlements)) + '\n' + str(nbientitlements) + '\n' 506 | # print imagenameslist 507 | 508 | # All done, pass the finalized list of NBIs the given clientsysid back 509 | return nbientitlements 510 | 511 | 512 | def parseOptions(bsdpoptions): 513 | """ 514 | The parseOptions function parses a given bsdpoptions list and decodes 515 | the BSDP options contained within, giving them the proper names that 516 | pydhcplib expects. References the bsdpoptioncodes dict that was defined 517 | earlier on. 518 | """ 519 | optionvalues = {} 520 | msgtypes = {} 521 | pointer = 0 522 | 523 | # Using a pointer we step through the given bsdpoptions. These are raw DHCP 524 | # packets we are decoding so this looks incredibly laborious, but the 525 | # input is a 16 bit list of encoded strings and options that needs to be 526 | # diced up according to the BSDP option encoding as set forth in the 527 | # Apple BSDP documentation. 528 | while pointer < len(bsdpoptions): 529 | start = pointer 530 | length = pointer + 1 531 | optionlength = bsdpoptions[length] 532 | pointer = optionlength + length + 1 533 | 534 | msgtypes[bsdpoptioncodes[bsdpoptions[start]]] = \ 535 | [length+1, bsdpoptions[length]] 536 | 537 | # Now that we have decoded the raw BSDP options we iterate the msgtypes dict 538 | # and pull its values, appending them to the optionvalues dict as we go 539 | for msg, values in msgtypes.items(): 540 | start = values[0] 541 | end = start + values[1] 542 | options = bsdpoptions[start:end] 543 | 544 | optionvalues[msg] = options 545 | 546 | return optionvalues 547 | 548 | 549 | def ack(packet, defaultnbi, msgtype): 550 | """ 551 | The ack function constructs either a BSDP[LIST] or BSDP[SELECT] ACK 552 | DhcpPacket(), determined by the given msgtype, 'list' or 'select'. 553 | It calls the previously defined getSysIdEntitlement() and parseOptions() 554 | functions for either msgtype. 555 | """ 556 | 557 | bsdpack = DhcpPacket() 558 | 559 | try: 560 | # Get the requesting client's clientsysid and MAC address from the 561 | # BSDP options 562 | clientsysid = \ 563 | str(strlist(packet.GetOption('vendor_class_identifier'))).split('/')[2] 564 | 565 | clientmacaddr = chaddr_to_mac(packet.GetOption('chaddr')) 566 | 567 | # Decode and parse the BSDP options from vendor_encapsulated_options 568 | bsdpoptions = \ 569 | parseOptions(packet.GetOption('vendor_encapsulated_options')) 570 | 571 | # Figure out the NBIs this clientsysid is entitled to 572 | enablednbis = getSysIdEntitlement(nbiimages, clientsysid, clientmacaddr, msgtype) 573 | 574 | # The Startup Disk preference panel in OS X uses a randomized reply port 575 | # instead of the standard port 68. We check for the existence of that 576 | # option in the bsdpoptions dict and if found set replyport to it. 577 | if 'reply_port' in bsdpoptions: 578 | replyport = int(str(format(bsdpoptions['reply_port'][0], 'x') + 579 | format(bsdpoptions['reply_port'][1], 'x')), 16) 580 | else: 581 | replyport = 68 582 | 583 | # Get the client's IP address, a standard DHCP option 584 | clientip = ipv4(packet.GetOption('ciaddr')) 585 | if str(clientip) == '0.0.0.0': 586 | clientip = ipv4(packet.GetOption('request_ip_address')) 587 | logging.debug("Did not get a valid clientip, using request_ip_address %s instead" % (str(clientip),)) 588 | except: 589 | logging.debug("Unexpected error: ack() common %s" % 590 | sys.exc_info()[1]) 591 | raise 592 | 593 | #print 'Configuring common BSDP packet options' 594 | 595 | # We construct the rest of our common BSDP reply parameters according to 596 | # Apple's spec. The only noteworthy parameter here is sname, a zero-padded 597 | # 64 byte string list containing the BSDP server's hostname. 598 | bsdpack.SetOption("op", [2]) 599 | bsdpack.SetOption("htype", packet.GetOption('htype')) 600 | bsdpack.SetOption("hlen", packet.GetOption('hlen')) 601 | bsdpack.SetOption("xid", packet.GetOption('xid')) 602 | bsdpack.SetOption("ciaddr", packet.GetOption('ciaddr')) 603 | bsdpack.SetOption("siaddr", serverip) 604 | bsdpack.SetOption("yiaddr", [0,0,0,0]) 605 | bsdpack.SetOption("sname", strlist(serverhostname.ljust(64,'\x00')).list()) 606 | bsdpack.SetOption("chaddr", packet.GetOption('chaddr')) 607 | bsdpack.SetOption("dhcp_message_type", [5]) 608 | bsdpack.SetOption("server_identifier", serverip) 609 | bsdpack.SetOption("vendor_class_identifier", strlist('AAPLBSDPC').list()) 610 | 611 | # Process BSDP[LIST] requests 612 | if msgtype == 'list': 613 | #print 'Creating LIST packet' 614 | try: 615 | nameslength = 0 616 | n = 2 617 | 618 | # First calculate the total length of the names of all combined 619 | # NBIs, a required parameter that is part of the BSDP 620 | # vendor_encapsulated_options. 621 | for i in enablednbis: 622 | nameslength += i['length'] 623 | 624 | # Next calculate the total length of all enabled NBIs 625 | totallength = len(enablednbis) * 5 + nameslength 626 | 627 | # The bsdpimagelist var is inserted into vendor_encapsulated_options 628 | # and comprises of the option code (9), total length of options, 629 | # the IDs and names of all NBIs and the 4 byte string list that 630 | # contains the default NBI ID. Promise, all of this is part of 631 | # the BSDP spec, go look it up. 632 | bsdpimagelist = [9,totallength] 633 | bsdpimagelist += imagenameslist 634 | defaultnbi = '%04X' % defaultnbi 635 | 636 | # Encode the default NBI option (7) its standard length (4) and the 637 | # 16 bit string list representation of defaultnbi 638 | defaultnbi = [7,4,129,0] + \ 639 | [int(defaultnbi[i:i+n], 16) for i in range(0, len(defaultnbi), n)] 640 | 641 | if int(defaultnbi[-1:][0]) == 0: 642 | hasnulldefault = True 643 | else: 644 | hasnulldefault = False 645 | 646 | # To prevent sending a default image ID of 0 (zero) to the client 647 | # after the initial INFORM[LIST] request we test for 0 and if 648 | # so, skip inserting the defaultnbi BSDP option. Since it is 649 | # optional anyway we won't confuse the client. 650 | compiledlistpacket = strlist([1,1,1,4,2,128,128]).list() 651 | if not hasnulldefault: 652 | compiledlistpacket += strlist(defaultnbi).list() 653 | compiledlistpacket += strlist(bsdpimagelist).list() 654 | 655 | # And finally, once we have all the image list encoding taken care 656 | # of, we plug them into the vendor_encapsulated_options DHCP 657 | # option after the option header: 658 | # - [1,1,1] = BSDP message type (1), length (1), value (1 = list) 659 | # - [4,2,255,255] = Server priority message type 4, length 2, 660 | # value 0xffff (65535 - Highest) 661 | # - defaultnbi (option 7) - Optional, not sent if '0' 662 | # - List of all available Image IDs (option 9) 663 | 664 | bsdpack.SetOption("vendor_encapsulated_options", compiledlistpacket) 665 | 666 | # Some debugging to stdout 667 | logging.debug('-=========================================-') 668 | logging.debug("Return ACK[LIST] to " + 669 | str(clientip) + 670 | ' on ' + 671 | str(replyport)) 672 | if hasnulldefault is False: logging.debug("Default boot image ID: " + 673 | str(defaultnbi[2:])) 674 | except: 675 | logging.debug("Unexpected error ack() list: %s" % 676 | sys.exc_info()[1]) 677 | raise 678 | 679 | # Process BSDP[SELECT] requests 680 | elif msgtype == 'select': 681 | #print 'Creating SELECT packet' 682 | # Get the value of selected_boot_image as sent by the client and convert 683 | # the value for later use. 684 | try: 685 | imageid = int('%02X' % bsdpoptions['selected_boot_image'][2] + 686 | '%02X' % bsdpoptions['selected_boot_image'][3], 16) 687 | except: 688 | logging.debug("Unexpected error ack() select: imageid %s" % 689 | sys.exc_info()[1]) 690 | raise 691 | 692 | # Initialize variables for the booter file (kernel) and the dmg path 693 | booterfile = '' 694 | rootpath = '' 695 | selectedimage = '' 696 | if nbiurl.hostname[0].isalpha(): 697 | basedmgpath = getBaseDmgPath(nbiurl) 698 | 699 | # Iterate over enablednbis and retrieve the kernel and boot DMG for each 700 | try: 701 | for nbidict in enablednbis: 702 | if nbidict['id'] == imageid: 703 | booterfile = nbidict['booter'] 704 | rootpath = basedmgpath + nbidict['dmg'] 705 | # logging.debug('-->> Using boot image URI: ' + str(rootpath)) 706 | selectedimage = bsdpoptions['selected_boot_image'] 707 | # logging.debug('ACK[SELECT] image ID: ' + str(selectedimage)) 708 | except: 709 | logging.debug("Unexpected error ack() selectedimage: %s" % 710 | sys.exc_info()[1]) 711 | raise 712 | 713 | # Generate the rest of the BSDP[SELECT] ACK packet by encoding the 714 | # name of the kernel (file), the TFTP path and the vendor encapsulated 715 | # options: 716 | # - [1,1,2] = BSDP message type (1), length (1), value (2 = select) 717 | # - [8,4] = BSDP selected_image (8), length (4), encoded image ID 718 | try: 719 | bsdpack.SetOption("file", 720 | strlist(booterfile.ljust(128,'\x00')).list()) 721 | bsdpack.SetOption("root_path", strlist(rootpath).list()) 722 | bsdpack.SetOption("vendor_encapsulated_options", 723 | strlist([1,1,2,8,4] + selectedimage).list()) 724 | except: 725 | logging.debug("Unexpected error ack() select SetOption: %s" % 726 | sys.exc_info()[1]) 727 | raise 728 | 729 | try: 730 | # Some debugging to stdout 731 | logging.debug('-=========================================-') 732 | logging.debug("Return ACK[SELECT] to " + 733 | str(clientip) + 734 | ' on ' + 735 | str(replyport)) 736 | logging.debug("--> TFTP path: %s\n-->Boot image URI: %s" 737 | % (str(strlist(bsdpack.GetOption("file"))), str(rootpath))) 738 | except: 739 | logging.debug("Unexpected error ack() select print debug: %s" % 740 | sys.exc_info()[1]) 741 | raise 742 | 743 | # Return the finished packet, client IP and reply port back to the caller 744 | return bsdpack, clientip, replyport 745 | 746 | imagenameslist = [] 747 | nbiimages = [] 748 | defaultnbi = 0 749 | hasdefault = False 750 | 751 | 752 | def main(): 753 | """Main routine. Do the work.""" 754 | 755 | # # First, write a PID so we can monitor the process 756 | # pid = str(os.getpid()) 757 | # pidfile = "/var/run/bspdserver.pid" 758 | # 759 | # if os.path.isfile(pidfile): 760 | # print "%s already exists, exiting" % pidfile 761 | # sys.exit() 762 | # else: 763 | # file(pidfile, 'w').write(pid) 764 | 765 | # Some logging preamble 766 | logging.debug('\n\n-=- Starting new BSDP server session -=-\n') 767 | 768 | # We are changing nbiimages for use by other functions 769 | global nbiimages 770 | 771 | # Instantiate a basic pydhcplib DhcpServer class using netopts (listen port, 772 | # reply port and listening IP) 773 | server = Server(netopt) 774 | 775 | # Do a one-time discovery of all available NBIs on the server. NBIs added 776 | # after the server was started will not be picked up until after a restart 777 | nbiimages, nbisources = getNbiOptions(tftprootpath) 778 | 779 | def scan_nbis(signal, frame): 780 | global nbiimages 781 | logging.debug('[========= Updating boot images list =========]') 782 | nbiimages, nbisources = getNbiOptions(tftprootpath) 783 | for nbi in nbisources: 784 | logging.debug(nbi) 785 | logging.debug('[========= End updated list =========]') 786 | 787 | signal.signal(signal.SIGUSR1, scan_nbis) 788 | signal.siginterrupt(signal.SIGUSR1, False) 789 | 790 | # Print the full list of eligible NBIs to the log 791 | logging.debug('[========= Using the following boot images =========]') 792 | for nbi in nbisources: 793 | logging.debug(nbi) 794 | logging.debug('[========= End boot image listing =========]') 795 | 796 | # Loop while the looping's good. 797 | while True: 798 | 799 | # Listen for DHCP packets. Since select() is used upstream we need to 800 | # catch the EINTR signal it trips on when we receive a USR1 signal to 801 | # reload the nbiimages list. 802 | try: 803 | packet = server.GetNextDhcpPacket() 804 | except select.error, e: 805 | if e[0] != errno.EINTR: raise 806 | 807 | try: 808 | # Check to see if any vendor_encapsulated_options are present 809 | if len(packet.GetOption('vendor_encapsulated_options')) > 1: 810 | 811 | # If we have vendor_encapsulated_options check for a value of 1 812 | # which in BSDP terms means the packet is a BSDP[LIST] request 813 | if packet.GetOption('vendor_encapsulated_options')[2] == 1: 814 | logging.debug('-=========================================-') 815 | logging.debug('Got BSDP INFORM[LIST] packet: ') 816 | 817 | # Pass ack() the matching packet, defaultnbi and 'list' 818 | bsdplistack, clientip, replyport = ack(packet, 819 | defaultnbi, 820 | 'list') 821 | # Once we have a finished DHCP packet, send it to the client 822 | server.SendDhcpPacketTo(bsdplistack, str(clientip), 823 | replyport) 824 | 825 | # If the vendor_encapsulated_options BSDP type is 2, we process 826 | # the packet as a BSDP[SELECT] request 827 | elif packet.GetOption('vendor_encapsulated_options')[2] == 2: 828 | logging.debug('-=========================================-') 829 | logging.debug('Got BSDP INFORM[SELECT] packet: ') 830 | 831 | 832 | bsdpselectack, selectackclientip, selectackreplyport = \ 833 | ack(packet, None, 'select') 834 | 835 | # Once we have a finished DHCP packet, send it to the client 836 | server.SendDhcpPacketTo(bsdpselectack, 837 | str(selectackclientip), 838 | selectackreplyport) 839 | # If the packet length is 7 or less, move on, BSDP packets are 840 | # at least 8 bytes long. 841 | elif len(packet.GetOption('vendor_encapsulated_options')) <= 7: 842 | pass 843 | except: 844 | # Error? No worries, keep going. 845 | pass 846 | 847 | if __name__ == '__main__': 848 | main() 849 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # BSDPy Dockerfile 2 | # Installs and prepares BSDPy to run inside a Docker container 3 | # Project home: https://bitbucket.org/bruienne/bsdpy 4 | # Version 0.1 5 | 6 | FROM douglasmiranda/python-base 7 | MAINTAINER Pepijn Bruienne "bruienne@umich.edu" 8 | 9 | ENV DEBIAN_FRONTEND noninteractive 10 | ENV DOCKER_BSDPY_IFACE eth0 11 | ENV DOCKER_BSDPY_PROTO http 12 | 13 | RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list 14 | RUN apt-get update -qq 15 | RUN dpkg-divert --local --rename --add /sbin/initctl 16 | RUN ln -s /bin/true /sbin/initctl 17 | RUN apt-get install -y -qq nginx tftpd-hpa nfs-common inotify-tools 18 | 19 | RUN git clone https://bitbucket.org/bruienne/bsdpy.git 20 | RUN git clone https://github.com/bruienne/pydhcplib.git 21 | RUN cd ~/pydhcplib; python setup.py install 22 | RUN pip install docopt 23 | RUN mkdir /nbi 24 | 25 | ADD nginx.conf /etc/nginx/nginx.conf 26 | ADD start.sh /start.sh 27 | # ADD nfs-client.sh /usr/local/bin/nfs-client 28 | 29 | RUN chown -R root:root /etc/nginx/nginx.conf start.sh 30 | # RUN chmod +x start.sh /usr/local/bin/nfs-client 31 | 32 | EXPOSE 67/udp 33 | EXPOSE 69/udp 34 | EXPOSE 80 35 | 36 | ENTRYPOINT ["/start.sh"] 37 | -------------------------------------------------------------------------------- /docker/Readme.md: -------------------------------------------------------------------------------- 1 | ## Running BSDPy as a Docker container 2 | 3 | ### Requirements 4 | 5 | In order to successfully run a BSDPy Docker container you will need: 6 | 7 | - A Docker host with a network device attached to the desired subnet 8 | 9 | - The IP of this network device 10 | 11 | - One or more NetBoot images (NBIs) to serve to clients 12 | 13 | - A dedicated storage container for the NBIs (setup details below) 14 | 15 | ### NBI Storage Container 16 | 17 | The BSDPy container remains fully portable by using a separate storage 18 | container that can be attached at runtime. This way the administrator 19 | does not need to modify the BSDPy container itself but only has to 20 | create a simple storage container for their organization's NBIs. The NBI 21 | storage container can then be moved around to other hosts manually or by 22 | pushing it to an internal Docker repository and running a "docker run -d 23 | myorg/NBI:latest" on other hosts that will host BSDP services. 24 | 25 | $ docker run -d -p 67:67/udp -p 69:69/udp -p 80:80 -e DOCKER_BSDPY_IP= --volumes-from NBI bruienne/bsdpy:latest 26 | -------------------------------------------------------------------------------- /docker/bsdpy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=bsdpy 3 | After=docker.service 4 | Requires=docker.service 5 | Requires=unfs3.service 6 | After=unfs3.service 7 | Requires=tftpd.service 8 | After=tftpd.service 9 | 10 | [Service] 11 | ExecStartPre=-/usr/bin/docker pull macadmins/bsdpy:latest 12 | ExecStart=/usr/bin/docker run --rm --name bsdpy -h bsdpy.local --volume=/data/bsdpy:/nbi -p 0.0.0.0:8901:80/tcp -p 0.0.0.0:67:67/udp -p 0.0.0.0:68:68/udp -e VIRTUAL_HOST=X.X.X.X -e VIRTUAL_PORT=80 -e DOCKER_BSDPY_IP=X.X.X.X -e DOCKER_BSDPY_PROTO=nfs macadmins/bsdpy:latest 13 | ExecStartPost=-/usr/bin/docker rm bsdpy 14 | ExecStop=/usr/bin/docker kill bsdpy 15 | ExecStopPost=-/usr/bin/docker rm bsdpy 16 | Restart=always 17 | RestartSec=10 18 | TimeoutStartSec=5min 19 | -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | #user html; 2 | worker_processes 8; 3 | 4 | error_log /var/log/nginx-error.log; 5 | # error_log logs/error.log notice; 6 | # error_log logs/error.log info; 7 | 8 | pid /var/run/nginx.pid; 9 | 10 | 11 | events { 12 | worker_connections 1024; 13 | } 14 | 15 | 16 | http { 17 | include mime.types; 18 | default_type application/octet-stream; 19 | 20 | access_log /var/log/nginx-access.log; 21 | 22 | sendfile on; 23 | #tcp_nopush on; 24 | 25 | #keepalive_timeout 0; 26 | keepalive_timeout 65; 27 | 28 | #gzip on; 29 | 30 | server { 31 | listen 80; 32 | server_name localhost; 33 | 34 | location / { 35 | root /nbi; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docker/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # /usr/local/bin/nfs-client 3 | sleep 2 4 | service nginx start 5 | /usr/sbin/in.tftpd -l --permissive /nbi 6 | cd /bsdpy 7 | git pull 8 | ./bsdpserver.py -p ${DOCKER_BSDPY_PATH} -i ${DOCKER_BSDPY_IFACE} -r ${DOCKER_BSDPY_PROTO} & 9 | sleep 2 10 | tail -f /var/log/bsdpserver.log 11 | -------------------------------------------------------------------------------- /docker/storage/Dockerfile: -------------------------------------------------------------------------------- 1 | # BSDPy storage container 2 | # 3 | # Provides separate persistent data storage for NetBoot images (NBIs) 4 | 5 | 6 | FROM busybox 7 | MAINTAINER Pepijn Bruienne "bruienne@umich.edu" 8 | 9 | ADD #/path/to/NBIs /nbi 10 | VOLUME ["/nbi"] 11 | 12 | CMD ["/bin/true"] 13 | -------------------------------------------------------------------------------- /docker/tftp.conf: -------------------------------------------------------------------------------- 1 | service tftp 2 | { 3 | protocol = udp 4 | port = 69 5 | socket_type = dgram 6 | wait = yes 7 | user = nobody 8 | server = /usr/sbin/in.tftpd 9 | server_args = /nbi 10 | disable = no 11 | } 12 | -------------------------------------------------------------------------------- /docker/tftpd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=TFTP Server 3 | After=docker.service 4 | Requires=docker.service 5 | Before=bsdpy.service 6 | 7 | [Service] 8 | ExecStartPre=/usr/sbin/modprobe nf_conntrack_tftp 9 | ExecStartPre=/usr/sbin/modprobe nf_nat_tftp 10 | ExecStartPre=/bin/bash -c '/usr/bin/docker inspect %n &> /dev/null && /usr/bin/docker rm %n || :' 11 | ExecStart=/usr/bin/docker run --rm --name %n -p 69:69/udp -v /data/bsdpy:/nbi -h tftpd macadmins/tftpd /usr/sbin/in.tftpd --listen --foreground -vvvv --verbosity 10 --user user -r blksize /nbi 12 | ExecStop=/usr/bin/docker stop %n 13 | RestartSec=5s 14 | Restart=always 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /docker/unfs3.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=unfs3 3 | After=docker.service 4 | Before=bsdpy.service 5 | 6 | [Service] 7 | ExecStartPre=-/usr/bin/docker pull macadmins/unfs3:latest 8 | ExecStart=/usr/bin/docker run --rm --privileged -p 111:111/udp -p 111:111/tcp -p 2049:2049/udp -p 2049:2049/tcp --name unfs3 -v /data/bsdpy:/nbi -v /data/unfs3/exports:/etc/exports macadmins/unfs3 9 | ExecStartPost=-/usr/bin/docker rm unfs3 10 | ExecStop=/usr/bin/docker kill unfs3 11 | ExecStopPost=-/usr/bin/docker rm unfs3 12 | Restart=always 13 | RestartSec=10 14 | TimeoutStartSec=5min 15 | --------------------------------------------------------------------------------