Repository: kelseyhightower/coreos-ipxe-server Branch: master Commit: 8b671421047f Files: 19 Total size: 24.0 KB Directory structure: gitextract_12q8nhyr/ ├── .gitignore ├── LICENSE ├── README.md ├── api.go ├── api_test.go ├── config/ │ └── config.go ├── docker/ │ ├── .gitignore │ ├── Dockerfile │ └── setup_docker.sh ├── docs/ │ ├── api.md │ ├── cloudconfigs.md │ ├── configuration.md │ ├── docker.md │ ├── getting_started.md │ ├── profiles.md │ └── sshkeys.md ├── kernel/ │ ├── options.go │ └── options_test.go └── main.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ data coreos-ipxe-server ================================================ FILE: LICENSE ================================================ Copyright (c) 2014 Kelsey Hightower Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # CoreOS iPXE Server [![Build Status](https://drone.io/github.com/kelseyhightower/coreos-ipxe-server/status.png)](https://drone.io/github.com/kelseyhightower/coreos-ipxe-server/latest) The CoreOS iPXE Server attempts to automate as much of the [Booting CoreOS via iPXE](https://coreos.com/docs/running-coreos/bare-metal/booting-with-ipxe/) process as possible, mainly generating iPXE boot scripts and serving CoreOS PXE boot images. ## Table of Contents - [Installation](#installation) - [Getting Started](docs/getting_started.md) - [API](docs/api.md) - [Profiles](docs/profiles.md) - [Docker setup example](docs/docker.md) ## Installation ### Binary Release ``` curl -L https://github.com/kelseyhightower/coreos-ipxe-server/releases/download/v0.3.0/coreos-ipxe-server-0.3.0-darwin-amd64 -o coreos-ipxe-server chmod +x coreos-ipxe-server ``` ### Source #### Clone ``` mkdir -p ${GOPATH}/src/github.com/kelseyhightower cd ${GOPATH}/src/github.com/kelseyhightower git clone git@github.com:kelseyhightower/coreos-ipxe-server.git ``` #### Build ``` cd ${GOPATH}/src/github.com/kelseyhightower/coreos-ipxe-server go build . ``` ================================================ FILE: api.go ================================================ package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "path/filepath" "text/template" "github.com/kelseyhightower/coreos-ipxe-server/config" "github.com/kelseyhightower/coreos-ipxe-server/kernel" ) const ipxeBootScript = `#!ipxe set coreos-version {{.Version}} set base-url http://{{.BaseUrl}}/images/amd64-usr/${coreos-version} kernel ${base-url}/coreos_production_pxe.vmlinuz{{.Options}} initrd ${base-url}/coreos_production_pxe_image.cpio.gz boot ` func ipxeBootScriptServer(w http.ResponseWriter, r *http.Request) { log.Printf("creating boot script for %s", r.RemoteAddr) baseUrl := config.BaseUrl if baseUrl == "" { baseUrl = r.Host } v := r.URL.Query() options := kernel.New() // Process the profile parameter. profile := v.Get("profile") if profile != "" { profilePath := filepath.Join(config.DataDir, fmt.Sprintf("profiles/%s.json", profile)) err := kernalOptionsFromFile(profilePath, options) if err != nil { log.Printf("Error reading kernal options from %s: %s", profilePath, err) http.Error(w, err.Error(), 500) return } } if options.CloudConfig != "" { options.SetCloudConfigUrl(fmt.Sprintf("http://%s/configs/%s.yml", baseUrl, options.CloudConfig)) } if options.SSHKey != "" { sshKeyPath := filepath.Join(config.DataDir, fmt.Sprintf("sshkeys/%s.pub", options.SSHKey)) sshKey, err := sshKeyFromFile(sshKeyPath) if err != nil { log.Printf("Error reading ssh publickey from %s: %s", sshKeyPath, err) http.Error(w, err.Error(), 500) return } options.SSHKey = sshKey } // Process the iPXE boot script template. t, err := template.New("ipxebootscript").Parse(ipxeBootScript) if err != nil { log.Print("Error generating iPXE boot script: " + err.Error()) http.Error(w, "Error generating the iPXE boot script", 500) return } data := map[string]string{ "BaseUrl": baseUrl, "Options": options.String(), "Version": options.Version, } err = t.Execute(w, data) if err != nil { log.Print("Error generating iPXE boot script: " + err.Error()) http.Error(w, "Error generating the iPXE boot script", 500) return } return } func sshKeyServer(w http.ResponseWriter, r *http.Request) { v := r.URL.Query() keyName := v.Get("name") if keyName != "" { log.Printf("retrieving ssh key %s.pub for %s", keyName, r.RemoteAddr) sshKeyPath := filepath.Join(config.DataDir, fmt.Sprintf("sshkeys/%s.pub", keyName)) sshKey, err := sshKeyFromFile(sshKeyPath) if err != nil { log.Printf("Error reading ssh publickey from %s: %s", sshKeyPath, err) http.Error(w, err.Error(), 500) return } data := []byte(fmt.Sprintf("[{\"key\": \"%s\"}]", sshKey)) w.Write(data) } return } func sshKeyFromFile(filename string) (string, error) { b, err := ioutil.ReadFile(filename) if err != nil { return "", err } return string(bytes.TrimSpace(b)), nil } func kernalOptionsFromFile(filename string, options *kernel.Options) error { data, err := ioutil.ReadFile(filename) if err != nil { return err } err = json.Unmarshal(data, options) if err != nil { return err } return nil } ================================================ FILE: api_test.go ================================================ // Copyright 2014 Kelsey Hightower. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/kelseyhightower/coreos-ipxe-server/config" "github.com/kelseyhightower/coreos-ipxe-server/kernel" ) func createTestData(profiles map[string]*kernel.Options, sshKeys map[string]string) (string, error) { d, err := ioutil.TempDir("", "coreos-ipxe-server") if err != nil { return "", err } sshKeyDir := filepath.Join(d, "sshkeys") err = os.Mkdir(sshKeyDir, 0755) if err != nil { return "", err } for k, v := range sshKeys { sshKeyPath := filepath.Join(sshKeyDir, fmt.Sprintf("%s.pub", k)) err := ioutil.WriteFile(sshKeyPath, []byte(v), 0644) if err != nil { return "", err } } profileDir := filepath.Join(d, "profiles") err = os.Mkdir(profileDir, 0755) if err != nil { return "", err } for k, v := range profiles { profilePath := filepath.Join(profileDir, fmt.Sprintf("%s.json", k)) data, err := json.Marshal(v) if err != nil { return "", err } err = ioutil.WriteFile(profilePath, data, 0644) if err != nil { return "", err } } return d, nil } var profileAOut = `#!ipxe set coreos-version 310.1.0 set base-url http://example.com/images/amd64-usr/${coreos-version} kernel ${base-url}/coreos_production_pxe.vmlinuz initrd ${base-url}/coreos_production_pxe_image.cpio.gz boot ` var profileBOut = `#!ipxe set coreos-version 310.1.0 set base-url http://example.com/images/amd64-usr/${coreos-version} kernel ${base-url}/coreos_production_pxe.vmlinuz rootfstype=btrfs console=tty0 console=ttyS0 cloud-config-url=http://example.com/configs/b.yml coreos.autologin=ttyS0 sshkey="ssh-rsa AAAAB3Ncoreos" root=/dev/sda1 initrd ${base-url}/coreos_production_pxe_image.cpio.gz boot ` var iPxeBootScriptTests = []struct { name string body string code int baseUrl string url string }{ {"a", profileAOut, 200, "", "http://example.com?profile=a"}, {"b", profileBOut, 200, "example.com", "http://example.com?profile=b"}, {"c", "", 500, "example.com", "http://example.com?profile=c"}, {"d", "", 500, "example.com", "http://example.com?profile=d"}, } func TestIPxeBootScriptServer(t *testing.T) { sshkeys := map[string]string{ "coreos": "ssh-rsa AAAAB3Ncoreos", } profiles := map[string]*kernel.Options{ "a": &kernel.Options{ CloudConfig: "", Console: []string{}, CoreOSAutologin: "", Root: "", RootFstype: "", SSHKey: "", Version: "310.1.0", }, "b": &kernel.Options{ CloudConfig: "b", Console: []string{"tty0", "ttyS0"}, CoreOSAutologin: "ttyS0", Root: "/dev/sda1", RootFstype: "btrfs", SSHKey: "coreos", Version: "310.1.0", }, "c": &kernel.Options{ CloudConfig: "c", Console: []string{"tty0", "ttyS0"}, CoreOSAutologin: "ttyS0", Root: "/dev/sda1", RootFstype: "btrfs", SSHKey: "imabadkey", Version: "310.1.0", }, } testDataDir, err := createTestData(profiles, sshkeys) if err != nil { t.Error(err) } defer os.RemoveAll(testDataDir) config.DataDir = testDataDir for _, v := range iPxeBootScriptTests { config.BaseUrl = v.baseUrl req, err := http.NewRequest("GET", v.url, nil) if err != nil { t.Error(err) } w := httptest.NewRecorder() ipxeBootScriptServer(w, req) if w.Code == 200 && (v.name == "a" || v.name == "b") { if w.Body.String() != v.body { t.Errorf("expected %s\ngot %s\n", v.body, w.Body.String()) } } else if (v.name == "c" || v.name == "d") && w.Code != 500 { t.Errorf("expected %d\ngot %d\n", v.code, w.Code) } } } var SSHKeyServerTests = []struct { name string body string code int baseUrl string url string }{ {"a", `[{"key": "ssh-rsa AAAAB3Ncoreos"}]`, 200, "", "http://example.com/keys?name=coreos"}, {"b", `[{"key": "ssh-rsa AAAAB3Nfoo"}]`, 200, "example.com", "http://example.com/keys?name=foo"}, {"c", "", 500, "example.com", "http://example.com/keys?name=badkey"}, } func TestSSHKeyServer(t *testing.T) { sshkeys := map[string]string{ "coreos": "ssh-rsa AAAAB3Ncoreos", "foo": "ssh-rsa AAAAB3Nfoo", } testDataDir, err := createTestData(nil, sshkeys) if err != nil { t.Error(err) } defer os.RemoveAll(testDataDir) config.DataDir = testDataDir for _, v := range SSHKeyServerTests { config.BaseUrl = v.baseUrl req, err := http.NewRequest("GET", v.url, nil) if err != nil { t.Error(err) } w := httptest.NewRecorder() sshKeyServer(w, req) if w.Code == 200 && (v.name == "a" || v.name == "b") { if w.Body.String() != v.body { t.Errorf("expected %s\ngot %s\n", v.body, w.Body.String()) } } else if (v.name == "c") && w.Code != 500 { t.Errorf("expected %d\ngot %d\n", v.code, w.Code) } } } ================================================ FILE: config/config.go ================================================ package config import ( "os" ) var ( BaseUrl string DataDir string ListenAddr string ) var defaultDataDir = "/opt/coreos-ipxe-server" var defaultListenAddr = "0.0.0.0:4777" func init() { BaseUrl = os.Getenv("COREOS_IPXE_SERVER_BASE_URL") // Set the data directory where the coreos directory containing // the ssh public key, kernal and boot images. DataDir = os.Getenv("COREOS_IPXE_SERVER_DATA_DIR") if DataDir == "" { DataDir = defaultDataDir } ListenAddr = os.Getenv("COREOS_IPXE_SERVER_LISTEN_ADDR") if ListenAddr == "" { ListenAddr = defaultListenAddr } } ================================================ FILE: docker/.gitignore ================================================ configs/ images/ profiles/ sshkeys/ ================================================ FILE: docker/Dockerfile ================================================ FROM google/golang RUN mkdir -p /gopath/src/github.com/kelseyhightower WORKDIR /gopath/src/github.com/kelseyhightower RUN git clone https://github.com/kelseyhightower/coreos-ipxe-server.git WORKDIR /gopath/src/github.com/kelseyhightower/coreos-ipxe-server RUN go install RUN mkdir -p /opt/coreos-ipxe-server ADD configs /opt/coreos-ipxe-server/configs ADD images /opt/coreos-ipxe-server/images ADD profiles /opt/coreos-ipxe-server/profiles ADD sshkeys /opt/coreos-ipxe-server/sshkeys ENV COREOS_IPXE_SERVER_DATA_DIR /opt/coreos-ipxe-server # URL has to be substituted with correct value. Can be overwritten during docker run ENV COREOS_IPXE_SERVER_BASE_URL coreos.ipxe.example.com:4777 ENV COREOS_IPXE_SERVER_LISTEN_ADDR 0.0.0.0:4777 CMD /gopath/bin/coreos-ipxe-server ================================================ FILE: docker/setup_docker.sh ================================================ #!/bin/bash # SETUP ENV COREOS_IPXE_SERVER_BASE_URL=":4777" COREOS_IPXE_SERVER_LISTEN_ADDR="0.0.0.0:4777" VERSIONS=("367.1.0" "379.3.0") # PREPARE DIRECTORY STRUCTURE mkdir -p {configs,images,profiles,sshkeys} # DOWNLOAD IMAGES for VERSION in "${VERSIONS[@]}" do echo "Downloading files for version ${VERSION}" mkdir -p images/amd64-usr/$VERSION wget -nc http://storage.core-os.net/coreos/amd64-usr/$VERSION/coreos_production_pxe_image.cpio.gz -O images/amd64-usr/${VERSION}/coreos_production_pxe_image.cpio.gz wget -nc http://storage.core-os.net/coreos/amd64-usr/$VERSION/coreos_production_pxe.vmlinuz -O images/amd64-usr/${VERSION}/coreos_production_pxe.vmlinuz done # CUSTOMIZE COREOS SERVER CONFIGURATION echo "" > sshkeys/coreos.pub cat > configs/development.yml < coreos: etcd: # generate a new token for each unique cluster from https://discovery.etcd.io/new # WARNING: replace each time you 'vagrant destroy' discovery: https://discovery.etcd.io/a1efed8239a47c98c12ce07e2b67f0ed addr: $public_ipv4:4001 peer-addr: $public_ipv4:7001 units: - name: etcd.service command: start - name: fleet.service command: start runtime: no content: | [Unit] Description=fleet [Service] Environment=FLEET_PUBLIC_IP=$public_ipv4 ExecStart=/usr/bin/fleet - name: docker-tcp.socket command: start enable: true content: | [Unit] Description=Docker Socket for the API [Socket] ListenStream=2375 Service=docker.service BindIPv6Only=both [Install] WantedBy=sockets.target EOF for VERSION in "${VERSIONS[@]}" do cat > profiles/development_${VERSION}.json <:4777/?profile=development_`` In my case it will be ``http://10.102.11.42:4777/?profile=development_367.1.0`` ================================================ FILE: docs/getting_started.md ================================================ # Getting Started - [Installation](#installation) - [Configuration](#configuration) - [Create the Data Directory](#create-the-data-directory) - [Download the CoreOS PXE Images](#download-the-coreos-pxe-images) - [Add a Cloud Config File](#add-a-cloud-config-file) - [Add a SSH Public Key](#add-an-ssh-public-key) - [Add an iPXE Profile](#add-an-ipxe-profile) - [Example Data Directory Layout](#example-data-directory-layout) ### Installation ``` curl -L https://github.com/kelseyhightower/coreos-ipxe-server/releases/download/v0.3.0/coreos-ipxe-server-0.3.0-darwin-amd64 -o coreos-ipxe-server chmod +x coreos-ipxe-server ``` ### Configuration All configuration is handled via environment variables with sane defaults. See [Configuration](configuration.md) for more details. ### Create the Data Directory The data directory is where the CoreOS images, SSH public keys, cloud configs and iPXE profiles are stored. The data directory defaults to `/opt/coreos-ipxe-server`; set it to a different directory via the `COREOS_IPXE_SERVER_DATA_DIR` environment variable: ``` mkdir -p $COREOS_IPXE_SERVER_DATA_DIR/{configs,images,profiles,sshkeys} ``` ### Download the CoreOS PXE Images The CoreOS PXE images are stored under the `$COREOS_IPXE_SERVER_DATA_DIR/images` directory. ``` mkdir -p $COREOS_IPXE_SERVER_DATA_DIR/images/amd64-usr/310.1.0 cd $COREOS_IPXE_SERVER_DATA_DIR/images/amd64-usr/310.1.0 wget http://storage.core-os.net/coreos/amd64-usr/310.1.0/coreos_production_pxe_image.cpio.gz wget http://storage.core-os.net/coreos/amd64-usr/310.1.0/coreos_production_pxe.vmlinuz ``` ### Add an SSH Public Key SSH public keys are used to login to your CoreOS instance. SSH public keys are stored under the `$COREOS_IPXE_SERVER_DATA_DIR/sshkeys` directory. Edit `$COREOS_IPXE_SERVER_DATA_DIR/sshkeys/coreos.pub` ``` ssh-rsa AAAAB3Nza... ``` ### Add a Cloud Config File Cloud config files are used to automated the setup of your CoreOS instance. See [Customize with Cloud Config](https://coreos.com/docs/cluster-management/setup/cloudinit-cloud-config/) for more details. Cloud config files are stored under the `$COREOS_IPXE_SERVER_DATA_DIR/configs` directory. Edit `$COREOS_IPXE_SERVER_DATA_DIR/configs/development.yml` ``` #cloud-config ssh_authorized_keys: - ssh-rsa AAAAB3Nza... coreos: etcd: addr: $private_ipv4:4001 peer-addr: $private_ipv4:7001 units: - name: etcd.service command: start - name: fleet.service command: start - name: docker.socket command: start oem: id: coreos name: CoreOS Custom version-id: 310.1.0 home-url: https://coreos.com ``` ### Add an iPXE Profile iPXE profiles are used to define CoreOS boot parameters. iPXE profiles are stored under the `$COREOS_IPXE_SERVER_DATA_DIR/profiles` directory. Edit `$COREOS_IPXE_SERVER_DATA_DIR/profiles/development.json` ``` { "cloud_config": "development", "rootfstype": "btrfs", "sshkey": "coreos", "version": "310.1.0" } ``` ### Example Data Directory Layout ``` /opt/coreos-ipxe-server/ ├── configs │   └── development.yml ├── images │   └── amd64-usr │   └── 310.1.0 │   ├── coreos_production_pxe.vmlinuz │   └── coreos_production_pxe_image.cpio.gz ├── profiles │   └── development.json └── sshkeys └── coreos.pub ``` ================================================ FILE: docs/profiles.md ================================================ # Profiles iPXE profiles are used to define CoreOS kernel options used during the PXE boot process. Profiles are identified by id and are stored under the `$COREOS_IPXE_SERVER_DATA_DIR/profiles` directory. ## File Format The iPXE profile file uses the JSON file format. A iPXE profile file should contain an associative array which has zero or more of the following keys: * cloud_config * console * coreos_autologin * rootfstype * sshkey * version See [Configuring pxelinux](https://coreos.com/docs/running-coreos/bare-metal/booting-with-pxe/#configuring-pxelinux) for more details. ### Example Profile ``` $COREOS_IPXE_SERVER_DATA_DIR/profiles/development.json ``` ``` { "cloud_config": "development", "console": ["tty0", "tty1"], "coreos_autologin": "tty1", "rootfstype": "btrfs", "sshkey": "coreos", "version": "310.1.0" } ``` ================================================ FILE: docs/sshkeys.md ================================================ # SSH Public Keys SSH keys are configured via the sshkey boot parameter, which is part of the CoreOS iPXE boot script. SSH keys are identified by id and are stored under the `$COREOS_IPXE_SERVER_DATA_DIR/sshkeys` directory. Example: ``` $COREOS_IPXE_SERVER_DATA_DIR/sshkeys/coreos.pub ``` ================================================ FILE: kernel/options.go ================================================ package kernel import ( "bytes" "fmt" ) type Options struct { CloudConfig string `json:"cloud_config"` Console []string `json:"console"` CoreOSAutologin string `json:"coreos_autologin"` Root string `json:"root"` RootFstype string `json:"rootfstype"` SSHKey string `json:"sshkey"` Version string `json:"version"` cloudConfigUrl string } func New() *Options { return &Options{} } func (o *Options) SetCloudConfigUrl(url string) { o.cloudConfigUrl = url } func (o *Options) String() string { var options bytes.Buffer if o.RootFstype != "" { options.WriteString(fmt.Sprintf(" rootfstype=%s", o.RootFstype)) } for _, c := range o.Console { options.WriteString(fmt.Sprintf(" console=%s", c)) } if o.cloudConfigUrl != "" { options.WriteString(fmt.Sprintf(" cloud-config-url=%s", o.cloudConfigUrl)) } if o.CoreOSAutologin != "" { options.WriteString(fmt.Sprintf(" coreos.autologin=%s", o.CoreOSAutologin)) } if o.SSHKey != "" { options.WriteString(fmt.Sprintf(" sshkey=\"%s\"", o.SSHKey)) } if o.Root != "" { options.WriteString(fmt.Sprintf(" root=%s", o.Root)) } return options.String() } ================================================ FILE: kernel/options_test.go ================================================ package kernel import ( "testing" ) func TestDefaultOptions(t *testing.T) { want := "" o := New() options := o.String() if options != want { t.Errorf("wanted %s, got %s", want, options) } } var optionstests = []struct { cloudConfigUrl string console []string coreOSAutologin string root string rootFstype string sshKey string options string }{ { "http://host/config.yml", []string{"tty0", "ttyS0"}, "ttyS0", "", "tmpfs", "ssh-rsa AAAAB3Nza...", " rootfstype=tmpfs console=tty0 console=ttyS0 cloud-config-url=http://host/config.yml coreos.autologin=ttyS0 sshkey=\"ssh-rsa AAAAB3Nza...\"", }, { "", nil, "", "", "", "ssh-rsa AAAAB3Nza...", " sshkey=\"ssh-rsa AAAAB3Nza...\"", }, } func TestOptions(t *testing.T) { for _, tt := range optionstests { o := New() o.SetCloudConfigUrl(tt.cloudConfigUrl) if tt.console != nil { o.Console = tt.console } o.RootFstype = tt.rootFstype o.SSHKey = tt.sshKey o.CoreOSAutologin = tt.coreOSAutologin o.Root = tt.root got := o.String() if got != tt.options { t.Errorf("wanted %s, got %s", tt.options, got) } } } ================================================ FILE: main.go ================================================ // Copyright 2014 Kelsey Hightower. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "fmt" "log" "net/http" "path/filepath" "github.com/kelseyhightower/coreos-ipxe-server/config" ) func main() { for _, s := range []string{"/images/", "/configs/", "/profiles/"} { // Register static file servers. http.Handle(s, http.StripPrefix(s, http.FileServer(http.Dir(filepath.Join(config.DataDir, s))))) } // Register the sshkey script server. http.HandleFunc("/keys", sshKeyServer) // Register the iPXE boot script server. http.HandleFunc("/", ipxeBootScriptServer) // Start the iPXE Boot Server. fmt.Println("Starting CoreOS iPXE Server...") fmt.Printf("Listening on %s\n", config.ListenAddr) if config.BaseUrl != "" { fmt.Printf("Advertised URL %s\n", config.BaseUrl) } fmt.Printf("Data directory: %s\n", config.DataDir) log.Fatal(http.ListenAndServe(config.ListenAddr, nil)) }