├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── Readme.md ├── cmd ├── automate.go ├── configGenerator.go ├── plunder.go └── server.go ├── docs ├── actions.md ├── application_architecture.md ├── deployment.md ├── example_architecture.md ├── example_deployment.md ├── provisioning.md ├── readme.md └── service.md ├── go.mod ├── go.sum ├── hack └── comboot_ipxe │ ├── Dockerfile │ └── gen_comboot.ipxe ├── image ├── parlay.jpg ├── plunder_captain.png └── simple_architecture.jpg ├── main.go ├── pkg ├── apiserver │ ├── README.md │ ├── client.go │ ├── config.go │ ├── endpoints.go │ ├── go.mod │ ├── handlerApiserver.go │ ├── logging.go │ ├── loggingHandlers.go │ ├── server.go │ └── types.go ├── certs │ ├── certs.go │ └── go.mod ├── go.mod ├── parlay │ ├── go.mod │ ├── handler.go │ ├── parlay.go │ ├── parlay_ui.go │ ├── parlaytypes │ │ ├── finder.go │ │ ├── go.mod │ │ └── parlaytypes.go │ ├── parser.go │ ├── parser_builder.go │ ├── plugin │ │ └── plugin.go │ ├── restore.go │ └── validate.go ├── plunderlogging │ ├── consolelogger.go │ ├── filelogger.go │ ├── go.mod │ ├── jsonlogger.go │ └── logger.go ├── services │ ├── deployments.go │ ├── go.mod │ ├── handler.go │ ├── server.go │ ├── serverDHCP.go │ ├── serverHTTP.go │ ├── serverHTTPISO.go │ ├── serverImageHTTP.go │ ├── serverTFTP.go │ ├── services.go │ ├── static_pxe.go │ ├── templateBOOTy.go │ ├── templateESXi.go │ ├── templateKickstart.go │ ├── templatePreseed.go │ ├── templateUtils.go │ └── types.go ├── ssh │ ├── go.mod │ ├── sshClient.go │ ├── sshCommand.go │ ├── sshConfig.go │ ├── sshImport.go │ └── sshTransfer.go └── utils │ ├── go.mod │ ├── ipxe.go │ ├── nic.go │ └── utils.go ├── plugin ├── docker │ ├── docker.go │ └── docker_actions.go ├── example.go └── kubeadm │ ├── kubeadm.go │ └── kubeadm_actions.go └── testing.sh /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .plunderserver.yaml 3 | plunder 4 | plunderclient.yaml 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:experimental 2 | 3 | # Build BOOTy as an init 4 | FROM golang:1.14-alpine as dev 5 | RUN apk add --no-cache git ca-certificates make 6 | COPY . /go/src/github.com/plunder-app/plunder 7 | WORKDIR /go/src/github.com/plunder-app/plunder 8 | ENV GO111MODULE=on 9 | RUN --mount=type=cache,sharing=locked,id=gomod,target=/go/pkg/mod/cache \ 10 | --mount=type=cache,sharing=locked,id=goroot,target=/root/.cache/go-build \ 11 | CGO_ENABLED=0 GOOS=linux make build 12 | 13 | FROM scratch 14 | COPY --from=dev /go/src/github.com/plunder-app/plunder/plunder / 15 | ENTRYPOINT ["/plunder"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | SHELL := /bin/sh 3 | 4 | # The name of the executable (default is current directory name) 5 | TARGET := plunder 6 | .DEFAULT_GOAL: $(TARGET) 7 | 8 | # These will be provided to the target 9 | VERSION := 0.5.0 10 | BUILD := `git rev-parse HEAD` 11 | 12 | # Required for the move to go modules for >v0.5.0 13 | export GO111MODULE=on 14 | 15 | # Operating System Default (LINUX) 16 | TARGETOS=linux 17 | 18 | # Use linker flags to provide version/build settings to the target 19 | LDFLAGS=-ldflags "-X=main.Version=$(VERSION) -X=main.Build=$(BUILD) -s" 20 | 21 | REPOSITORY = plndr 22 | DOCKERREPO ?= $(TARGET) 23 | DOCKERTAG ?= latest 24 | 25 | .PHONY: all build clean install uninstall fmt simplify check run lint vet 26 | 27 | all: check install 28 | 29 | $(TARGET): $(SRC) 30 | @go build $(LDFLAGS) -o $(TARGET) 31 | 32 | build: $(TARGET) 33 | @true 34 | 35 | clean: 36 | @rm -f $(TARGET) 37 | 38 | install: 39 | @echo Building and Installing project 40 | @go install $(LDFLAGS) 41 | 42 | install_plugin: 43 | @make plugins 44 | @echo Installing plugins 45 | -mkdir ~/plugin 46 | -cp -pr ./plugin/*.plugin ~/plugin/ 47 | 48 | uninstall: clean 49 | @rm -f $$(which ${TARGET}) 50 | 51 | fmt: 52 | @gofmt -l -w $(SRC) 53 | 54 | vet: 55 | @go vet $(SRC) 56 | 57 | lint: 58 | @golint $(SRC) 59 | 60 | # This is typically only for quick testing 61 | dockerx86: 62 | @docker buildx build --platform linux/amd64 --load -t $(REPOSITORY)/$(TARGET):$(DOCKERTAG) -f Dockerfile . 63 | @echo New Multi Architecture Docker image created 64 | 65 | docker: 66 | @docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push -t $(REPOSITORY)/$(TARGET):$(DOCKERTAG) -f Dockerfile . 67 | @echo New Multi Architecture Docker image created 68 | 69 | plugins: 70 | @echo Building plugins 71 | @GO111Module=off go build -buildmode=plugin -o ./plugin/example.plugin ./plugin/example.go 72 | @GO111Module=off go build -buildmode=plugin -o ./plugin/kubeadm.plugin ./plugin/kubeadm/* 73 | @GO111Module=off go build -buildmode=plugin -o ./plugin/docker.plugin ./plugin/docker/* 74 | 75 | release_darwin: 76 | @echo Creating Darwin Build 77 | @GOOS=darwin make build 78 | @GOOS=darwin make plugins 79 | @zip -9 -r plunder-darwin-$(VERSION).zip ./plunder ./plugin/*.plugin 80 | @rm plunder 81 | @rm ./plugin/*.plugin 82 | 83 | release_linux: 84 | @echo Creating Linux Build 85 | @GOOS=linux make build 86 | @GOOS=linux make plugins 87 | @zip -9 -r plunder-linux-$(VERSION).zip ./plunder ./plugin/*.plugin 88 | @rm plunder 89 | @rm ./plugin/*.plugin 90 | 91 | simplify: 92 | @gofmt -s -l -w $(SRC) 93 | 94 | check: 95 | @test -z $(shell gofmt -l main.go | tee /dev/stderr) || echo "[WARN] Fix formatting issues with 'make fmt'" 96 | make lint 97 | make vet 98 | 99 | run: install 100 | @$(TARGET) 101 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Plunder 3 | 4 | The complete tool for finding **Infrastructure** gold amongst bits of bare-metal! 5 | 6 | ![Plunder Captain](./image/plunder_captain.png) 7 | 8 | ## Overview 9 | 10 | Plunder is a single-binary server that is all designed in order to make the provisioning of servers, platforms and applications easier. It is deployed as a server that an end user can interact with through it's **Api-server** in order to control and automate the usage. At this time interacting with the api-server is detailed in the source [https://github.com/plunder-app/plunder/blob/master/pkg/apiserver/endpoints.go](https://github.com/plunder-app/plunder/blob/master/pkg/apiserver/endpoints.go), however documentation will be added soon. 11 | 12 | From an end-user interaction a plunder control utility has been created: 13 | 14 | [https://github.com/plunder-app/pldrctl](https://github.com/plunder-app/pldrctl) - provides the capability to query and create deployments and configurations within a plunder instance. 15 | 16 | ### Services 17 | 18 | - `DHCP` - Allocating an IP addressing and pointing to a TFTP server 19 | - `TFTP` - Bootstrapping an Operating system install (uses iPXE) 20 | - `HTTP` - Provides a services where the bootstrap can pull the components needed for the OS install. 21 | 22 | An operating system can be easily performed using either **preseed** or **kickstart**, alternatively custom kernels and init ramdisks can be specified to be used based upon Mac address. 23 | 24 | ### Automation 25 | 26 | Further more once the operating system has been provisioned there are usually post-deployment tasks in order to complete an installation. Plunder has the capability to do the following: 27 | 28 | - `Remote command execution` - Over SSH (key configured above) 29 | - `Scripting engine` - A JSON/YAML language that also supports plugins to extend the capablities of the automation engine. 30 | 31 | A small repository of existing deployment maps has been created [https://github.com/plunder-app/maps](https://github.com/plunder-app/maps) 32 | 33 | ### Additional features 34 | 35 | - `iso support` - Plunder no longer requires a user with elevated privileges to mount an OS ISO in order to read the contents. Plunder can read files directly from the iso file and expose them to an installer through `http`. 36 | - `online updates` - As all configuration to plunder is exposed and managed through an API, it provides the capability of performing most configuration changes with no down time or restarts. 37 | - `in-memory configurations` - Plunder will create all deployment configurations and hold them in memory, meaning that it is stateless and it doesn't leave configuration all over a filesystem 38 | - `VMware deployment support` - Plunder can deploy preseed/kickstart and now vSphere installations. 39 | - `Management of unclaimed devices` - Plunder will watch and keep a pool of devices that aren't being deployed and can force them to reboot/restart until they're needed for deployment. 40 | - `Logging of remote execution` - Plunder can now store all execution logs in-memory until told to clear them. 41 | 42 | ## Getting Plunder 43 | 44 | Prebuilt binaries for Darwin(MacOS)/Linux and Windows can be found on the [releases](https://github.com/plunder-app/plunder/releases) page. 45 | 46 | ### Building 47 | 48 | If you wish to build the code yourself then this can be done simply by running: 49 | 50 | ``` 51 | go get -u github.com/plunder-app/plunder 52 | ``` 53 | Alternatively clone the repository and either `go build` or `make build`, note that using the makefile will ensure that the current git commit and version number are returned by `plunder version`. 54 | 55 | ## Usage! 56 | 57 | One of the key design concepts was to try to simplify the amount of moving parts required to bootstrap a server, therefore `plunder` aims to be a single tool that you can use. It also aims to simplify the amount of configuration files and configuration work required, it does this by auto-detecting most configuration and producing mainly completed configuration as needed. 58 | 59 | One thing to be aware of is that `plunder` doesn't require replacing anything that already exists in the infrastructure. 60 | 61 | The documentation is available [here](./docs/) 62 | 63 | ### Warning 64 | 65 | *NOTE 1* As this provides low-level networking services, only run on a network that is safe to do so. Providing DHCP on a network that already provides DHCP services can lead to un-expected behaviour (and angry network administrators) 66 | 67 | *NOTE 2* As DHCP/TFTP and HTTP all bind to low ports < 1024, root access (or sudo) is required to start the plunder services. 68 | 69 | # Troubleshooting 70 | 71 | PXE booting provides very little feedback when things aren't working, but usually the hand-off is why things wont work i.e. `DHCP` -> `TFTP` boot. Logs from `plunder` should show the hand-off from the CLI. 72 | 73 | # Roadmap 74 | 75 | - Ability to automate deployments over VMware VMTools 76 | 77 | - Windows deployments 78 | 79 | - Tidier logging 80 | 81 | - Stability enhancements 82 | 83 | - Additional plugins 84 | 85 | 86 | -------------------------------------------------------------------------------- /cmd/plunder.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/plunder-app/plunder/pkg/utils" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // Release - this struct contains the release information populated when building plunder 15 | var Release struct { 16 | Version string 17 | Build string 18 | } 19 | 20 | var plunderCmd = &cobra.Command{ 21 | Use: "plunder", 22 | Short: "This is a tool for finding gold amongst bare-metal (and provisioning kubernetes)", 23 | } 24 | 25 | var logLevel int 26 | var filePath string 27 | 28 | func init() { 29 | plunderUtilsEncode.Flags().StringVar(&filePath, "path", "", "Path to a file to encode") 30 | // Global flag across all subcommands 31 | plunderCmd.PersistentFlags().IntVar(&logLevel, "logLevel", 4, "Set the logging level [0=panic, 3=warning, 5=debug]") 32 | plunderCmd.AddCommand(plunderVersion) 33 | plunderCmd.AddCommand(plunderUtils) 34 | plunderUtils.AddCommand(plunderUtilsEncode) 35 | } 36 | 37 | // Execute - starts the command parsing process 38 | func Execute() { 39 | if os.Getenv("PLUNDER_LOGLEVEL") != "" { 40 | i, err := strconv.ParseInt(os.Getenv("PLUNDER_LOGLEVEL"), 10, 8) 41 | if err != nil { 42 | log.Fatalf("Error parsing environment variable [PLUNDER_LOGLEVEL") 43 | } 44 | // We've only parsed to an 8bit integer, however i is still a int64 so needs casting 45 | logLevel = int(i) 46 | } else { 47 | // Default to logging anything Info and below 48 | logLevel = int(log.InfoLevel) 49 | } 50 | 51 | log.SetLevel(log.Level(logLevel)) 52 | if err := plunderCmd.Execute(); err != nil { 53 | fmt.Println(err) 54 | os.Exit(1) 55 | } 56 | } 57 | 58 | var plunderVersion = &cobra.Command{ 59 | Use: "version", 60 | Short: "Version and Release information about the plunder tool", 61 | Run: func(cmd *cobra.Command, args []string) { 62 | fmt.Printf("Plunder Release Information\n") 63 | fmt.Printf("Version: %s\n", Release.Version) 64 | fmt.Printf("Build: %s\n", Release.Build) 65 | }, 66 | } 67 | 68 | var plunderUtils = &cobra.Command{ 69 | Use: "utils", 70 | Short: "Additional utilities for Plunder", 71 | Run: func(cmd *cobra.Command, args []string) { 72 | cmd.Help() 73 | }, 74 | } 75 | 76 | var plunderUtilsEncode = &cobra.Command{ 77 | Use: "encode", 78 | Short: "This will encode a file into Hex", 79 | Run: func(cmd *cobra.Command, args []string) { 80 | hex, err := utils.FileToHex(filePath) 81 | if err != nil { 82 | log.Fatalf("%v", err) 83 | } 84 | fmt.Printf("%s", hex) 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/plunder-app/plunder/pkg/apiserver" 8 | "github.com/plunder-app/plunder/pkg/parlay" 9 | "github.com/plunder-app/plunder/pkg/services" 10 | "github.com/plunder-app/plunder/pkg/utils" 11 | "github.com/spf13/cobra" 12 | 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | //var controller server.BootController 17 | var dhcpSettings services.DHCPSettings 18 | 19 | var apiServerPath, gateway, dns, startAddress, configPath, deploymentPath, defaultKernel, defaultInitrd, defaultCmdLine *string 20 | 21 | var leasecount, port *int 22 | 23 | var anyboot, insecure *bool 24 | 25 | func init() { 26 | 27 | // Prepopulate the flags with the found nic information 28 | services.Controller.AdapterName = PlunderServer.Flags().String("adapter", "", "Name of adapter to use e.g eth0, en0") 29 | 30 | services.Controller.HTTPAddress = PlunderServer.Flags().String("addressHTTP", "", "Address of HTTP to use, if blank will default to [addressDHCP]") 31 | services.Controller.TFTPAddress = PlunderServer.Flags().String("addressTFTP", "", "Address of TFTP to use, if blank will default to [addressDHCP]") 32 | 33 | services.Controller.EnableDHCP = PlunderServer.Flags().Bool("enableDHCP", false, "Enable the DCHP Server") 34 | services.Controller.EnableTFTP = PlunderServer.Flags().Bool("enableTFTP", false, "Enable the TFTP Server") 35 | services.Controller.EnableHTTP = PlunderServer.Flags().Bool("enableHTTP", false, "Enable the HTTP Server") 36 | 37 | services.Controller.PXEFileName = PlunderServer.Flags().String("iPXEPath", "undionly.kpxe", "Path to an iPXE bootloader") 38 | 39 | // DHCP Settings 40 | PlunderServer.Flags().StringVar(&services.Controller.DHCPConfig.DHCPAddress, "addressDHCP", "", "Address to advertise leases from, ideally will be the IP address of --adapter") 41 | PlunderServer.Flags().StringVar(&services.Controller.DHCPConfig.DHCPGateway, "gateway", "", "Address of Gateway to use, if blank will default to [addressDHCP]") 42 | PlunderServer.Flags().StringVar(&services.Controller.DHCPConfig.DHCPDNS, "dns", "", "Address of DNS to use, if blank will default to [addressDHCP]") 43 | PlunderServer.Flags().IntVar(&services.Controller.DHCPConfig.DHCPLeasePool, "leasecount", 20, "Amount of leases to advertise") 44 | PlunderServer.Flags().StringVar(&services.Controller.DHCPConfig.DHCPStartAddress, "startAddress", "", "Start advertised address [REQUIRED]") 45 | 46 | //HTTP Settings 47 | defaultKernel = PlunderServer.Flags().String("kernel", "", "Path to a kernel to set as the *default* kernel") 48 | defaultInitrd = PlunderServer.Flags().String("initrd", "", "Path to a ramdisk to set as the *default* ramdisk") 49 | defaultKernel = PlunderServer.Flags().String("cmdline", "", "Additional command line to pass to the *default* kernel") 50 | 51 | // Config File 52 | configPath = PlunderServer.Flags().String("config", "", "Path to a plunder server configuration") 53 | deploymentPath = PlunderServer.Flags().String("deployment", "", "Path to a plunder deployment configuration") 54 | PlunderServer.Flags().StringVar(&services.DefaultBootType, "defaultBoot", "", "In the event a boot type can't be found default to this") 55 | 56 | // API Server configuration 57 | port = PlunderServer.Flags().IntP("port", "p", 60443, "Port that the Plunder API server will listen on") 58 | insecure = PlunderServer.Flags().BoolP("insecure", "i", false, "Start the Plunder API server without encryption") 59 | apiServerPath = PlunderServer.Flags().String("path", ".plunderserver.yaml", "Path to configuration for the API Server") 60 | 61 | plunderCmd.AddCommand(PlunderServer) 62 | } 63 | 64 | // PlunderServer - This is for intialising a blank or partial configuration 65 | var PlunderServer = &cobra.Command{ 66 | Use: "server", 67 | Short: "Start Plunder Services", 68 | Run: func(cmd *cobra.Command, args []string) { 69 | log.SetLevel(log.Level(logLevel)) 70 | var deployment []byte 71 | // If deploymentPath is not blank then the flag has been used 72 | if *deploymentPath != "" { 73 | // if *anyboot == true { 74 | // log.Errorf("AnyBoot has been enabled, all configuration will be ignored") 75 | // } 76 | log.Infof("Reading deployment configuration from [%s]", *deploymentPath) 77 | if _, err := os.Stat(*deploymentPath); !os.IsNotExist(err) { 78 | deployment, err = ioutil.ReadFile(*deploymentPath) 79 | if err != nil { 80 | log.Fatalf("%v", err) 81 | } 82 | } 83 | } 84 | 85 | // if *anyboot == true { 86 | // services.AnyBoot = true 87 | // } 88 | 89 | // If configPath is not blank then the flag has been used 90 | if *configPath != "" { 91 | log.Infof("Reading configuration from [%s]", *configPath) 92 | 93 | // Check the actual path from the string 94 | if _, err := os.Stat(*configPath); !os.IsNotExist(err) { 95 | configFile, err := ioutil.ReadFile(*configPath) 96 | if err != nil { 97 | log.Fatalf("%v", err) 98 | } 99 | 100 | // Read the controller from either a yaml or json format 101 | err = services.ParseControllerData(configFile) 102 | if err != nil { 103 | log.Fatalf("%v", err) 104 | } 105 | 106 | } else { 107 | log.Fatalf("Unable to open [%s]", *configPath) 108 | } 109 | } 110 | 111 | if *services.Controller.EnableDHCP == false && *services.Controller.EnableHTTP == false && *services.Controller.EnableTFTP == false { 112 | log.Warnln("All services are currently disabled") 113 | } 114 | 115 | // If we've enabled DHCP, then we need to ensure a start address for the range is populated 116 | if *services.Controller.EnableDHCP && services.Controller.DHCPConfig.DHCPStartAddress == "" { 117 | log.Fatalln("A DHCP Start address is required") 118 | } 119 | 120 | if services.Controller.DHCPConfig.DHCPLeasePool == 0 { 121 | log.Fatalln("At least one available lease is required") 122 | } 123 | 124 | services.Controller.StartServices(deployment) 125 | 126 | // Run the API server in a seperate go routine 127 | go func() { 128 | err := apiserver.StartAPIServer(*apiServerPath, *port, *insecure) 129 | if err != nil { 130 | log.Fatalf("%v", err) 131 | } 132 | }() 133 | 134 | // Register the packages to the apiserver 135 | services.RegisterToAPIServer() 136 | parlay.RegisterToAPIServer() 137 | 138 | // Sit and wait for a control-C 139 | utils.WaitForCtrlC() 140 | 141 | return 142 | }, 143 | } 144 | -------------------------------------------------------------------------------- /docs/actions.md: -------------------------------------------------------------------------------- 1 | # Actions 2 | 3 | When a deployment is executed against a host(s) typically one or more **actions** will be performed against that host in order to configure as expected. This document details the **built-in** actions, however to extend the functionality of [plunder](github.com/plunder-app/plunder) there is the capability to extend the available actions through the use of plugins. 4 | 5 | 6 | 7 | ## Built-in Actions 8 | 9 | All actions are defined by a `type` which specifies what tasks the action will perform, also all actions should come with a `name` that identifies what the action will perform. The names should make it easy to identify relevant tasks as they're executed or when selecting individual tasks when using the user Interface. 10 | 11 | Example in json and yaml below: 12 | 13 | ```json 14 | { 15 | "task" : "command", 16 | "command" : "docker run image", 17 | "name" : "Starts the docker image \"image\"" 18 | } 19 | ``` 20 | 21 | ```yaml 22 | - task: download 23 | source: "/home/user/my_archive.tar.gz" 24 | name: "Retrieve the home archive" 25 | ``` 26 | 27 | ### Command 28 | 29 | The **command** action type is used to execute a command either locally or remote, it will exit execution if the command fails (or it can be ignored) and the results can be stored to be executed at a later point. 30 | 31 | Set the `ignoreFail` to `true` to allow execution of tasks to continue in the event that the command fails. If a long running task should be known to only execute for a specific amount of time, commands can be given a timeout which will end the command should it not complete in time. The `timeout` setting should be set in seconds which will specify how long the task is allowed to execute for. 32 | 33 | ```yaml 34 | - task: command 35 | command: "sleep 100" 36 | timeout: 99 37 | ignoreFail: true 38 | ``` 39 | 40 | *The above example will execute a sleep for a hundred seconds, however the command has a timeout set for only 99 seconds. Execution will be halted once the timeout is met, and if the task returns a fail code the execution will continue onto the next action* 41 | 42 | If a command requires elevated privileges, the `commandSudo` option allows executing a command as different user, with it's entitled privileges. 43 | 44 | **Note**: This requires `NOPASSWD` to be set for the current user. 45 | 46 | ```yaml 47 | { 48 | "task" : "command", 49 | "command" : "cat /dev/null > /var/log/messages", 50 | "name" : "Concatenate the messages file to clear space", 51 | "commandSudo" : "root" 52 | } 53 | ``` 54 | 55 | #### Using commands between actions deployments 56 | 57 | There may be a requirment to save the output of a command to be used in a different action or a different deployment, some commands will generate tokens or output that can be used at a later point. 58 | 59 | There are two options to save the output of a command: 60 | 61 | - `commandSaveFile` - saves the command output to a path 62 | - `commandSaveAsKey` - Saves the ouput in-memory under a specified `key` 63 | 64 | These saved ouputs can then be used later through the use of the `key` options: 65 | 66 | - `KeyFile` - executes the commands in the file specified under the `path` 67 | 68 | - `KeyName` - executes the commands saved in-memory under the specified `key` 69 | 70 | 71 | 72 | The below example will create a command Key under the name `joinKey` (JSON format) : 73 | 74 | ```json 75 | { 76 | "name" : "Generate a join token", 77 | "type" : "command", 78 | "command" : "kubeadm token create --print-join-command 2>/dev/null", 79 | "commandSaveAsKey" : "joinKey" 80 | } 81 | ``` 82 | 83 | 84 | 85 | This key can now be used in a different deployment with different hosts (YAML format): 86 | 87 | ```yaml 88 | - type: "command" 89 | name: "Join worker to Kubernetes cluster" 90 | keyName: "joinKey" 91 | commandSudo : "root" 92 | ``` 93 | 94 | 95 | 96 | #### Piping data between commands 97 | 98 | In the event that data needs to piped into a remote command the options `commandPipeFile` and `commandPipeCmd` can be used. The first will take the contents of `path` and pass it as `STDIN` to the command being executed under the option `command`. The `commandPipeCmd` will execute a command locally and pass the `STDOUT` of that command into the `STDIN` of the command being ran under the `command` option. 99 | 100 | 101 | 102 | The below example will run the command `echo "deb https://apt.kubernetes.io/ kubernetes-xenial main"` locally, and pass the `STDOUT` to the command `tee /etc/apt/sources.list.d/kubernetes.list` that is being ran using `sudo` privileges. 103 | 104 | ```yaml 105 | - type: command 106 | command: "tee /etc/apt/sources.list.d/kubernetes.list" 107 | commandPipeCmd: echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" 108 | name: Set Kubernetes Repository 109 | commandSudo: root 110 | 111 | ``` 112 | 113 | This is useful for a variety of usecases, although it has been found very useful for appending data to existing files that require elevated privilieges. 114 | 115 | **Example reasons for piping data to a command** 116 | 117 | The command `echo "SOME data" | tee /a/file/that/needs/sudo/privs` will fail even with `commandSudo`, the reason for this is that the `sudo` is only going to work for everything upto the pipe. The remaining part of the command will be ran as the current user and therefore doesn't have the required privileges. 118 | 119 | ### Upload / Download of files 120 | 121 | Both of the command types `upload` and `download` have the same set of options: 122 | 123 | - `destination` - Where the file will be once the `upload`/`download` has completed 124 | - `source` - The file that will be either `uploaded`/`downloaded` 125 | - `name` - Details what the action will be doing 126 | 127 | ```yaml 128 | - type: download 129 | destination: ./ubuntu.tar.gz 130 | name: Retrieve local copy of ubuntu.tar.gz 131 | source: ./ubuntu.tar.gz 132 | ``` 133 | 134 | 135 | 136 | ### Plugins 137 | 138 | Plugins allow the creation of unique actions to be performed, such as specific interactions with platforms, programs and infrastructure. All plugins will load at startup and register their actions into the parlay engine. Passing information to a plugin should be done in the following manner: 139 | 140 | ```yaml 141 | - name: Push kubernetes images for managers 142 | plugin: 143 | imageName: 144 | - k8s.gcr.io/kube-apiserver:v1.14.0 145 | - k8s.gcr.io/kube-controller-manager:v1.14.0 146 | - k8s.gcr.io/kube-scheduler:v1.14.0 147 | localSudo: true 148 | remoteSudo: true 149 | type: docker/image 150 | ``` 151 | 152 | The main differences are: 153 | 154 | - `plugin` - Contains all of the specifics that will be passed to the plugin logic 155 | - `type` - Should be the action defined by the plugin itself. 156 | 157 | 158 | 159 | 160 | 161 | ## Additonal configuration 162 | 163 | ### No Password sudo 164 | 165 | To enable password-less sudo the `/etc/sudoers` file needs modifying (DO NOT DO THIS MANUALLY). 166 | 167 | To edit the sudo file use the following command: 168 | 169 | ``` 170 | sudo visudo 171 | ``` 172 | 173 | Then add the following entry to the end of the file, replacing the `username` with the correct entry : 174 | 175 | ``` 176 | username ALL=(ALL) NOPASSWD:ALL 177 | ``` 178 | 179 | This can be tested by either opening a new session or logging out and back in and then testing that `sudo ` doesn't require a password. 180 | -------------------------------------------------------------------------------- /docs/application_architecture.md: -------------------------------------------------------------------------------- 1 | # Application Architecture 2 | 3 | The purpose of this document is to outline the architecture of the plunder program itself, as it has predominantly been developed by a single developer the logic sometimes is hard to fathom (or to understand after a period of absence) 4 | 5 | ## Application Server routine 6 | 7 | When starting plunder as a server for deployment a number of files are parsed and internal structures populated, below is a step through of the actions that take place. 8 | 9 | ### Starting the server (HTTP Enabled) 10 | 11 | We will start `plunder` with a *default* json configuration, with the services enabled and pointing to a default ubuntu kernel/initrd. The deployment file has a single server defined in it. 12 | 13 | `plunder server --config ./config.json --deployment ./deployment.json` 14 | 15 | 1. Plunder starts 16 | - parses flags 17 | - parses global `config.json` 18 | 2. Plunder will start services enabled in the configuration `controller.StartServices(deployment)` (`cmd\server.go`) 19 | 3. If a deployment file is passed then it should be parsed `err := UpdateConfiguration(deployment)` (`pkg\server\services.go`) 20 | - The parsing of this will generate strings that are mapped to urls that are tracked in a map `httpPaths` 21 | - The function `UpdateConfiguration(configFile []byte)` (`pkg/server/generator.go`) will generate these in memory by iterating through the file and checking the deployment type. 22 | 4. HTTP Server is started with `err := c.serveHTTP()`(`pkg\server\services.go`) 23 | 5. This function will create a number of prebuilt PXE boot strings using the kernels etc. from `config.json`, configurations such as `/preboot.ipxe` etc. 24 | 6. In the event that new configuration is passed to the server then steps 3 are ran again. 25 | 26 | ### Client connections 27 | 28 | 1. A Host starts and proceeds to PXE boot, by doing a DHCP request. 29 | 2. The DHCP server defaults to point the `BootFileName` Option `dhcp.OptionBootFileName:[]byte(*c.PXEFileName)`(`pkg/server/services.go`), also checks for the dhcp option `77` saying `iPXE` (which will be false) 30 | 3. This is passed of TFTP to the booting host which will start iPXE and re-do a DHCP request 31 | 4. This time however the DHCP client will have the option `77` set to `iPXE` which means that it's ready for provisioning. 32 | 5. The DHCP server will look for an existing configuration `deploymentType := FindDeployment(mac)` (`pkg/server/serve_dhcp.go`), which should return `preseed` etc. 33 | 6. The DHCP server will then look to see if a specific configuration has been created with `if httpPaths[fmt.Sprintf("%s.ipxe", dashMac)] == ""` (`pkg/server/serve_dhcp.go`), if not it will default to a deployment type 34 | 7. If there exists a pre-defined configuration then it will set the DHCP option to that. 35 | -------------------------------------------------------------------------------- /docs/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment Configuration 2 | 3 | ## Generating a configuration 4 | A `./plunder config deployment > deployment.json` will create a blank deployment configuration that can be pre-populated in order to create specific deployments. 5 | 6 | A configured deployment should resemble something like the example below: 7 | 8 | ```json 9 | { 10 | "globalConfig": { 11 | "adapter": "ens192", 12 | "gateway": "192.168.0.1", 13 | "subnet": "255.255.255.0", 14 | "nameserver": "192.168.0.1", 15 | "ntpserver": "192.168.0.1", 16 | "username": "user", 17 | "password": "pass", 18 | "repoaddress": "192.168.0.1", 19 | "mirrordir": "/ubuntu", 20 | "sshkeypath": "/home/deploy/.ssh/id_pub.rsa", 21 | "sshkey": "ssh-rsa AABBCCDDEE1122334455", 22 | "packages": "nginx openssh-server" 23 | }, 24 | "deployments": [ 25 | { 26 | "mac": "00:11:22:33:44:55", 27 | "bootConfigName": "default", 28 | "bootConfig": { 29 | "configName": "", 30 | "kernelPath": "", 31 | "initrdPath": "", 32 | "cmdline": "" 33 | }, 34 | "config": { 35 | "address": "192.168.0.2", 36 | "hostname": "Server01" 37 | } 38 | } 39 | ] 40 | } 41 | ``` 42 | 43 | ## Configuration overview 44 | 45 | The *globalConfig* is the configuration that is inherited by any of the deployment configurations where that information has been omitted, typically a lot of networking information, keys or package information will be shared amongst deployments. 46 | 47 | Placing the same information into an actual deployment will **override** the configuration inherited from the `globalConfig`. 48 | 49 | ### Shared Configuration overview 50 | 51 | - `gateway` - The gateway a server will be configured to use as default router 52 | - `subnet` - The network range server will be configured to use 53 | - `nameserver` - DNS server to resolve hostnames 54 | - `ntpserver` - The address of a timeserver 55 | - `adapter` - Which specific adapter will be configured 56 | - `swapEnabled` - Build the Operating system without swap being created 57 | - `username` - A default user that will be created 58 | - `password` - A password for the above user 59 | - `repoaddress` - The hostname/ip address of the server where the OS packages reside 60 | - `sshkeypath` - The path to an ssh key that will be added to the image for authenticating 61 | 62 | 63 | 64 | ### Deployment specific 65 | 66 | - `address` - A unique network address that will be added to the server 67 | - `hostname` - A unique hostname to be added to the provisioned server 68 | 69 | 70 | 71 | As mentioned above, a lot of fields can be ignored and the entry from the `globalConfig` will be used. 72 | 73 | 74 | 75 | ### Deployments 76 | 77 | The deployment contains things that will make a server unique! 78 | 79 | - `mac` - The unqique HW mac address of a server to configure 80 | 81 | - `kernelPath` - If a specific kernel should be used (for things like LinuxKit) 82 | 83 | - `initrdPath` - If a specific init ramdisk should be used 84 | 85 | - `cmdline` - Any arguments that should be passed to the kernel ramdisk 86 | 87 | 88 | 89 | The `deployment` specifies how the server will be provisioned, there are three options: 90 | 91 | - `preseed` Ubuntu/Debian pressed deployment 92 | - `kickstart` CentOS/RHEL deployment 93 | - `reboot` This is for servers that need to be kept on a reboot loop. 94 | 95 | 96 | 97 | The remaining `config` allows updates or overrides to the global confgiguration detailed above. 98 | 99 | 100 | 101 | ### Online updates of deployment configuration 102 | The webserver exposes a `/deployment` end point that can be used to provide an online update of the configuration, this has the following benefits: 103 | 104 | - Allows automation of updates, through things like an API call 105 | - Provides no-downtime, stopping and starting the server to load a new configuration can result in a broken installation as the network connection will be broken during restart 106 | 107 | *Retrieve the existing configuration* 108 | 109 | The currently active configuration can be retrieved through a simple get on the `/deployment` endpoint 110 | 111 | e.g. 112 | 113 | `curl -vX /deployment` 114 | 115 | *Updating the configuration* 116 | 117 | The configuration can be updated by `POST`ing the configuration JSON to the same URL. 118 | 119 | e.g. 120 | 121 | `curl -vX POST deploy01/deployment -d @deployment.json --header "Content-Type: application/json"` 122 | 123 | ## Usage 124 | 125 | With configuration for both the services and the deployments completed, they can both be passed to `plunder` in order for servers to be built. 126 | 127 | As shown below: 128 | 129 | ``` 130 | sudo ./plunder server --config ./config.json --deployment ./deployment.json --logLevel 5 131 | [sudo] password for dan: 132 | INFO[0000] Reading configuration from [./config.json] 133 | INFO[0000] Starting Remote Boot Services, press CTRL + c to stop 134 | DEBU[0000] 135 | Server IP: 192.168.1.1 136 | Adapter: ens192 137 | Start Address: 192.168.1.2 138 | Pool Size: 100 139 | 140 | INFO[0000] RemoteBoot => Starting DHCP 141 | INFO[0000] RemoteBoot => Starting TFTP 142 | DEBU[0000] 143 | Server IP: 192.168.1.1 144 | PXEFile: undionly.kpxe 145 | 146 | INFO[0000] Opening and caching undionly.kpxe 147 | INFO[0000] RemoteBoot => Starting HTTP 148 | INFO[0286] DCHP Message: Discover 149 | ``` 150 | 151 | ## Next Steps 152 | Servers that have their mac addresses in the `deployment` file will be passed the correct bootloader and they will ultimately be provisioned with the networking information as part of the configuration, they also will be provisioned with the credentials and specified ssh key. 153 | 154 | For provisioning applications or a platform details are [here](./provisioning.md). 155 | -------------------------------------------------------------------------------- /docs/example_architecture.md: -------------------------------------------------------------------------------- 1 | # Example architecture 2 | 3 | This document outlines an example architecture that one can consider when structuring or designing a network that will ultimately make use of servers bootstrapped by plunder. 4 | 5 | ## Infrastructure design 6 | 7 | In the architecture below the blue cube is the server or VM that will host Plunder and expose it's services. This machine has two adapters `ens160` and `ens192` although they could well be `eth0`/`eth1` depending on your Linux distribution. 8 | 9 | The adapter `ens160` is connected to a public network where a user can connect to it's exposed IP address (`192.168.0.100`) over a protocol such as SSH, in order to interact with the OS (and plunder). The second adapter `ens192` is connected to a private network, where a number of other hosts as repeatedly rebooting waiting for a bootstrap server to provision them. 10 | 11 | ![](../image/simple_architecture.jpg) 12 | 13 | ## Services Overview 14 | 15 | The services that plunder can expose will bind to the existing operating system in two ways. 16 | 17 | #### DHCP 18 | 19 | This will ultimately bind to an adapter, and this adapter should be configured with an address. 20 | 21 | #### TFTP 22 | 23 | This will ultimately bind to an IP address. 24 | 25 | #### HTTP 26 | 27 | This will also bind to an IP address. 28 | 29 | ## Example Plunder usage 30 | 31 | The CLI examples below don't make use of any configuration files or dynamic updates and provide a quick and easy way of exposing multiple services from Plunder. 32 | 33 | ``` 34 | sudo ./plunder server \ 35 | --adapter ens192 \ 36 | --enableDHCP \ 37 | --enableTFTP \ 38 | --enableHTTP \ 39 | --initrd initrd.gz \ 40 | --kernel kernel \ 41 | --cmdline "console=tty0" \ 42 | --addressDHCP 192.168.1.1 \ 43 | --startAddress 192.168.1.130 \ 44 | --addressTFTP 192.1.1.1 \ 45 | --addressHTTP 192.168.1.1 \ 46 | --anyboot 47 | ``` 48 | 49 | To understand the CLI line above, we will break it down into what some of the more hard-to-understand flags actually are doing. 50 | 51 | - `--adapter <...>` This specified which adapter DHCP will broadcast from 52 | - `--enableXXXX` Enable a specific service, in most cases all will be needed unless existing services already exist. 53 | - `--addressDHCP ` This is the address that should be configured on the adapter that you're binding too. 54 | - `--addressTFTP/HTTP` This can either be the same address as above or an address of an existing service 55 | - `--startAddress ` This is the beginning on the advertised DHCP addresses. 56 | 57 | **Note** `sudo` has to be used as binding to an adapter and ports <1024 requires root privileges. 58 | 59 | 60 | ## Example Plunder usage with Linuxkit 61 | 62 | If you're using [LinuxKit](https://github.com/linuxkit/linuxkit) images then they can be consumed in the same way as described above. We've simply copied the created OS image files from linuxkit and copied them to our deployment server in the `~/linuxkit/` folder. 63 | 64 | ``` 65 | sudo ./plunder server \ 66 | --adapter ens192 \ 67 | --enableDHCP \ 68 | --enableTFTP \ 69 | --enableHTTP \ 70 | --initrd linuxkit/linuxkit-initrd.img \ 71 | --kernel linuxkit/linuxkit-kernel \ 72 | --cmdline $(cat ./linuxkit/linuxkit-cmdline) \ 73 | --addressDHCP 192.168.1.1 \ 74 | --startAddress 192.168.1.130 \ 75 | --addressTFTP 192.1.1.1 \ 76 | --addressHTTP 192.168.1.1 \ 77 | --anyboot 78 | ``` 79 | -------------------------------------------------------------------------------- /docs/example_deployment.md: -------------------------------------------------------------------------------- 1 | # Example Deployment for off-line Kubernetes 2 | 3 | **This example will make use of Plunders User Interface** 4 | 5 | In order for an offline installation to be succesful, a lot of the packages and containers will need downloading to where Plunder will be ran from. 6 | 7 | ## Offline Calico parts 8 | 9 | ### Download the manifests 10 | 11 | ``` 12 | wget https://docs.projectcalico.org/v3.5/getting-started/kubernetes/installation/hosted/etcd.yaml 13 | ``` 14 | and 15 | 16 | ``` 17 | wget https://docs.projectcalico.org/v3.5/getting-started/kubernetes/installation/hosted/calico.yaml 18 | ``` 19 | 20 | ### Download the named images 21 | 22 | One Liner to pull the calico images and etcd image 23 | 24 | ``` 25 | for image in $(cat etcd.yaml | grep image | awk '{ print $2 }') ; do sudo docker pull $image; done 26 | ``` 27 | ``` 28 | for image in $(cat calico.yaml | grep image | awk '{ print $2 }') ; do sudo docker pull $image; done 29 | ``` 30 | 31 | At this point you'll have the images as part of the local docker repository and the two manifests in the local directory. 32 | 33 | ## Offline Ubuntu packages 34 | 35 | One liner to get the packages needed for the kubernetes hosts to run `kubelet` 36 | 37 | ``` 38 | apt-get download socat ethtool ebtables; tar -cvzf ubuntu_pkg.tar.gz socat* ethtool* ebtables*; rm socat* ethtool* ebtables* 39 | ``` 40 | 41 | This command will download everything needed into an archive named `ubuntu_pkg.tar.gz` 42 | 43 | ## Offline Docker packages 44 | 45 | One liner to get the docker-ce packages for all kubernetes hosts, ensure that the docker repository has been added to the hosts repositories before attempting to download the package. 46 | 47 | ``` 48 | apt-get download docker-ce=18.06.1~ce~3-0~ubuntu; tar -cvzf docker_pkg.tar.gz ./docker-ce_18.06.1~ce~3-0~ubuntu_amd64.deb; rm docker-ce_18.06.1~ce~3-0~ubuntu_amd64.deb 49 | ``` 50 | 51 | ## Offline Kubernetes packages 52 | 53 | One liner to get the kubernetes packages for all kubernetes hosts, ensure that the kubernets repository has been added to the hosts repositories before attempting to download the package. 54 | 55 | ``` 56 | apt-get download kubelet kubeadm kubectl cri-tools kubernetes-cni; tar -cvzf kubernetes_pkg.tar.gz kubelet* kubeadm* kubectl* cri-tools* kubernetes-cni*; rm kubelet* kubeadm* kubectl* cri-tools* kubernetes-cni* 57 | ``` 58 | 59 | ## Offline Kubernetes images 60 | 61 | The easiest way of managing this is to install kubeadm on the pluder host and use `kubeadm` to prep the local docker image store with the images needed. 62 | 63 | `kubeadm config images list` - will list all images 64 | 65 | `kubeadm config images pull` - will pull them all to the local host 66 | 67 | Once all of the images have been pulled locally or downloaded as tars manually from the registry we can modify out deployment map and deploy as expected. 68 | 69 | ## Example deployment map 70 | 71 | There is an example deployment map as a `gist` available [https://gist.github.com/thebsdbox/f12b621a9d3943128b6bb16688497cd0](https://gist.github.com/thebsdbox/f12b621a9d3943128b6bb16688497cd0) 72 | 73 | ## Deployment in action 74 | 75 | [![asciicast](https://asciinema.org/a/reh3reEgJQKCOB5e92D96l6tt.png)](https://asciinema.org/a/reh3reEgJQKCOB5e92D96l6tt) 76 | 77 | -------------------------------------------------------------------------------- /docs/provisioning.md: -------------------------------------------------------------------------------- 1 | # Provisioning Configuration 2 | 3 | The provisioning works by running remote commands or uploading/downloading files to a remote system, in order for it to be configured correctly. A parsing engine called "parlay" was written in order to provide repeatable scripting to ease deployments. 4 | 5 | A Deployment map can contain multiple **deployments**, which in turn will contain one or more **actions** that will be performed on one or more **hosts**. 6 | 7 | Also a deployment map can be parsed as either **JSON** or as **YAML** (yaml being somewhat easier to read as a human and creating much smaller files). 8 | 9 | ### Example deployment map 10 | 11 | This script below (for offline installations) will upload a tarball containing the docker packages and then install them on all remote systems listed under `hosts`. 12 | 13 | **Note** the tarball was created by `apt-get download docker-ce=18.06.1~ce~3-0~ubuntu; tar -cvzf docker_pkg.tar.gz ./docker-ce_18.06.1~ce~3-0~ubuntu_amd64.deb` 14 | 15 | #### JSON Example 16 | 17 | ```json 18 | { 19 | "deployments": [ 20 | { 21 | "name": "Upload Docker Packages", 22 | "parallel": false, 23 | "sessions": 0, 24 | "hosts": [ 25 | "192.168.1.3", 26 | "192.168.1.4", 27 | "192.168.1.5" 28 | ], 29 | "actions": [ 30 | { 31 | "name": "Upload Docker Packages", 32 | "type": "upload", 33 | "source": "./docker_pkg.tar.gz", 34 | "destination": "/tmp/docker_pkg.tar.gz" 35 | }, 36 | { 37 | "name": "Extract Docker packages", 38 | "type": "command", 39 | "command": "tar -C /tmp -xvzf /tmp/docker_pkg.tar.gz" 40 | }, 41 | { 42 | "name": "Install Docker packages", 43 | "type": "command", 44 | "command": "dpkg -i /tmp/docker/*", 45 | "commandSudo": "root" 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | 52 | ``` 53 | #### YAML Example 54 | 55 | ```yaml 56 | deployments: 57 | - actions: 58 | - destination: /tmp/docker_pkg.tar.gz 59 | name: Upload Docker Packages 60 | source: ./docker_pkg.tar.gz 61 | timeout: 0 62 | type: upload 63 | - command: tar -C /tmp -xvzf /tmp/docker_pkg.tar.gz 64 | name: Extract Docker packages 65 | timeout: 0 66 | type: command 67 | - command: dpkg -i /tmp/docker/* 68 | commandSudo: root 69 | name: Install Docker packages 70 | timeout: 0 71 | type: command 72 | hosts: 73 | - 192.168.1.3 74 | - 192.168.1.4 75 | - 192.168.1.5 76 | name: Upload Docker Packages 77 | parallel: false 78 | parallelSessions: 0 79 | ``` 80 | 81 | The above example only covers simple usage of `uploading` and `command` usages. 82 | 83 | 84 | 85 | ## Usage 86 | 87 | When automating a deployment ssh credentials are required to map a host with the correct credentials. 88 | 89 | To simplify this `plunder` can make use of: 90 | 91 | - A `deployment` file as detailed [here](./deployment.md), which parlay will extract the `ssh` information from to allow authentication 92 | - A **deployment endpoint**, which is effectively the url of a running plunder instance. Parlay will evaluate the endpoint for the configuration details to allow authentication. 93 | 94 | **Example** 95 | 96 | Using a map to deploy wordpress (`wordpress.yaml`) and a local deployment file. 97 | 98 | `plunder automate ssh --map ./wordpress.yaml --deployconfig ./deployment.json` 99 | 100 | Using a map to deploy wordpress (`wordpress.yaml`) and a deployment endpoint. 101 | 102 | `plunder automate ssh --map ./wordpress.yaml --deployendpoint http://localhost` 103 | 104 | It is possible to override or completely omit deployment configuration and specify the configuration at runtime through the flags `--override{Address/Keypath/Username}`. By **default** plunder will attempt to populate the Keypath and username from the current user and their `$HOME/.ssh/` directory. 105 | 106 | `/plunder automate --map ./stackedmanager.yaml --overrideAddress 192.168.1.105` 107 | 108 | Under most circumstances plunder will execute all actions in every deployment (on every host in the deployment), however it is possible to tell plunder to execute a single deployment/action from a map and on which particular host. 109 | 110 | Additional Flags: 111 | 112 | - The `--deployment` flag now will point to a specific deployment in a map 113 | - The `--action` flag can be used to point to a specific action in a deployment 114 | - The `--host` flag will point to a specific host in the deployment 115 | - The `--resume` will determine if to continue executing all remaining actions 116 | 117 | ### User Interface 118 | 119 | Plunder can also make automation easier by providing a user interface for a map and allowing the user to select which Deployments, actions and the hosts that will be acted upon. To use the user interface the subcommand `ui` should be used, all other flags are the same as above. 120 | 121 | **Example** 122 | 123 | ``` 124 | plunder automate ui --map ./stackedmanager.yaml --deployendpoint http://localhost 125 | INFO[0000] Reading deployment configuration from [./stackedmanager.yaml] 126 | ? Select deployment(s) [Use arrows to move, type to filter] 127 | > [ ] Reset any Kubernetes configuration (and remove packages) 128 | [ ] Configure host OS for kubernetes nodes 129 | [ ] Deploy Kubernetes Images for 1.14.0 130 | [ ] Initialise Kubernetes Master (1.14) 131 | [ ] Deploy Calico (3.6) 132 | ``` 133 | 134 | The UI also provides additional capability to create new maps based upon selected deployments and actions, and also to convert between formats. 135 | 136 | - `--json` Print the JSON to stdout, no execution of commands 137 | - `--yaml` Print the YAML to stdout, no execution of commands 138 | 139 | 140 | **Execution of a map is shown in the screen shot below** 141 | 142 | ![](../image/parlay.jpg) 143 | *The above example uses screen, where the output from `plunder` is on the top and `tail -f output` is below* 144 | 145 | 146 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # Plunder Usage 2 | 3 | When using `plunder` there are a few things that you will need to ensure that a configuration exists for, these things are: 4 | 5 | - Service configuration (IP Addresses, adapter names, service enablement) 6 | - Deployment configuration (MAC Addresses, Package management, networking) 7 | - Provisioning configuration (File transfer, remote command execution) 8 | 9 | Most of the configuration required will be automatically generated by `plunder` through the use of the `plunder config` sub command. 10 | 11 | To view an example architecture and quick usage than look [here](./example_architecture.md). 12 | 13 | ### Service 14 | The services such as DHCP and TFTP etc.. are the basic requirement in order to bootstrap a **blank** bare-metal server or new virtual machine. 15 | 16 | Service configuration overview and usage is located [here](./service.md). 17 | 18 | ### Deployment 19 | Once a **blank** server boots it will need an Operating System (+ packages) installing, along with setting up networking and credentials. 20 | 21 | Deployment configuration overview and usage is located [here](./deployment.md). 22 | 23 | ### Provisioning 24 | Once a server has been deployed it is on to provisioning that server for a particular use case, such as a docker swarm cluster or a kubernetes platform 25 | 26 | A Provisioning overview with usage is located [here](./provisioning.md). -------------------------------------------------------------------------------- /docs/service.md: -------------------------------------------------------------------------------- 1 | # Service Configuration 2 | 3 | ## Generating a configuration 4 | 5 | A `./plunder config server > config.json` will look at the network configuration of the current machine and build a default configuration file (in json). This file will need opening in your favourite text editor an modifying to ensure that `plunder` works correctly. 6 | 7 | ### Modifying the configuration 8 | 9 | ```json 10 | { 11 | "adapter": "en0", 12 | "enableDHCP": false, 13 | "dhcpConfig": { 14 | "addressDHCP": "192.168.0.142", 15 | "startDHCP": "192.168.0.143", 16 | "leasePoolDHCP": 20, 17 | "gatewayDHCP": "192.168.0.142", 18 | "nameserverDHCP": "192.168.0.142" 19 | }, 20 | "enableTFTP": false, 21 | "addressTFTP": "192.168.0.142", 22 | "enableHTTP": false, 23 | "addressHTTP": "192.168.0.142", 24 | "pxePath": "undionly.kpxe", 25 | "bootConfigs": [ 26 | { 27 | "configName": "default", 28 | "kernelPath": "/kernelPath", 29 | "initrdPath": "/initPath", 30 | "cmdline": "cmd=options", 31 | "isoPrefix": "ubuntu", 32 | "isoPath": "/path/to/iso" 33 | } 34 | ] 35 | } 36 | ``` 37 | 38 | *Example generated configuration above* 39 | 40 | ### Sections 41 | 42 | By **default** the configuration that is generated will have all of the services disabled (dhcp/tftp/http) and attempting to start plunder will result in an error message saying that no services are being started. 43 | 44 | #### Services 45 | 46 | The `enable` will ensure that a particular functionality is enabled within Plunder. 47 | 48 | The `addressTFTP` and `addressHTTP` are still required to be set even if you're not enabling the service, this is because those values will be passed through `DHCP` to a server that is being bootstrapped. So if `TFTP` or `HTTP` services already exist on your network, then modify those values accordingly. 49 | 50 | #### DHCP 51 | 52 | 53 | The `dhcpConfig` section details all of the configuration for the running DHCP server such as the `startDHCP` setting which should typically be `addressDHCP` +1 and then the `leasePoolDHCP` defines how many free addresses will be allocated sequentially from the start address. 54 | 55 | #### Boot Configurations 56 | 57 | The boot configurations are an array of configurations that define various remote booting configurations and are referenced via the `configName`. 58 | 59 | The `kernelPath` and `initrdPath` should point to a kernel and init ramdisk on the local filesystem that will be passed to the server once the bootloader has finished. 60 | 61 | Finally, the `isoPrefix` (determines the beginning and unique path to contents) and the `isoPath` allow OS installation content to be read from within an ISO file. 62 | 63 | e.g. 64 | 65 | `plunderAddress/isoPrefix/path/to/file` 66 | 67 | #### Additional 68 | 69 | The `pxePath` should point to an iPXE bootloader if needed, however if the file doesn't exist or if the option is blank then `plunder` will fall back to an embedded bootloader. 70 | 71 | ## Usage 72 | At this point you can start various services and you'll see servers on the network requesting `DHCP` addresses etc.. however in order to do anything we will need to configure the [deployment](./deployment.md). -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plunder-app/plunder 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.0.7 // indirect 7 | github.com/c4milo/gotoolkit v0.0.0-20190525173301-67483a18c17a // indirect 8 | github.com/ghodss/yaml v1.0.0 9 | github.com/gorilla/mux v1.7.4 // indirect 10 | github.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214 // indirect 11 | github.com/hooklift/iso9660 v1.0.0 // indirect 12 | github.com/kr/pty v1.1.8 // indirect 13 | github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 // indirect 14 | github.com/mattn/go-colorable v0.1.6 // indirect 15 | github.com/pkg/errors v0.9.1 // indirect 16 | github.com/pkg/sftp v1.11.0 // indirect 17 | github.com/plunder-app/BOOTy v0.0.0-20200513203223-f43f6ea742c4 18 | github.com/plunder-app/plunder/pkg/apiserver v0.0.0-20200514155151-dfdcaab2e5cd 19 | github.com/plunder-app/plunder/pkg/certs v0.0.0-20200514155151-dfdcaab2e5cd 20 | github.com/plunder-app/plunder/pkg/parlay v0.0.0-20200514155151-dfdcaab2e5cd 21 | github.com/plunder-app/plunder/pkg/parlay/parlaytypes v0.0.0-20200514155151-dfdcaab2e5cd 22 | github.com/plunder-app/plunder/pkg/plunderlogging v0.0.0-20200514155151-dfdcaab2e5cd // indirect 23 | github.com/plunder-app/plunder/pkg/services v0.0.0-20200514155151-dfdcaab2e5cd 24 | github.com/plunder-app/plunder/pkg/ssh v0.0.0-20200514155151-dfdcaab2e5cd 25 | github.com/plunder-app/plunder/pkg/utils v0.0.0-20200514155151-dfdcaab2e5cd 26 | github.com/sirupsen/logrus v1.6.0 27 | github.com/spf13/cobra v1.0.0 28 | github.com/spf13/pflag v1.0.5 // indirect 29 | github.com/thebsdbox/go-tftp v0.0.0-20190329154032-a7263f18c49c // indirect 30 | github.com/whyrusleeping/go-tftp v0.0.0-20180830013254-3695fa5761ee // indirect 31 | golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 // indirect 32 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120 // indirect 33 | golang.org/x/text v0.3.2 // indirect 34 | ) 35 | 36 | replace ( 37 | github.com/plunder-app/plunder/pkg/apiserver => ./pkg/apiserver 38 | github.com/plunder-app/plunder/pkg/certs => ./pkg/certs 39 | github.com/plunder-app/plunder/pkg/parlay => ./pkg/parlay 40 | github.com/plunder-app/plunder/pkg/services => ./pkg/services 41 | github.com/plunder-app/plunder/pkg/ssh => ./pkg/ssh 42 | github.com/plunder-app/plunder/pkg/utils => ./pkg/utils 43 | github.com/plunder-app/BOOTy => ../../plunder-app/BOOTy 44 | ) 45 | -------------------------------------------------------------------------------- /hack/comboot_ipxe/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gcc:latest AS IPXE_BUILD 2 | RUN git clone git://git.ipxe.org/ipxe.git 3 | RUN sed -i '/COMBOOT/s/\/\///g' ipxe/src/config/general.h 4 | WORKDIR /ipxe/src/ 5 | RUN make bin/undionly.kpxe 6 | 7 | FROM scratch 8 | COPY --from=IPXE_BUILD /ipxe/src/bin/undionly.kpxe . 9 | -------------------------------------------------------------------------------- /hack/comboot_ipxe/gen_comboot.ipxe: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo Building latest container for iPXE, with comboot support 3 | cd comboot_ipxe 4 | docker build -t ipxe_comboot . 5 | docker run -it -v $(echo $PWD):/tmp/ipxe ipxe_comboot /bin/sh -c "cp undionly.kpxe /tmp/ipxe" 6 | -------------------------------------------------------------------------------- /image/parlay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plunder-app/plunder/58306fce81c981d60bc5ab32a77a3a60aec4454e/image/parlay.jpg -------------------------------------------------------------------------------- /image/plunder_captain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plunder-app/plunder/58306fce81c981d60bc5ab32a77a3a60aec4454e/image/plunder_captain.png -------------------------------------------------------------------------------- /image/simple_architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plunder-app/plunder/58306fce81c981d60bc5ab32a77a3a60aec4454e/image/simple_architecture.jpg -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/plunder-app/plunder/cmd" 4 | 5 | // Version is populated from the Makefile and is tied to the release TAG 6 | var Version string 7 | 8 | // Build is the last GIT commit 9 | var Build string 10 | 11 | func main() { 12 | cmd.Release.Version = Version 13 | cmd.Release.Build = Build 14 | cmd.Execute() 15 | } 16 | -------------------------------------------------------------------------------- /pkg/apiserver/README.md: -------------------------------------------------------------------------------- 1 | # API Server documentation 2 | 3 | This documentation is a quick overview of the CRUD operations that take place within `plunder`, this should be a living document as the various endpoint mature over time. 4 | 5 | ## Using the API Server 6 | 7 | The API Server now starts as default and listens on a different port to HTTP services used for deployment, by default the `plunder` API server will listen on port `60443` however the `-p` `--port` flag can be used to specify a specific port. Currently the API server will bind to all interfaces. 8 | 9 | ### Starting the API Server 10 | 11 | The below example will start the API server on a custom port. 12 | 13 | ``` 14 | plunder server -p 12345 15 | ``` 16 | 17 | ## Accessing the API Server 18 | 19 | The API Endpoints should be accessed using REST methodologies and JSON payloads, the API Endpoints should **always** be defined in `endpoints.go` (this may change later). 20 | 21 | ## Current issues 22 | 23 | ### Server configuration 24 | 25 | Currently DHCP can be stopped and started but logging output is buggy, HTTP/TFTP Can be started but can't be stopped or restarted. 26 | -------------------------------------------------------------------------------- /pkg/apiserver/client.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | //FindFunctionEndpoint - will do a look up to find an exposed dynamic endpoint 17 | func FindFunctionEndpoint(u *url.URL, c *http.Client, f, m string) (*EndPoint, *Response) { 18 | // Create local URL for the API call 19 | newURL := *u 20 | newURL.Path = fmt.Sprintf("%s/%s/%s", FunctionPath(), f, m) 21 | 22 | // Interact with the API server to find the endpoint 23 | response, err := ParsePlunderGet(&newURL, c) 24 | if err != nil { 25 | 26 | return nil, &Response{ 27 | Warning: fmt.Sprintf("Unable to find method [%s] for function [%s]", m, f), 28 | Error: err.Error(), 29 | } 30 | } 31 | var ep EndPoint 32 | err = json.Unmarshal(response.Payload, &ep) 33 | if err != nil { 34 | response.Error = err.Error() 35 | return nil, response 36 | } 37 | return &ep, response 38 | } 39 | 40 | //BuildEnvironmentFromConfig will use the apiserver pkg to parse a configuration file and create a http client with the correct authentication and URL 41 | func BuildEnvironmentFromConfig(path, urlFlag string) (*url.URL, *http.Client, error) { 42 | log.Debugf("Parsing Configuration file [%s]", path) 43 | 44 | // Open the configuration 45 | c, err := openClientConfig(path) 46 | if err != nil { 47 | return nil, nil, err 48 | } 49 | // Retrieve the certificate 50 | cert, err := c.RetrieveClientCert() 51 | if err != nil { 52 | return nil, nil, err 53 | } 54 | 55 | // Build the certificate pool from the unencrypted cert 56 | caCertPool := x509.NewCertPool() 57 | caCertPool.AppendCertsFromPEM(cert) 58 | 59 | // Create a HTTPS client and supply the created CA pool 60 | client := &http.Client{ 61 | Transport: &http.Transport{ 62 | TLSClientConfig: &tls.Config{ 63 | RootCAs: caCertPool, 64 | }, 65 | }, 66 | } 67 | 68 | // Build the URL from the configuration 69 | serverURL := c.GetServerAddressURL() 70 | 71 | // Overwrite the configuration url if 72 | if urlFlag != "" { 73 | serverURL, err = url.Parse(urlFlag) 74 | if err != nil { 75 | return nil, nil, err 76 | } 77 | } 78 | 79 | return serverURL, client, nil 80 | } 81 | 82 | //ParsePlunderGet will attempt to retrieve data from the plunder API server 83 | func ParsePlunderGet(u *url.URL, c *http.Client) (*Response, error) { 84 | var response Response 85 | 86 | log.Debugf("Querying the Plunder Server [%s]", u.String()) 87 | 88 | resp, err := c.Get(u.String()) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | defer resp.Body.Close() 94 | body, err := ioutil.ReadAll(resp.Body) 95 | 96 | if resp.StatusCode > 200 { 97 | return nil, fmt.Errorf(resp.Status) 98 | } 99 | 100 | err = json.Unmarshal(body, &response) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | return &response, nil 106 | } 107 | 108 | //ParsePlunderPatch will attempt to retrieve data from the plunder API server 109 | func ParsePlunderPatch(u *url.URL, c *http.Client, data []byte) (*Response, error) { 110 | var response Response 111 | 112 | log.Debugf("Posting [%d] bytes to the Plunder Server [%s]", len(data), u.String()) 113 | 114 | req, err := http.NewRequest("PATCH", u.String(), bytes.NewBuffer(data)) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | resp, err := c.Do(req) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | defer resp.Body.Close() 125 | body, err := ioutil.ReadAll(resp.Body) 126 | 127 | if resp.StatusCode > 200 { 128 | return nil, fmt.Errorf(resp.Status) 129 | } 130 | 131 | err = json.Unmarshal(body, &response) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return &response, nil 137 | 138 | } 139 | 140 | //ParsePlunderPost will attempt to retrieve data from the plunder API server 141 | func ParsePlunderPost(u *url.URL, c *http.Client, data []byte) (*Response, error) { 142 | var response Response 143 | 144 | log.Debugf("Posting [%d] bytes to the Plunder Server [%s]", len(data), u.String()) 145 | 146 | resp, err := c.Post(u.String(), "application/json", bytes.NewBuffer(data)) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | defer resp.Body.Close() 152 | body, err := ioutil.ReadAll(resp.Body) 153 | 154 | if resp.StatusCode > 200 { 155 | return nil, fmt.Errorf(resp.Status) 156 | } 157 | 158 | err = json.Unmarshal(body, &response) 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | return &response, nil 164 | 165 | } 166 | 167 | //ParsePlunderDelete will attempt to retrieve data from the plunder API server 168 | func ParsePlunderDelete(u *url.URL, c *http.Client) (*Response, error) { 169 | var response Response 170 | 171 | log.Debugf("Requesting DELETE method to [%s]", u.String()) 172 | 173 | // Create request 174 | req, err := http.NewRequest("DELETE", u.String(), nil) 175 | if err != nil { 176 | return nil, err 177 | } 178 | req.Header.Set("Content-Type", "application/json") 179 | resp, err := c.Do(req) 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | defer resp.Body.Close() 185 | body, err := ioutil.ReadAll(resp.Body) 186 | 187 | if resp.StatusCode > 200 { 188 | return nil, fmt.Errorf(resp.Status) 189 | } 190 | 191 | err = json.Unmarshal(body, &response) 192 | if err != nil { 193 | return nil, err 194 | } 195 | 196 | return &response, nil 197 | 198 | } 199 | -------------------------------------------------------------------------------- /pkg/apiserver/config.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/url" 9 | 10 | "github.com/ghodss/yaml" 11 | ) 12 | 13 | // ClientConfig is the structure of an expected configuration for pldctl 14 | type ClientConfig struct { 15 | Address string `json:"address,omitempty"` 16 | Port int `json:"port"` 17 | ClientCert string `json:"cert"` 18 | } 19 | 20 | // ServerConfig is the structure of an expected configuration for pldctl 21 | type ServerConfig struct { 22 | ClientConfig 23 | ServerKey string `json:"key"` 24 | } 25 | 26 | //openClientConfig will open and parse a Plunder server configuration file 27 | func openClientConfig(path string) (*ClientConfig, error) { 28 | var c ClientConfig 29 | // Create a CA certificate pool and add cert.pem to it 30 | b, err := ioutil.ReadFile(path) 31 | if err != nil { 32 | return nil, err 33 | } 34 | jsonBytes, err := yaml.YAMLToJSON(b) 35 | if err == nil { 36 | // If there were no errors then the YAML => JSON was successful, no attempt to unmarshall 37 | err = json.Unmarshal(jsonBytes, &c) 38 | if err != nil { 39 | return nil, fmt.Errorf("Unable to parse configuration as either yaml or json") 40 | } 41 | } else { 42 | // Couldn't parse the yaml to JSON 43 | // Attempt to parse it as JSON 44 | err = json.Unmarshal(b, &c) 45 | if err != nil { 46 | return nil, fmt.Errorf("Unable to parse configuration as either yaml or json") 47 | } 48 | } 49 | return &c, nil 50 | } 51 | 52 | //OpenServerConfig will open and parse a Plunder server configuration file 53 | func OpenServerConfig(path string) (*ServerConfig, error) { 54 | var s ServerConfig 55 | // Create a CA certificate pool and add cert.pem to it 56 | b, err := ioutil.ReadFile(path) 57 | if err != nil { 58 | return nil, err 59 | } 60 | jsonBytes, err := yaml.YAMLToJSON(b) 61 | if err == nil { 62 | // If there were no errors then the YAML => JSON was successful, no attempt to unmarshall 63 | err = json.Unmarshal(jsonBytes, &s) 64 | if err != nil { 65 | return nil, fmt.Errorf("Unable to parse configuration as either yaml or json") 66 | } 67 | } else { 68 | // Couldn't parse the yaml to JSON 69 | // Attempt to parse it as JSON 70 | err = json.Unmarshal(b, &s) 71 | if err != nil { 72 | return nil, fmt.Errorf("Unable to parse configuration as either yaml or json") 73 | } 74 | } 75 | return &s, nil 76 | } 77 | 78 | // WriteServerConfig - will write out the server configuration for the API Server 79 | func WriteServerConfig(path, hostname, address string, port int, cert, key []byte) error { 80 | var s ServerConfig 81 | 82 | // base64 the certificates 83 | encodedKey := base64.StdEncoding.EncodeToString(key) 84 | encodedCert := base64.StdEncoding.EncodeToString(cert) 85 | 86 | // Add the encoded certificates to the struct 87 | s.ClientCert = encodedCert 88 | s.ServerKey = encodedKey 89 | 90 | // Add the port for automated startup 91 | s.Port = port 92 | 93 | // Marshall to yaml 94 | b, err := yaml.Marshal(s) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | err = ioutil.WriteFile(path, b, 0600) 100 | if err != nil { 101 | return err 102 | } 103 | return nil 104 | } 105 | 106 | // WriteClientConfig - will write out the server configuration for the API Server 107 | func WriteClientConfig(path, address string, s *ServerConfig) error { 108 | var c ClientConfig 109 | 110 | // Add the encoded certificates to the struct 111 | c.ClientCert = s.ClientCert 112 | 113 | // Add the host information for automated startup 114 | c.Port = s.Port 115 | c.Address = address 116 | 117 | // Marshall client configuration to yaml 118 | b, err := yaml.Marshal(c) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | err = ioutil.WriteFile(path, b, 0600) 124 | if err != nil { 125 | return err 126 | } 127 | return nil 128 | 129 | } 130 | 131 | //GetServerAddressURL will retrieve a parsed URL 132 | func (c *ClientConfig) GetServerAddressURL() *url.URL { 133 | var plunderURL url.URL 134 | plunderURL.Scheme = "https" 135 | // Build a url 136 | plunderURL.Host = fmt.Sprintf("%s:%d", c.Address, +c.Port) 137 | return &plunderURL 138 | } 139 | 140 | func retrieveCert(cert string) ([]byte, error) { 141 | return base64.StdEncoding.DecodeString(cert) 142 | } 143 | 144 | // RetrieveKey will decode the base64 certificate 145 | func (s *ServerConfig) RetrieveKey() ([]byte, error) { 146 | return retrieveCert(s.ServerKey) 147 | } 148 | 149 | // RetrieveClientCert will decode the base64 certificate 150 | func (s *ServerConfig) RetrieveClientCert() ([]byte, error) { 151 | return retrieveCert(s.ClientCert) 152 | } 153 | 154 | // RetrieveClientCert will decode the base64 certificate 155 | func (c *ClientConfig) RetrieveClientCert() ([]byte, error) { 156 | return retrieveCert(c.ClientCert) 157 | } 158 | -------------------------------------------------------------------------------- /pkg/apiserver/endpoints.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import ( 4 | "net/http" 5 | 6 | log "github.com/sirupsen/logrus" 7 | //"github.com/gorilla/mux" 8 | ) 9 | 10 | // EndPointManager - Contains all of the dynamically created endpoints 11 | var EndPointManager []EndPoint 12 | 13 | // EndPoint is the source of truth for handling all of the endpoints exposed through the API Server 14 | // it also provides a mechanism to interact with the apiserver to find/create api endpoints 15 | type EndPoint struct { 16 | Name string `json:"name"` 17 | Path string `json:"path"` 18 | FunctionPath string `json:"functionEndpoint"` 19 | Description string `json:"description"` 20 | Method string `json:"method"` 21 | } 22 | 23 | // AddDynamicEndpoint - will add an endpoint to the api server and link it back to a function 24 | func AddDynamicEndpoint(endpointPattern, path, description, name, method string, epFunc http.HandlerFunc) { 25 | for i := range EndPointManager { 26 | if EndPointManager[i].Name == name && EndPointManager[i].Method == method { 27 | log.Warnf("Endpoint [%s] already exists with method [%s]", name, method) 28 | } 29 | } 30 | // First we add the endpoint to the Manager so we can query it 31 | EndPointManager = append(EndPointManager, EndPoint{ 32 | FunctionPath: endpointPattern, 33 | Path: path, 34 | Description: description, 35 | Method: method, 36 | Name: name, 37 | }) 38 | // Then we add the endpoint to the apiServer 39 | endpoints.HandleFunc(endpointPattern, epFunc).Methods(method) 40 | } 41 | 42 | // GetEndpoint - will return the details for an endpoint 43 | func GetEndpoint(name, method string) *EndPoint { 44 | for i := range EndPointManager { 45 | if EndPointManager[i].Name == name && EndPointManager[i].Method == method { 46 | return &EndPointManager[i] 47 | } 48 | } 49 | return nil 50 | } 51 | 52 | // FunctionPath - this will return the api server path for any external caller using the package 53 | func FunctionPath() string { 54 | return "/api" 55 | } 56 | -------------------------------------------------------------------------------- /pkg/apiserver/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plunder-app/plunder/pkg/apiserver 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /pkg/apiserver/handlerApiserver.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/gorilla/mux" 9 | ) 10 | 11 | // Delete the parlay results from the plunder server 12 | func getAPIFunctionMethod(w http.ResponseWriter, r *http.Request) { 13 | w.Header().Set("Content-Type", "application/json") 14 | var rsp Response 15 | // Find the deployment ID 16 | f := mux.Vars(r)["function"] 17 | m := mux.Vars(r)["method"] 18 | 19 | ep := GetEndpoint(f, m) 20 | if ep == nil { 21 | // RETREIVE the deployment Logs (TODO) 22 | rsp.Warning = fmt.Sprintf("Unable to find HTTP method [%s] for function [%s]", m, f) 23 | rsp.Error = "Error looking up in API Server" 24 | } else { 25 | jsonData, err := json.Marshal(ep) 26 | if err != nil { 27 | w.Header().Set("Content-Type", "application/json") 28 | rsp.Warning = "Error retrieving deployment Configuration" 29 | rsp.Error = err.Error() 30 | } else { 31 | rsp.Payload = jsonData 32 | } 33 | } 34 | 35 | json.NewEncoder(w).Encode(rsp) 36 | } 37 | 38 | // Delete the parlay results from the plunder server 39 | func getAPIs(w http.ResponseWriter, r *http.Request) { 40 | w.Header().Set("Content-Type", "application/json") 41 | var rsp Response 42 | 43 | jsonData, err := json.Marshal(EndPointManager) 44 | if err != nil { 45 | w.Header().Set("Content-Type", "application/json") 46 | rsp.Warning = "Error retrieving deployment Configuration" 47 | rsp.Error = err.Error() 48 | } else { 49 | rsp.Payload = jsonData 50 | } 51 | 52 | json.NewEncoder(w).Encode(rsp) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/apiserver/logging.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "sync" 7 | 8 | "github.com/gorilla/mux" 9 | ) 10 | 11 | // MVP of a streaming logging provider 12 | 13 | // The notificationCenter is in charge of handling the various notification managers, whihc 14 | // in turn will notify all of their subscribers 15 | var notificationCenter map[string]*notificationManager 16 | 17 | // Notification is what will be sent to subscribers of a manager 18 | type Notification struct { 19 | ID string 20 | RawData []byte 21 | } 22 | 23 | // RegisterNotificationManager will create a manager and an endpoint 24 | func RegisterNotificationManager(managerName, endpoint string) error { 25 | // Register the new Manager to the Notification Center 26 | notificationCenter[managerName] = newNotificationManager() 27 | AddDynamicEndpoint(endpoint, 28 | endpoint, 29 | fmt.Sprintf("Automatically generated notification endpoint for [%s]", managerName), 30 | managerName, 31 | http.MethodGet, 32 | handleSubscribers(notificationCenter[managerName])) 33 | return nil 34 | } 35 | 36 | // NotifyManager - This will Notify a Manager that there is a new notification that needs to go to subscribers 37 | func NotifyManager(managerName string, n Notification) error { 38 | manager := notificationCenter[managerName] 39 | if manager == nil { 40 | return fmt.Errorf("Notification Manager [%s], hasn't been registered", managerName) 41 | } 42 | manager.notifySubscribers(n) 43 | return nil 44 | } 45 | 46 | // -------------- Notication MAGIC below -------------- 47 | 48 | func init() { 49 | // Initialise the notificationCenter map 50 | 51 | notificationCenter = make(map[string]*notificationManager) 52 | 53 | } 54 | 55 | type unsubscribeFunc func() error 56 | 57 | type subscriber interface { 58 | subscribe(n chan Notification) (unsubscribeFunc, error) 59 | } 60 | 61 | func handleSubscribers(s subscriber) http.HandlerFunc { 62 | return func(w http.ResponseWriter, r *http.Request) { 63 | // Subscribe 64 | n := make(chan Notification) 65 | unsubscribeFn, err := s.subscribe(n) 66 | if err != nil { 67 | http.Error(w, err.Error(), http.StatusInternalServerError) 68 | return 69 | } 70 | 71 | // Set environment for streaming events 72 | w.Header().Set("Content-Type", "text/event-stream") 73 | w.Header().Set("Cache-Control", "no-cache") 74 | w.Header().Set("Connection", "keep-alive") 75 | w.Header().Set("Access-Control-Allow-Origin", "*") 76 | 77 | Looping: 78 | for { 79 | select { 80 | case <-r.Context().Done(): 81 | if err := unsubscribeFn(); err != nil { 82 | http.Error(w, err.Error(), http.StatusInternalServerError) 83 | return 84 | } 85 | break Looping 86 | 87 | default: 88 | // Find the log ID 89 | id := mux.Vars(r)["id"] 90 | // retrieve the notification 91 | newNotification := <-n 92 | // compare the notification ID with that of the URL, optionally retrieve "all" notifications 93 | if newNotification.ID == id || id == "all" { 94 | // if the correct id then send them the data 95 | fmt.Fprintf(w, "%s\n", newNotification.RawData) 96 | } 97 | 98 | w.(http.Flusher).Flush() 99 | } 100 | } 101 | } 102 | } 103 | 104 | type notifier interface { 105 | Notify(n Notification) error 106 | } 107 | 108 | type notificationManager struct { 109 | subscribers map[chan Notification]struct{} 110 | subscribersMu *sync.Mutex 111 | } 112 | 113 | func newNotificationManager() *notificationManager { 114 | return ¬ificationManager{ 115 | subscribers: map[chan Notification]struct{}{}, 116 | subscribersMu: &sync.Mutex{}, 117 | } 118 | } 119 | 120 | func (nc *notificationManager) subscribe(n chan Notification) (unsubscribeFunc, error) { 121 | nc.subscribersMu.Lock() 122 | nc.subscribers[n] = struct{}{} 123 | nc.subscribersMu.Unlock() 124 | 125 | unsubscribeFn := func() error { 126 | nc.subscribersMu.Lock() 127 | delete(nc.subscribers, n) 128 | nc.subscribersMu.Unlock() 129 | 130 | return nil 131 | } 132 | 133 | return unsubscribeFn, nil 134 | } 135 | 136 | func (nc *notificationManager) notifySubscribers(n Notification) error { 137 | // Lock them until updates are complete 138 | nc.subscribersMu.Lock() 139 | defer nc.subscribersMu.Unlock() 140 | 141 | for c := range nc.subscribers { 142 | select { 143 | case c <- n: 144 | default: 145 | } 146 | } 147 | 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /pkg/apiserver/loggingHandlers.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | //var map parlay[] 4 | -------------------------------------------------------------------------------- /pkg/apiserver/server.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/gorilla/mux" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var endpoints *mux.Router 14 | 15 | func init() { 16 | // Initialise a new HTTP Router so that connections can be created before the API server starts, any registered will be added to this router and 17 | // evaluated once the API Server starts 18 | endpoints = mux.NewRouter() 19 | } 20 | 21 | //StartAPIServer - will parse a configuration file and passed variables and start the API Server 22 | func StartAPIServer(path string, port int, insecure bool) error { 23 | // Open and Parse the server configuration 24 | conf, err := OpenServerConfig(path) 25 | if err != nil { 26 | log.Warnln(err) 27 | if insecure == false { 28 | log.Warningln("Secure server enabled, but no certificates have been loaded [no communication to API server is possible]") 29 | } 30 | // Create a blank server config as one wont be returned by the above OpenFile 31 | conf = &ServerConfig{} 32 | } 33 | if port != 0 { 34 | conf.Port = port 35 | } 36 | 37 | log.Infof("Starting API server on port %d", conf.Port) 38 | address := fmt.Sprintf(":%d", conf.Port) 39 | 40 | // Add the apiserver end point 41 | AddDynamicEndpoint("/api", 42 | "/api", 43 | "Endpoint for interacting with the api server", 44 | "apis", 45 | http.MethodGet, 46 | getAPIs) 47 | 48 | // Add the apiserver end point 49 | AddDynamicEndpoint("/api/{function}/{method}", 50 | "/api", 51 | "Endpoint for interacting with the api server", 52 | "apiFunctions", 53 | http.MethodGet, 54 | getAPIFunctionMethod) 55 | 56 | // Begin the start of a secure endpoint (TODO) 57 | if insecure == false { 58 | cert, err := conf.RetrieveClientCert() 59 | if err != nil { 60 | return err 61 | } 62 | key, err := conf.RetrieveKey() 63 | if err != nil { 64 | return err 65 | } 66 | certPair, err := tls.X509KeyPair(cert, key) 67 | cfg := &tls.Config{Certificates: []tls.Certificate{certPair}} 68 | srv := &http.Server{ 69 | TLSConfig: cfg, 70 | Addr: address, 71 | Handler: endpoints, 72 | // TODO - exposing no timeout will lead to exhausted file descriptors 73 | // ReadTimeout: time.Minute, 74 | // WriteTimeout: time.Minute, 75 | } 76 | 77 | return srv.ListenAndServeTLS("", "") 78 | 79 | } 80 | 81 | // Start an insecure http server (TODO - warning) 82 | return http.ListenAndServe(address, endpoints) 83 | 84 | } 85 | -------------------------------------------------------------------------------- /pkg/apiserver/types.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import "encoding/json" 4 | 5 | //Response - This is the wrapper for responses back to a client, if any errors are created then the payload isn't guarenteed 6 | type Response struct { 7 | Warning string `json:"warning,omitempty"` // when it maybe worked 8 | Error string `json:"error,omitempty"` // when it goes wrong 9 | Success string `json:"success,omitempty"` // when it goes correct 10 | 11 | Payload json.RawMessage `json:"payload,omitempty"` 12 | } 13 | -------------------------------------------------------------------------------- /pkg/certs/certs.go: -------------------------------------------------------------------------------- 1 | package certs 2 | 3 | // generate-tls-cert generates root, leaf, and client TLS certificates. 4 | 5 | import ( 6 | "bytes" 7 | "crypto/rand" 8 | "crypto/rsa" 9 | "crypto/x509" 10 | "crypto/x509/pkix" 11 | "encoding/pem" 12 | "io/ioutil" 13 | "math/big" 14 | "os" 15 | "time" 16 | 17 | "github.com/plunder-app/plunder/pkg/utils" 18 | ) 19 | 20 | // Internal variables to hold the outputs 21 | var keyData, pemData []byte 22 | 23 | // GenerateKeyPair - (TODO) 24 | func GenerateKeyPair(hosts []string, start time.Time, length time.Duration) error { 25 | ca := &x509.Certificate{ 26 | SerialNumber: big.NewInt(2019), 27 | Subject: pkix.Name{ 28 | Organization: []string{"Plunder"}, 29 | Country: []string{"UK"}, 30 | Province: []string{""}, 31 | Locality: []string{"Yorkshire"}, 32 | StreetAddress: []string{""}, 33 | PostalCode: []string{""}, 34 | }, 35 | NotBefore: time.Now(), 36 | NotAfter: time.Now().AddDate(10, 0, 0), 37 | IsCA: true, 38 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 39 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 40 | BasicConstraintsValid: true, 41 | } 42 | 43 | // create our private and public key 44 | caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | // create the CA 50 | caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | // pem encode 56 | caPEM := new(bytes.Buffer) 57 | pem.Encode(caPEM, &pem.Block{ 58 | Type: "CERTIFICATE", 59 | Bytes: caBytes, 60 | }) 61 | 62 | caPrivKeyPEM := new(bytes.Buffer) 63 | pem.Encode(caPrivKeyPEM, &pem.Block{ 64 | Type: "RSA PRIVATE KEY", 65 | Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), 66 | }) 67 | 68 | // Find all IP addresses on a server 69 | serverAddresses, err := utils.FindAllIPAddresses() 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // Find the hostname of the server 75 | serverName, err := os.Hostname() 76 | if err != nil { 77 | return err 78 | } 79 | 80 | // set up our server certificate 81 | cert := &x509.Certificate{ 82 | SerialNumber: big.NewInt(2019), 83 | Subject: pkix.Name{ 84 | Organization: []string{"Plunder"}, 85 | Country: []string{"UK"}, 86 | Province: []string{""}, 87 | Locality: []string{"Yorkshire"}, 88 | StreetAddress: []string{""}, 89 | PostalCode: []string{""}, 90 | }, 91 | IPAddresses: serverAddresses, 92 | NotBefore: time.Now(), 93 | NotAfter: time.Now().AddDate(10, 0, 0), 94 | SubjectKeyId: []byte{1, 2, 3, 4, 6}, 95 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 96 | KeyUsage: x509.KeyUsageDigitalSignature, 97 | DNSNames: []string{serverName}, 98 | } 99 | 100 | certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | certPEM := new(bytes.Buffer) 111 | pem.Encode(certPEM, &pem.Block{ 112 | Type: "CERTIFICATE", 113 | Bytes: certBytes, 114 | }) 115 | pemData = certPEM.Bytes() 116 | 117 | certPrivKeyPEM := new(bytes.Buffer) 118 | pem.Encode(certPrivKeyPEM, &pem.Block{ 119 | Type: "RSA PRIVATE KEY", 120 | Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), 121 | }) 122 | 123 | keyData = certPrivKeyPEM.Bytes() 124 | 125 | return nil 126 | } 127 | 128 | // WriteKeyToFile - will write a generated Key to a file path 129 | func WriteKeyToFile(path string) error { 130 | 131 | err := ioutil.WriteFile(path, keyData, 0600) 132 | if err != nil { 133 | return err 134 | } 135 | return nil 136 | } 137 | 138 | // WritePemToFile - will write a generated pem to a file path 139 | func WritePemToFile(path string) error { 140 | 141 | err := ioutil.WriteFile(path, pemData, 0600) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | return nil 147 | } 148 | 149 | // GetKey - will return the []byte of the key 150 | func GetKey() []byte { 151 | return keyData 152 | } 153 | 154 | // GetPem - will return the []byte of the key 155 | func GetPem() []byte { 156 | return pemData 157 | } 158 | -------------------------------------------------------------------------------- /pkg/certs/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plunder-app/plunder/pkg/certs 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /pkg/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plunder-app/plunder/pkg 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /pkg/parlay/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plunder-app/plunder/pkg/parlay 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /pkg/parlay/handler.go: -------------------------------------------------------------------------------- 1 | package parlay 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/gorilla/mux" 10 | "github.com/plunder-app/plunder/pkg/apiserver" 11 | "github.com/plunder-app/plunder/pkg/parlay/parlaytypes" 12 | "github.com/plunder-app/plunder/pkg/services" 13 | "github.com/plunder-app/plunder/pkg/ssh" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var registered bool 18 | 19 | // RegisterToAPIServer - will add the endpoints to the API server 20 | func RegisterToAPIServer() { 21 | // Ensure registration only happens once 22 | if registered == true { 23 | return 24 | } 25 | 26 | // ------------------------------------------------ 27 | // Parlay API registration 28 | // ------------------------------------------------ 29 | 30 | apiserver.AddDynamicEndpoint("/parlay", 31 | "/parlay", 32 | "Create a parlay automation deployment", 33 | "parlay", 34 | http.MethodPost, 35 | postParlay) 36 | 37 | apiserver.AddDynamicEndpoint("/parlay/logs/{id}", 38 | "/parlay/logs", 39 | "Retrieve the logs from a parlay deployment", 40 | "parlayLog", 41 | http.MethodGet, 42 | getParlay) 43 | 44 | apiserver.AddDynamicEndpoint("/parlay/logs/{id}", 45 | "/parlay/logs", 46 | "Delete the cached logs from a specific parlay deployment", 47 | "parlayLog", 48 | http.MethodDelete, 49 | delParlay) 50 | registered = true 51 | } 52 | 53 | // Retrieve a specific plunder deployment configuration 54 | func postParlay(w http.ResponseWriter, r *http.Request) { 55 | w.Header().Set("Content-Type", "application/json") 56 | var rsp apiserver.Response 57 | 58 | if b, err := ioutil.ReadAll(r.Body); err == nil { 59 | // Parse the treasure map in the POST data 60 | var m parlaytypes.TreasureMap 61 | err := json.Unmarshal(b, &m) 62 | // Unable to parse the JSON payload 63 | if err != nil { 64 | rsp.Warning = "Error parsing the parlay actions" 65 | rsp.Error = err.Error() 66 | } else { 67 | // Parsed succesfully, we will deploy this in a go routine and use GET /parlay/MAC to view progress 68 | // 69 | err = ssh.ImportHostsFromDeployment(services.Deployments) 70 | if err != nil { 71 | rsp.Warning = "Error importing the hosts from deployment" 72 | rsp.Error = err.Error() 73 | } else { 74 | err = DeploySSH(&m, "", true, true) 75 | if err != nil { 76 | rsp.Warning = "Error performing the parlay actions" 77 | rsp.Error = err.Error() 78 | log.Errorf("%s", err.Error()) 79 | } 80 | 81 | } 82 | } 83 | } else { 84 | rsp.Warning = "Error reading HTTP data" 85 | rsp.Error = err.Error() 86 | 87 | } 88 | 89 | json.NewEncoder(w).Encode(rsp) 90 | } 91 | 92 | // Retrieve a specific parlay automation 93 | func getParlay(w http.ResponseWriter, r *http.Request) { 94 | w.Header().Set("Content-Type", "application/json") 95 | var rsp apiserver.Response 96 | // Find the deployment ID 97 | id := mux.Vars(r)["id"] 98 | 99 | // We need to revert the mac address back to the correct format (dashes back to colons) 100 | target := strings.Replace(id, "-", ".", -1) 101 | 102 | // Use the mac address to lookup the deployment 103 | logs, err := GetTargetLogs(target) 104 | // If the deployment exists then process the POST data 105 | if err != nil { 106 | // RETREIVE the deployment Logs (TODO) 107 | rsp.Warning = "Error reading Parlay Logs" 108 | rsp.Error = err.Error() 109 | } else { 110 | jsonData, err := json.Marshal(logs) 111 | if err != nil { 112 | 113 | // RETREIVE the deployment Logs (TODO) 114 | rsp.Warning = "Error parsing Parlay Logs" 115 | rsp.Error = err.Error() 116 | } else { 117 | rsp.Payload = jsonData 118 | } 119 | } 120 | 121 | json.NewEncoder(w).Encode(rsp) 122 | } 123 | 124 | // Delete the parlay results from the plunder server 125 | func delParlay(w http.ResponseWriter, r *http.Request) { 126 | w.Header().Set("Content-Type", "application/json") 127 | var rsp apiserver.Response 128 | // Find the deployment ID 129 | id := mux.Vars(r)["id"] 130 | 131 | // We need to revert the mac address back to the correct format (dashes back to colons) 132 | target := strings.Replace(id, "-", ".", -1) 133 | 134 | // Use the mac address to lookup the deployment 135 | err := DeleteTargetLogs(target) 136 | // If the deployment exists then process the POST data 137 | if err != nil { 138 | 139 | // RETREIVE the deployment Logs (TODO) 140 | rsp.Warning = "Error reading deleting logs" 141 | rsp.Error = err.Error() 142 | } 143 | 144 | json.NewEncoder(w).Encode(rsp) 145 | } 146 | -------------------------------------------------------------------------------- /pkg/parlay/parlay.go: -------------------------------------------------------------------------------- 1 | package parlay 2 | 3 | type actionType string 4 | 5 | const ( 6 | //upload - defines that this action will upload a file to a remote system 7 | upload actionType = "upload" // 8 | download actionType = "download" 9 | command actionType = "command" 10 | pkg actionType = "package" 11 | ) 12 | 13 | // KeyMap 14 | 15 | // Keys are used to store information between sessions and deployments 16 | var Keys map[string]string 17 | 18 | func init() { 19 | // Initialise the map 20 | Keys = make(map[string]string) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/parlay/parlay_ui.go: -------------------------------------------------------------------------------- 1 | package parlay 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/AlecAivazis/survey/v2" 8 | "github.com/plunder-app/plunder/pkg/parlay/parlaytypes" 9 | ) 10 | 11 | func contains(v string, a []string) bool { 12 | for _, i := range a { 13 | if strings.Contains(v, i) { 14 | return true 15 | } 16 | } 17 | return false 18 | } 19 | 20 | // StartUI will enable parlay to provide an easier way of selecting which operations will be performed 21 | func StartUI(m *parlaytypes.TreasureMap) (*parlaytypes.TreasureMap, error) { 22 | 23 | deployments := []string{} 24 | for i := range m.Deployments { 25 | deployments = append(deployments, m.Deployments[i].Name) 26 | } 27 | if len(deployments) == 0 { 28 | return nil, fmt.Errorf("No Deployments were found") 29 | } 30 | 31 | var multiQs = []*survey.Question{ 32 | { 33 | Name: "letter", 34 | Prompt: &survey.MultiSelect{ 35 | Message: "Select deployment(s)", 36 | Options: deployments, 37 | }, 38 | }, 39 | } 40 | deploymentAnswers := []string{} 41 | 42 | // ask the question 43 | err := survey.Ask(multiQs, &deploymentAnswers) 44 | 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | // Create a new TreasureMap from the answered questions 50 | newMap, err := m.FindDeployments(deploymentAnswers) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | for i := range newMap.Deployments { 56 | 57 | // Ask for Hosts 58 | multiQs[0].Prompt = &survey.MultiSelect{ 59 | Message: fmt.Sprintf("Select Hosts(s) for [%s]", newMap.Deployments[i].Name), 60 | Options: newMap.Deployments[i].Hosts, 61 | } 62 | 63 | hostAnswers := []string{} 64 | err := survey.Ask(multiQs, &hostAnswers) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | // Ask for Actions 70 | actions := []string{} 71 | for y := range newMap.Deployments[i].Actions { 72 | actions = append(actions, m.Deployments[i].Actions[y].Name) 73 | } 74 | 75 | if len(actions) == 0 { 76 | return nil, fmt.Errorf("No Deployments were found") 77 | } 78 | multiQs[0].Prompt = &survey.MultiSelect{ 79 | Message: fmt.Sprintf("Select Actions(s) for [%s]", newMap.Deployments[i].Name), 80 | Options: actions, 81 | } 82 | 83 | deploymentAnswers := []string{} 84 | err = survey.Ask(multiQs, &deploymentAnswers) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | newMap.Deployments[i].Hosts = hostAnswers 90 | foundActions, err := newMap.Deployments[i].FindActions(deploymentAnswers) 91 | if err != nil { 92 | return nil, err 93 | } 94 | newMap.Deployments[i].Actions = foundActions 95 | } 96 | 97 | return newMap, nil 98 | } 99 | -------------------------------------------------------------------------------- /pkg/parlay/parlaytypes/finder.go: -------------------------------------------------------------------------------- 1 | package parlaytypes 2 | 3 | import ( 4 | "fmt" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // FindDeployments - This will iterate through a deployment map and build a new deployment map from found deployments 10 | func (m *TreasureMap) FindDeployments(deployment []string) (*TreasureMap, error) { 11 | 12 | var newDeploymentList []Deployment 13 | 14 | for x := range deployment { 15 | for y := range m.Deployments { 16 | if m.Deployments[y].Name == deployment[x] { 17 | newDeploymentList = append(newDeploymentList, m.Deployments[y]) 18 | } 19 | } 20 | } 21 | // If this is zero it means that no deployments have been found 22 | if len(m.Deployments) == 0 { 23 | return nil, fmt.Errorf("No Deployment(s) have been found") 24 | } 25 | m.Deployments = newDeploymentList 26 | return m, nil 27 | } 28 | 29 | // FindHosts - will iterate through the deployment hosts and compare to the array of hosts to return 30 | func (d *Deployment) FindHosts(hosts []string) (*Deployment, error) { 31 | 32 | var newHostList []string 33 | 34 | for x := range hosts { 35 | for y := range d.Hosts { 36 | if d.Hosts[y] == hosts[x] { 37 | newHostList = append(newHostList, d.Hosts[y]) 38 | } 39 | } 40 | } 41 | // If this is zero it means that no hosts have been found 42 | if len(d.Hosts) == 0 { 43 | return nil, fmt.Errorf("No Host(s) have been found") 44 | } 45 | d.Hosts = newHostList 46 | return d, nil 47 | } 48 | 49 | // FindActions - will iterate through the deployment actions and compare to the array of actions to return 50 | func (d *Deployment) FindActions(actions []string) ([]Action, error) { 51 | var newActionList []Action 52 | 53 | for x := range actions { 54 | for y := range d.Actions { 55 | if d.Actions[y].Name == actions[x] { 56 | newActionList = append(newActionList, d.Actions[y]) 57 | } 58 | } 59 | } 60 | // If this is zero it means that no hosts have been found 61 | if len(d.Actions) == 0 { 62 | return nil, fmt.Errorf("No Action(s) have been found") 63 | } 64 | return newActionList, nil 65 | } 66 | 67 | //FindDeployment - takes a number of flags and builds a new map to be processed 68 | func (m *TreasureMap) FindDeployment(deployment, action, host, logFile string, resume bool) (*TreasureMap, error) { 69 | var foundMap TreasureMap 70 | if deployment != "" { 71 | log.Debugf("Looking for deployment [%s]", deployment) 72 | for x := range m.Deployments { 73 | if m.Deployments[x].Name == deployment { 74 | foundMap.Deployments = append(foundMap.Deployments, m.Deployments[x]) 75 | // Find a specific action and add or resume from 76 | if action != "" { 77 | // Clear the slice as we will be possibly adding different actions 78 | foundMap.Deployments[0].Actions = nil 79 | for y := range m.Deployments[x].Actions { 80 | if m.Deployments[x].Actions[y].Name == action { 81 | // If we're not resuming that just add the action that we want to happen 82 | if resume != true { 83 | foundMap.Deployments[0].Actions = append(foundMap.Deployments[0].Actions, m.Deployments[x].Actions[y]) 84 | } else { 85 | // Alternatively add all actions from this point 86 | foundMap.Deployments[0].Actions = m.Deployments[x].Actions[y:] 87 | } 88 | } 89 | } 90 | // If this is zero it means that no actions have been found 91 | if len(foundMap.Deployments[0].Actions) == 0 { 92 | return nil, fmt.Errorf("No actions have been found, looking for action [%s]", action) 93 | } 94 | } 95 | // If a host is specified act soley on it 96 | if host != "" { 97 | // Clear the slice as we will be possibly adding different actions 98 | foundMap.Deployments[0].Hosts = nil 99 | for y := range m.Deployments[x].Hosts { 100 | 101 | if m.Deployments[x].Hosts[y] == host { 102 | foundMap.Deployments[0].Hosts = append(foundMap.Deployments[0].Hosts, m.Deployments[x].Hosts[y]) 103 | } 104 | } 105 | // If this is zero it means that no hosts have been found 106 | if len(foundMap.Deployments[0].Hosts) == 0 { 107 | return nil, fmt.Errorf("No host has been found, looking for host [%s]", host) 108 | } 109 | } 110 | } 111 | } 112 | // If this is zero it means that no actions have been found 113 | if len(foundMap.Deployments) == 0 { 114 | return nil, fmt.Errorf("No deployment has been found, looking for deployment [%s]", deployment) 115 | } 116 | } else { 117 | return nil, fmt.Errorf("No deployment was specified") 118 | } 119 | return &foundMap, nil 120 | //return parlay.DeploySSH(foundMap, logFile, false, false) 121 | } 122 | -------------------------------------------------------------------------------- /pkg/parlay/parlaytypes/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plunder-app/plunder/pkg/parlay/parlaytypes 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /pkg/parlay/parlaytypes/parlaytypes.go: -------------------------------------------------------------------------------- 1 | package parlaytypes 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // TreasureMap - X Marks the spot 8 | // The treasure maps define the automation that will take place on the hosts defined 9 | type TreasureMap struct { 10 | // An array/list of deployments that will take places as part of this "map" 11 | Deployments []Deployment `json:"deployments"` 12 | } 13 | 14 | // Deployment defines the hosts and the action(s) that should be performed on them 15 | type Deployment struct { 16 | // Name of the deployment that is taking place i.e. (Install MySQL) 17 | Name string `json:"name"` 18 | // An array/list of hosts that these actions should be performed upon 19 | Hosts []string `json:"hosts"` 20 | 21 | // Parallel allow multiple actions across multiple hosts in parallel 22 | Parallel bool `json:"parallel"` 23 | ParallelSessions int `json:"parallelSessions"` 24 | 25 | // The actions that should be performed 26 | Actions []Action `json:"actions"` 27 | } 28 | 29 | // Action defines what the instructions that will be executed 30 | type Action struct { 31 | Name string `json:"name"` 32 | ActionType string `json:"type"` 33 | Timeout int `json:"timeout"` 34 | 35 | // File based operations 36 | Source string `json:"source,omitempty"` 37 | Destination string `json:"destination,omitempty"` 38 | FileMove bool `json:"fileMove,omitempty"` 39 | 40 | // Package manager operations 41 | PkgManager string `json:"packageManager,omitempty"` 42 | PkgOperation string `json:"packageOperation,omitempty"` 43 | Packages string `json:"packages,omitempty"` 44 | 45 | // Command operations 46 | Command string `json:"command,omitempty"` 47 | Commands []string `json:"commands,omitempty"` 48 | CommandLocal bool `json:"commandLocal,omitempty"` 49 | CommandSaveFile string `json:"commandSaveFile,omitempty"` 50 | CommandSaveAsKey string `json:"commandSaveAsKey,omitempty"` 51 | CommandSudo string `json:"commandSudo,omitempty"` 52 | 53 | // Piping commands, read in a file and send over stdin, or capture stdout from a local command 54 | CommandPipeFile string `json:"commandPipeFile,omitempty"` 55 | CommandPipeCmd string `json:"commandPipeCmd,omitempty"` 56 | 57 | // Ignore any failures 58 | IgnoreFailure bool `json:"ignoreFail,omitempty"` 59 | 60 | // Key operations 61 | KeyFile string `json:"keyFile,omitempty"` 62 | KeyName string `json:"keyName,omitempty"` 63 | 64 | //Plugin Spec 65 | Plugin json.RawMessage `json:"plugin,omitempty"` 66 | } 67 | -------------------------------------------------------------------------------- /pkg/parlay/parser_builder.go: -------------------------------------------------------------------------------- 1 | package parlay 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/plunder-app/plunder/pkg/parlay/parlaytypes" 10 | "github.com/plunder-app/plunder/pkg/ssh" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func buildCommand(a parlaytypes.Action) (string, error) { 15 | var command string 16 | 17 | // An executable Key takes presedence 18 | if a.KeyName != "" { 19 | keycmd := Keys[a.KeyName] 20 | // Check that the key exists 21 | if keycmd == "" { 22 | return "", fmt.Errorf("Unable to find command under key '%s'", a.KeyName) 23 | 24 | } 25 | if a.CommandSudo != "" { 26 | // Add sudo to the Key command 27 | command = fmt.Sprintf("sudo -n -u %s %s", a.CommandSudo, keycmd) 28 | } else { 29 | command = keycmd 30 | } 31 | } else { 32 | // Not using a key, using a shell command 33 | if a.CommandSudo != "" { 34 | // Add sudo to the Shell command 35 | command = fmt.Sprintf("sudo -n -u %s %s", a.CommandSudo, a.Command) 36 | } else { 37 | command = a.Command 38 | } 39 | } 40 | return command, nil 41 | } 42 | 43 | func parseAndExecute(a parlaytypes.Action, h *ssh.HostSSHConfig) ssh.CommandResult { 44 | // This will parse the options passed in the action and execute the required string 45 | var cr ssh.CommandResult 46 | var b []byte 47 | 48 | command, err := buildCommand(a) 49 | if err != nil { 50 | cr.Error = err 51 | return cr 52 | } 53 | 54 | if a.CommandLocal == true { 55 | log.Debugf("Command [%s]", command) 56 | cmd := exec.Command("bash", "-c", command) 57 | b, cr.Error = cmd.CombinedOutput() 58 | if cr.Error != nil { 59 | return cr 60 | } 61 | cr.Result = strings.TrimRight(string(b), "\r\n") 62 | } else { 63 | log.Debugf("Executing command [%s] on host [%s]", command, h.Host) 64 | cr = ssh.SingleExecute(command, a.CommandPipeFile, a.CommandPipeCmd, *h, a.Timeout) 65 | 66 | cr.Result = strings.TrimRight(cr.Result, "\r\n") 67 | 68 | // If the command hasn't returned anything, put a filler in 69 | if cr.Result == "" { 70 | cr.Result = "[No Output]" 71 | } 72 | if cr.Error != nil { 73 | return cr 74 | } 75 | } 76 | 77 | // Save the results into a key to be used at another point 78 | if a.CommandSaveAsKey != "" { 79 | log.Debugf("Adding new results to key [%s]", a.CommandSaveAsKey) 80 | Keys[a.CommandSaveAsKey] = cr.Result 81 | } 82 | 83 | // Save the results into a file to be used at another point 84 | if a.CommandSaveFile != "" { 85 | var f *os.File 86 | f, cr.Error = os.Create(a.CommandSaveFile) 87 | if cr.Error != nil { 88 | return cr 89 | } 90 | 91 | defer f.Close() 92 | 93 | _, cr.Error = f.WriteString(cr.Result) 94 | if cr.Error != nil { 95 | return cr 96 | } 97 | f.Sync() 98 | } 99 | 100 | return cr 101 | } 102 | -------------------------------------------------------------------------------- /pkg/parlay/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package parlayplugin 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "plugin" 9 | 10 | "github.com/plunder-app/plunder/pkg/parlay/parlaytypes" 11 | 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // The pluginCache contains a map of action->plugin 16 | var pluginCache map[string]string 17 | 18 | func init() { 19 | // Initialise the map 20 | pluginCache = make(map[string]string) 21 | } 22 | 23 | // Find plugins returns an array of all .plugin files 24 | func findPlugins(pluginDir string) ([]string, error) { 25 | var plugins []string 26 | // This function will look for all files in a specified directory (defaults to PWD/plugin) 27 | filepath.Walk(pluginDir, func(path string, f os.FileInfo, err error) error { 28 | if err != nil { 29 | return err 30 | } 31 | if !f.IsDir() { 32 | if filepath.Ext(path) == ".plugin" { 33 | absPath, _ := filepath.Abs(path) 34 | 35 | plugins = append(plugins, absPath) 36 | } 37 | } 38 | return nil 39 | }) 40 | return plugins, nil 41 | } 42 | 43 | func findFunctionInPlugin(pluginPath, functionName string) (plugin.Symbol, error) { 44 | 45 | plug, err := plugin.Open(pluginPath) 46 | if err != nil { 47 | log.Debugf("%v", err) 48 | return nil, fmt.Errorf("Unable to open Plugin [%s]", pluginPath) 49 | 50 | } 51 | 52 | symbol, err := plug.Lookup(functionName) 53 | if err != nil { 54 | log.Debugf("%v", err) 55 | return nil, fmt.Errorf("Unable to read functions from Plugin [%s]", pluginPath) 56 | } 57 | 58 | return symbol, nil 59 | } 60 | 61 | func init() { 62 | 63 | pluginList, err := findPlugins("./plugin") 64 | if err != nil { 65 | log.Errorf("%v", err) 66 | } else { 67 | log.Debugf("Found [%d] plugins", len(pluginList)) 68 | for x := range pluginList { 69 | symbol, err := findFunctionInPlugin(pluginList[x], "ParlayActionList") 70 | if err != nil { 71 | log.Errorf("%v", err) 72 | continue 73 | } 74 | 75 | pluginExec, ok := symbol.(func() []string) 76 | if !ok { 77 | log.Errorf("Unable to read functions from Plugin [%s]", pluginList[x]) 78 | continue 79 | } 80 | 81 | actions := pluginExec() 82 | 83 | for z := range actions { 84 | // This will give us a mapping of "action" => plugin 85 | pluginCache[actions[z]] = pluginList[x] 86 | } 87 | } 88 | } 89 | } 90 | 91 | //ListPlugins - 92 | func ListPlugins() { 93 | 94 | pluginList, err := findPlugins("./plugin") 95 | if err != nil { 96 | log.Errorf("%v", err) 97 | } else { 98 | log.Debugf("Found [%d] plugins", len(pluginList)) 99 | for x := range pluginList { 100 | symbol, err := findFunctionInPlugin(pluginList[x], "ParlayPluginInfo") 101 | if err != nil { 102 | log.Errorf("%v", err) 103 | continue 104 | } 105 | 106 | pluginExec, ok := symbol.(func() string) 107 | if !ok { 108 | log.Errorf("Unable to read functions from Plugin [%s]", pluginList[x]) 109 | continue 110 | } 111 | sanitizedPath := filepath.Base(pluginList[x]) 112 | fmt.Printf("%s\t%s\n", sanitizedPath, pluginExec()) 113 | } 114 | } 115 | } 116 | 117 | //ListPluginActions - 118 | func ListPluginActions(pluginPath string) { 119 | 120 | symbol, err := findFunctionInPlugin(pluginPath, "ParlayActionList") 121 | if err != nil { 122 | log.Errorf("%v", err) 123 | return 124 | } 125 | 126 | pluginExec, ok := symbol.(func() []string) 127 | if !ok { 128 | log.Errorf("Unable to read functions from Plugin [%s]", pluginPath) 129 | return 130 | } 131 | 132 | actions := pluginExec() 133 | 134 | symbol, err = findFunctionInPlugin(pluginPath, "ParlayActionDetails") 135 | if err != nil { 136 | log.Errorf("%v", err) 137 | return 138 | } 139 | 140 | pluginExec, ok = symbol.(func() []string) 141 | if !ok { 142 | log.Errorf("Unable to read functions from Plugin [%s]", pluginPath) 143 | return 144 | } 145 | 146 | descriptions := pluginExec() 147 | 148 | if len(actions) != len(descriptions) { 149 | log.Warnf("Not all actions have descriptions, contact your plugin provider to have this fixed") 150 | } 151 | 152 | for x := range actions { 153 | fmt.Printf("%s\t%s\n", actions[x], descriptions[x]) 154 | } 155 | } 156 | 157 | //UsagePlugin returns the usage of a plugin function 158 | func UsagePlugin(pluginPath, action string) { 159 | 160 | symbol, err := findFunctionInPlugin(pluginPath, "ParlayUsage") 161 | if err != nil { 162 | log.Errorf("%v", err) 163 | return 164 | } 165 | 166 | pluginExec, ok := symbol.(func(string) (json.RawMessage, error)) 167 | if !ok { 168 | log.Errorf("Unable to read functions from Plugin [%s]", pluginPath) 169 | return 170 | } 171 | result, err := pluginExec(action) 172 | if err != nil { 173 | log.Errorf("%v", err) 174 | return 175 | } 176 | 177 | a := parlaytypes.Action{ 178 | Name: fmt.Sprintf("Example name for action [%s]", action), 179 | ActionType: action, 180 | Plugin: result, 181 | } 182 | b, _ := json.MarshalIndent(a, "", "\t") 183 | fmt.Printf("%s\n", b) 184 | } 185 | 186 | // ExecuteAction uses the cache to find an action/plugin mapping 187 | func ExecuteAction(action, host string, raw json.RawMessage) ([]parlaytypes.Action, error) { 188 | if pluginCache[action] == "" { 189 | // No KeyMap meaning that the action doesn't map to a plugin 190 | return nil, fmt.Errorf("Action [%s] does not exist or has no plugin associated with it", action) 191 | } 192 | return ExecuteActionInPlugin(pluginCache[action], action, host, raw) 193 | } 194 | 195 | // ExecuteActionInPlugin specifies the plugin and action directly 196 | func ExecuteActionInPlugin(pluginPath, action, host string, raw json.RawMessage) ([]parlaytypes.Action, error) { 197 | 198 | // Check a function with the name ParlayExec exists 199 | symbol, err := findFunctionInPlugin(pluginPath, "ParlayExec") 200 | if err != nil { 201 | return nil, fmt.Errorf("%v", err) 202 | } 203 | log.Debugf("Attempting plugin [%s]", action) 204 | // Check the function has the correct parameters 205 | pluginExec, ok := symbol.(func(string, string, json.RawMessage) ([]parlaytypes.Action, error)) 206 | if !ok { 207 | return nil, fmt.Errorf("Unable to read functions from Plugin [%s]", pluginPath) 208 | } 209 | 210 | // Pass the action type and the interface to the plugin 211 | return pluginExec(action, host, raw) 212 | } 213 | -------------------------------------------------------------------------------- /pkg/parlay/restore.go: -------------------------------------------------------------------------------- 1 | package parlay 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/mitchellh/go-homedir" 9 | ) 10 | 11 | //Restore provides a checkpoint to resume from 12 | type Restore struct { 13 | Deployment string `json:"deployment"` // Name of deployment to restore from 14 | Action string `json:"action"` // Action to restore from 15 | Host string `json:"host"` // Single host to start from 16 | Hosts []string `json:"hosts"` // Restart operation on a number of hosts 17 | } 18 | 19 | // restore is an interal struct used for execution restoration 20 | var restore Restore 21 | 22 | const restoreFile = ".parlay_restore" 23 | 24 | // restoreFilePath will build a path where a file will be read/writted 25 | func restoreFilePath() (string, error) { 26 | home, err := homedir.Dir() 27 | if err != nil { 28 | return "", err 29 | } 30 | return home + "/" + restoreFile, nil 31 | } 32 | 33 | func (r *Restore) createCheckpoint() error { 34 | // This function will create a checkpoint file that will allow Plunder to restart in the event of failure 35 | path, err := restoreFilePath() 36 | if err != nil { 37 | return err 38 | } 39 | 40 | // Marshall the struct to a byte array 41 | b, err := json.Marshal(r) 42 | if err != nil { 43 | return err 44 | } 45 | // Write the checkpoint file 46 | err = ioutil.WriteFile(path, b, 0644) 47 | 48 | return err 49 | } 50 | 51 | //RestoreFromCheckpoint will attempt to find a restoration checkpoint file 52 | func RestoreFromCheckpoint() *Restore { 53 | path, err := restoreFilePath() 54 | if err != nil { 55 | return nil 56 | } 57 | if _, err := os.Stat(path); !os.IsNotExist(err) { 58 | b, err := ioutil.ReadFile(path) 59 | if err != nil { 60 | return nil 61 | } 62 | var r Restore 63 | err = json.Unmarshal(b, &r) 64 | if err != nil { 65 | return nil 66 | } 67 | return &r 68 | } 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/parlay/validate.go: -------------------------------------------------------------------------------- 1 | package parlay 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/plunder-app/plunder/pkg/parlay/parlaytypes" 7 | ) 8 | 9 | // ValidateAction will parse an action to ensure it is valid 10 | func ValidateAction(action *parlaytypes.Action) error { 11 | switch action.ActionType { 12 | case "upload": 13 | // Validate the upload action 14 | if action.Source == "" { 15 | return fmt.Errorf("The Source field can not be blank") 16 | } 17 | 18 | if action.Destination == "" { 19 | return fmt.Errorf("The Destination field can not be blank") 20 | } 21 | return nil 22 | case "download": 23 | // Validate the download action 24 | if action.Source == "" { 25 | return fmt.Errorf("The Source field can not be blank") 26 | } 27 | 28 | if action.Destination == "" { 29 | return fmt.Errorf("The Destination field can not be blank") 30 | } 31 | return nil 32 | case "command": 33 | // Validate the Command action 34 | if action.Command == "" && action.KeyName == "" { 35 | return fmt.Errorf("Neither a command or a key has been specified to execute") 36 | } 37 | if action.Command != "" && action.KeyName != "" { 38 | return fmt.Errorf("Unable to use both a Command and a Command Key") 39 | } 40 | 41 | return nil 42 | case "pkg": 43 | // Validate the Package action 44 | if action.PkgManager == "" { 45 | return fmt.Errorf("The Package Manager field can not be blank") 46 | } else if action.PkgManager != "apt" && action.PkgManager != "yum" { 47 | return fmt.Errorf("Unknown Package Manager [%s]", action.PkgManager) 48 | } 49 | 50 | if action.PkgOperation == "" { 51 | return fmt.Errorf("The Package Operation field can not be blank") 52 | } else if action.PkgOperation != "install" && action.PkgOperation != "remove" { 53 | return fmt.Errorf("Unknown Package Operation [%s]", action.PkgOperation) 54 | } 55 | 56 | if action.Packages == "" { 57 | return fmt.Errorf("The Packages field can not be blank") 58 | } 59 | return nil 60 | case "key": 61 | // Validate the Key action 62 | if action.KeyFile == "" { 63 | return fmt.Errorf("The KeyField field can not be blank") 64 | } 65 | return nil 66 | default: 67 | return fmt.Errorf("Unknown Action [%s]", action.ActionType) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/plunderlogging/consolelogger.go: -------------------------------------------------------------------------------- 1 | package plunderlogging 2 | -------------------------------------------------------------------------------- /pkg/plunderlogging/filelogger.go: -------------------------------------------------------------------------------- 1 | package plunderlogging 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | ) 8 | 9 | // FileLogger allows parlay to log output to a file on the local filesystem 10 | type FileLogger struct { 11 | enabled bool 12 | f *os.File 13 | } 14 | 15 | var fileLogging FileLogger 16 | 17 | func (l *FileLogger) initFileLogger(logFile string) (err error) { 18 | l.enabled = true 19 | l.f, err = os.Create(logFile) 20 | if err != nil { 21 | return err 22 | } 23 | return nil 24 | } 25 | 26 | // This file based logging function may error, but logging should never break the running of a system, so errors are passed to "Debug" logging 27 | func (l *FileLogger) writeEntry(target, entry string) error { 28 | var fileMutex sync.Mutex 29 | if l.enabled == true { 30 | 31 | // As this may be called by numerous goroutines, we impose a mutex lock on it 32 | fileMutex.Lock() 33 | defer fileMutex.Unlock() 34 | 35 | // TODO - Does this produce readable logging output 36 | _, err := l.f.WriteString(fmt.Sprintf("Target=%s Entry=%s", target, entry)) 37 | 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | func (l *FileLogger) setLoggingState(target, state string) error { 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/plunderlogging/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plunder-app/plunder/pkg/plunderlogging 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /pkg/plunderlogging/jsonlogger.go: -------------------------------------------------------------------------------- 1 | package plunderlogging 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // JSONLogger allows parlay to log output to an in-memory jsonStruct 11 | type JSONLogger struct { 12 | enabled bool 13 | logger map[string]*JSONLog 14 | } 15 | 16 | // JSONLog contains all of the output from a parlay execution 17 | type JSONLog struct { 18 | State string `json:"state"` 19 | Entries []JSONLogEntry `json:"entries"` 20 | } 21 | 22 | // JSONLogEntry contains the details a specific action 23 | type JSONLogEntry struct { 24 | Created time.Time `json:"created"` 25 | TaskName string `json:"task"` 26 | Err string `json:"error"` 27 | Entry string `json:"entry"` 28 | } 29 | 30 | func (j *JSONLogger) initJSONLogger() { 31 | j.enabled = true 32 | j.logger = make(map[string]*JSONLog) 33 | } 34 | 35 | func (j *JSONLogger) writeEntry(target, task, entry, err string) { 36 | // Create new entry 37 | newEntry := JSONLogEntry{ 38 | Created: time.Now(), 39 | Entry: entry, 40 | TaskName: task, 41 | Err: err, 42 | } 43 | 44 | // Check if the logger exists 45 | existingLog, ok := j.logger[target] 46 | if ok { 47 | // Update an existing entry 48 | existingLog.Entries = append(existingLog.Entries, newEntry) 49 | } else { 50 | // Create a new logger 51 | newLog := JSONLog{ 52 | State: "Running", 53 | } 54 | // Append the entry to it 55 | newLog.Entries = append(newLog.Entries, newEntry) 56 | // Update the in-memory log store 57 | j.logger[target] = &newLog 58 | log.Debugf("Creating new logs for target [%s]", target) 59 | 60 | } 61 | } 62 | 63 | func (j *JSONLogger) deleteLog(target string) error { 64 | // Check if the entry exists 65 | _, ok := j.logger[target] 66 | if ok { 67 | // If it does, then we use the in-built function to delete the log entry 68 | delete(j.logger, target) 69 | } else { 70 | // Return a warning 71 | return fmt.Errorf("In-Memory logging for [%s] either doesn't exist or has already been deleted", target) 72 | } 73 | return nil 74 | } 75 | 76 | func (j *JSONLogger) setLoggingState(target, state string) error { 77 | // Check if the logger exists 78 | existingLog, ok := j.logger[target] 79 | if ok { 80 | // Update an existing entry 81 | existingLog.State = state 82 | } else { 83 | return fmt.Errorf("In-Memory logging for [%s] either doesn't exist or has already been deleted", target) 84 | } 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /pkg/plunderlogging/logger.go: -------------------------------------------------------------------------------- 1 | package plunderlogging 2 | 3 | import "fmt" 4 | 5 | // Logger - is a stuct that manages the verious types of logger available 6 | type Logger struct { 7 | json JSONLogger 8 | file FileLogger 9 | } 10 | 11 | // EnableJSONLogging - will enable logging through JSON 12 | func (l *Logger) EnableJSONLogging(e bool) { 13 | l.json.enabled = e 14 | l.json.initJSONLogger() 15 | } 16 | 17 | // EnableFileLogging - will enable logging to a file 18 | func (l *Logger) EnableFileLogging(e bool) { 19 | l.file.enabled = e 20 | } 21 | 22 | // InitLogFile - will initialise file based logging 23 | func (l *Logger) InitLogFile(path string) error { 24 | if l.file.enabled != true { 25 | return l.file.initFileLogger(path) 26 | } 27 | // Dont re-initialise the file 28 | return nil 29 | 30 | } 31 | 32 | // InitJSON - will start/initialise the JSON logging functionality 33 | func (l *Logger) InitJSON() { 34 | // Dont re-initialise the json 35 | 36 | if l.json.enabled != true { 37 | l.json.initJSONLogger() 38 | } 39 | 40 | } 41 | 42 | // target - the entity we're affecting 43 | // entry - the results of the operation on the target 44 | 45 | // WriteLogEntry will capture what is transpiring and where 46 | func (l *Logger) WriteLogEntry(target, task, entry, err string) { 47 | if l.file.enabled { 48 | l.file.writeEntry(target, entry) 49 | } 50 | if l.json.enabled { 51 | l.json.writeEntry(target, task, entry, err) 52 | } 53 | 54 | // A logging system shouldnt break anything so any errors are just outputed to STDOUT 55 | 56 | } 57 | 58 | // SetLoggingState - currently a NOOP (TODO) 59 | func (l *Logger) SetLoggingState(target, state string) { 60 | if l.file.enabled { 61 | l.file.setLoggingState(target, state) 62 | } 63 | if l.json.enabled { 64 | l.json.setLoggingState(target, state) 65 | } 66 | 67 | // A logging system shouldnt break anything so any errors are just outputed to STDOUT 68 | 69 | } 70 | 71 | // GetJSONLogs - returns a pointer to the current JSON Logs 72 | func (l *Logger) GetJSONLogs(target string) (*JSONLog, error) { 73 | if l.json.logger == nil { 74 | return nil, fmt.Errorf("JSON Logging hasn't been enabled") 75 | } 76 | // Check if the logger exists 77 | existingLog, ok := l.json.logger[target] 78 | if ok { 79 | return existingLog, nil 80 | } 81 | return nil, fmt.Errorf("No Logs for Target [%s] exist", target) 82 | } 83 | 84 | // DeleteLogs - will remove logs for a particular target 85 | func (l *Logger) DeleteLogs(target string) error { 86 | if l.json.logger == nil { 87 | return nil 88 | } 89 | return l.json.deleteLog(target) 90 | 91 | } 92 | -------------------------------------------------------------------------------- /pkg/services/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plunder-app/plunder/pkg/services 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /pkg/services/server.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/ghodss/yaml" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // This is needed by other functions to build strings 12 | var httpAddress string 13 | 14 | // Controller contains all the "current" settings for booting servers 15 | var Controller BootController 16 | 17 | // Deployments - contains an accessible "current" configuration for all deployments 18 | var Deployments DeploymentConfigurationFile 19 | 20 | // ParseControllerData will read in a byte array and attempt to parse it as yaml or json 21 | func ParseControllerData(b []byte) error { 22 | 23 | jsonBytes, err := yaml.YAMLToJSON(b) 24 | if err == nil { 25 | // If there were no errors then the YAML => JSON was successful, no attempt to unmarshall 26 | err = json.Unmarshal(jsonBytes, &Controller) 27 | if err != nil { 28 | return fmt.Errorf("Unable to parse configuration as either yaml or json") 29 | } 30 | 31 | } else { 32 | // Couldn't parse the yaml to JSON 33 | // Attempt to parse it as JSON 34 | err = json.Unmarshal(b, &Controller) 35 | if err != nil { 36 | return fmt.Errorf("Unable to parse configuration as either yaml or json") 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | // Parse will read through a new configuration and implement the configuration if possible 43 | func (b *BootConfig) Parse() error { 44 | if isoMapper == nil { 45 | // Ensure it is initialised before trying to use it 46 | isoMapper = make(map[string]string) 47 | } 48 | 49 | if b.ISOPrefix == "" || b.ISOPath == "" { 50 | log.Debugf("No ISO is being parsed for configuration %s", b.ConfigName) 51 | } else { 52 | // Atempt to open the ISO and add it to the map for usage later 53 | err := OpenISO(b.ISOPath, b.ISOPrefix) 54 | if err != nil { 55 | log.Errorf("Error parsing ISO [%v]", err) 56 | return err 57 | } 58 | 59 | // Create the prefix 60 | urlPrefix := fmt.Sprintf("/%s/", b.ISOPrefix) 61 | 62 | // Only create the handler if one doesn't exist 63 | if _, ok := isoMapper[b.ISOPrefix]; !ok { 64 | log.Debugf("Adding handler %s", urlPrefix) 65 | serveMux.HandleFunc(urlPrefix, isoReader) 66 | 67 | // Add the iso path to the correct prefix 68 | isoMapper[b.ISOPrefix] = b.ISOPath 69 | } 70 | 71 | log.Debugf("Updating handler %s for config %s", urlPrefix, b.ConfigName) 72 | } 73 | log.Infof("Boot Config [%s] of type [%s] parsed succesfully", b.ConfigName, b.ConfigType) 74 | // No errors and BootConfig is applied 75 | return nil 76 | } 77 | 78 | // // ParseBootController - will iterate through the boot controller and see if any changes need applying 79 | // // this is mainly for the dynamic loading of ISOs 80 | // func (c *BootController) ParseBootController() error { 81 | 82 | // for i := range c.BootConfigs { 83 | // // If either the prefix or path are blank then iterate over, both need to be set in order to load the ISO 84 | // if c.BootConfigs[i].ISOPrefix == "" || c.BootConfigs[i].ISOPath == "" { 85 | // log.Debugf("No ISO is being parsed for configuration %s", c.BootConfigs[i].ConfigName) 86 | // } else { 87 | // // Atempt to open the ISO and add it to the map for usage later 88 | // err := OpenISO(c.BootConfigs[i].ISOPath, c.BootConfigs[i].ISOPrefix) 89 | // if err != nil { 90 | // log.Errorf("Error parsing ISO [%v]", err) 91 | // return err 92 | // } 93 | 94 | // // Create the prefix 95 | // urlPrefix := fmt.Sprintf("/%s/", c.BootConfigs[i].ISOPrefix) 96 | 97 | // // Only create the handler if one doesn't exist 98 | // if _, ok := isoMapper[c.BootConfigs[i].ISOPrefix]; !ok { 99 | // log.Debugf("Adding handler %s", urlPrefix) 100 | 101 | // serveMux.HandleFunc(urlPrefix, isoReader) 102 | // } 103 | 104 | // log.Debugf("Updating handler %s for config %s", urlPrefix, c.BootConfigs[i].ConfigName) 105 | 106 | // } 107 | // } 108 | // // Parse the boot controllers for new configuration changes 109 | // c.generateBootTypeHanders() 110 | // return nil 111 | // } 112 | 113 | // DeleteBootControllerConfig - will iterate through the boot controller and see if any changes need applying 114 | // this is mainly for the dynamic loading of ISOs 115 | func (c *BootController) DeleteBootControllerConfig(configName string) error { 116 | 117 | for i := range c.BootConfigs { 118 | if c.BootConfigs[i].ConfigName == configName { 119 | // Remove the mapping to an ISO path 120 | if isoMapper != nil { 121 | // Ensure it is initialised before trying to remove boot config 122 | isoMapper[c.BootConfigs[i].ISOPrefix] = "" 123 | } 124 | c.BootConfigs = append(c.BootConfigs[:i], c.BootConfigs[i+1:]...) 125 | return nil 126 | } 127 | } 128 | return fmt.Errorf("Unable to find boot configuration %s", configName) 129 | } 130 | 131 | // ParseDeployment will read in a byte array and attempt to parse it as yaml or json 132 | func ParseDeployment(b []byte) (*DeploymentConfigurationFile, error) { 133 | 134 | var deployment DeploymentConfigurationFile 135 | 136 | jsonBytes, err := yaml.YAMLToJSON(b) 137 | if err == nil { 138 | // If there were no errors then the YAML => JSON was successful, no attempt to unmarshall 139 | err = json.Unmarshal(jsonBytes, &deployment) 140 | if err != nil { 141 | return nil, fmt.Errorf("Unable to parse configuration as either yaml or json\n %s", err.Error()) 142 | } 143 | 144 | } else { 145 | // Couldn't parse the yaml to JSON 146 | // Attempt to parse it as JSON 147 | err = json.Unmarshal(b, &deployment) 148 | if err != nil { 149 | return nil, fmt.Errorf("Unable to parse configuration as either yaml or json\n %s", err.Error()) 150 | } 151 | } 152 | return &deployment, nil 153 | } 154 | -------------------------------------------------------------------------------- /pkg/services/serverHTTP.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "path/filepath" 7 | 8 | "github.com/plunder-app/plunder/pkg/utils" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // These strings container the generated iPXE details that are passed to the bootloader when the correct url is requested 13 | var autoBoot, preseed, kickstart, defaultBoot, vsphere, reboot string 14 | 15 | // controller Pointer for the config API endpoint handler 16 | var controller *BootController 17 | 18 | var serveMux *http.ServeMux 19 | 20 | // TODO - this should be removed 21 | func (c *BootController) generateBootTypeHanders() { 22 | 23 | // Find the default configuration 24 | defaultConfig := findBootConfigForType("default") 25 | if defaultConfig != nil { 26 | defaultBoot = utils.IPXEPreeseed(*c.HTTPAddress, defaultConfig.Kernel, defaultConfig.Initrd, defaultConfig.Cmdline) 27 | } //else { 28 | // log.Warnf("Found [%d] configurations and no \"default\" configuration", len(c.BootConfigs)) 29 | //} 30 | 31 | // If a preeseed configuration has been configured then add it, and create a HTTP endpoint 32 | preeseedConfig := findBootConfigForType("preseed") 33 | if preeseedConfig != nil { 34 | preseed = utils.IPXEPreeseed(*c.HTTPAddress, preeseedConfig.Kernel, preeseedConfig.Initrd, preeseedConfig.Cmdline) 35 | 36 | } 37 | 38 | // If a kickstart configuration has been configured then add it, and create a HTTP endpoint 39 | kickstartConfig := findBootConfigForType("kickstart") 40 | if kickstartConfig != nil { 41 | kickstart = utils.IPXEPreeseed(*c.HTTPAddress, kickstartConfig.Kernel, kickstartConfig.Initrd, kickstartConfig.Cmdline) 42 | } 43 | 44 | // If a vsphereConfig configuration has been configured then add it, and create a HTTP endpoint 45 | vsphereConfig := findBootConfigForType("vsphere") 46 | if vsphereConfig != nil { 47 | vsphere = utils.IPXEVSphere(*c.HTTPAddress, vsphereConfig.Kernel, vsphereConfig.Cmdline) 48 | } 49 | } 50 | 51 | func (c *BootController) serveHTTP() error { 52 | 53 | // This function will pre-generate the boot handlers for the various boot types 54 | c.generateBootTypeHanders() 55 | 56 | autoBoot = utils.IPXEAutoBoot() 57 | reboot = utils.IPXEReboot() 58 | 59 | docroot, err := filepath.Abs("./") 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // Created only once 65 | 66 | // TOTO - alloew this to be customisable 67 | serveMux.Handle("/", http.FileServer(http.Dir(docroot))) 68 | 69 | // Boot handlers 70 | serveMux.HandleFunc("/health", HealthCheckHandler) 71 | serveMux.HandleFunc("/reboot.ipxe", rebootHandler) 72 | serveMux.HandleFunc("/autoBoot.ipxe", autoBootHandler) 73 | serveMux.HandleFunc("/default.ipxe", rootHandler) 74 | serveMux.HandleFunc("/kickstart.ipxe", kickstartHandler) 75 | serveMux.HandleFunc("/preseed.ipxe", preseedHandler) 76 | serveMux.HandleFunc("/vsphere.ipxe", vsphereHandler) 77 | 78 | // Set the pointer to the boot config 79 | controller = c 80 | 81 | return http.ListenAndServe(":80", serveMux) 82 | } 83 | 84 | func rootHandler(w http.ResponseWriter, r *http.Request) { 85 | log.Debugf("Requested URL [%s]", r.RequestURI) 86 | 87 | w.WriteHeader(http.StatusOK) 88 | w.Header().Set("Content-Type", "text/plain") 89 | // Return the preseed content 90 | log.Debugf("Requested URL [%s]", r.URL.Host) 91 | io.WriteString(w, httpPaths[r.URL.Path]) 92 | } 93 | 94 | func preseedHandler(w http.ResponseWriter, r *http.Request) { 95 | w.WriteHeader(http.StatusOK) 96 | w.Header().Set("Content-Type", "text/plain") 97 | // Return the preseed content 98 | io.WriteString(w, preseed) 99 | } 100 | 101 | func kickstartHandler(w http.ResponseWriter, r *http.Request) { 102 | w.WriteHeader(http.StatusOK) 103 | w.Header().Set("Content-Type", "text/plain") 104 | // Return the kickstart content 105 | io.WriteString(w, kickstart) 106 | } 107 | 108 | func vsphereHandler(w http.ResponseWriter, r *http.Request) { 109 | w.WriteHeader(http.StatusOK) 110 | w.Header().Set("Content-Type", "text/plain") 111 | // Return the vsphere content 112 | io.WriteString(w, vsphere) 113 | } 114 | 115 | func defaultBootHandler(w http.ResponseWriter, r *http.Request) { 116 | w.WriteHeader(http.StatusOK) 117 | w.Header().Set("Content-Type", "text/plain") 118 | // Return the default boot content 119 | io.WriteString(w, defaultBoot) 120 | } 121 | 122 | func rebootHandler(w http.ResponseWriter, r *http.Request) { 123 | w.WriteHeader(http.StatusOK) 124 | w.Header().Set("Content-Type", "text/plain") 125 | // Return the reboot content 126 | io.WriteString(w, reboot) 127 | } 128 | 129 | func autoBootHandler(w http.ResponseWriter, r *http.Request) { 130 | w.WriteHeader(http.StatusOK) 131 | w.Header().Set("Content-Type", "text/plain") 132 | // Return the reboot content 133 | io.WriteString(w, autoBoot) 134 | } 135 | 136 | // HealthCheckHandler - 137 | func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { 138 | // A very simple health check. 139 | w.WriteHeader(http.StatusOK) 140 | w.Header().Set("Content-Type", "application/json") 141 | 142 | // In the future we could report back on the status of our DB, or our cache 143 | // (e.g. Redis) by performing a simple PING, and include them in the response. 144 | io.WriteString(w, `{"alive": true}`) 145 | } 146 | -------------------------------------------------------------------------------- /pkg/services/serverHTTPISO.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/hooklift/iso9660" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // TODO - This currently is inefficient and results in an open/parse of an iso for every file operation. 18 | // github.com/qeedquan/iso9660 may need looking at later on. ( thebsdbox / [1/9/19] ) 19 | // Comments are left incase we/I revert 20 | 21 | // isoMapper at this point just maps the prefix to the path this may change 22 | var isoMapper map[string]string 23 | 24 | // iso9660PathSanitiser will take a "standard" file path and convert it into something that make sense within iso9660 TOC 25 | // The iso9660 constraints: 26 | // - A-Z (uppercase) 27 | // - '_' is the only other character 28 | // - Filename can only be 32 characters (inclucing the terminating semicolon ';') 29 | 30 | func iso9660PathSanitiser(unsanitisedPath string) string { 31 | // Get the filename from the string 32 | fullFilename := filepath.Base(unsanitisedPath) 33 | // Get the extension 34 | extension := filepath.Ext(fullFilename) 35 | 36 | // Remove the extension and leave just the filename 37 | filename := strings.TrimSuffix(fullFilename, extension) 38 | // Store the filename and shorten if over 31 characters 39 | 40 | trimmedFilename := filename 41 | pathLength := len(filename) + len(extension) 42 | if pathLength > 31 { 43 | // If the path is too long then we shrink the extension to a seperator and three characters 44 | if len(extension) > 3 { 45 | extension = extension[0:4] 46 | } 47 | // work out how much of the remaining filename can survive 48 | trimCount := 31 - len(extension) 49 | trimmedFilename = filename[0:trimCount] 50 | } 51 | 52 | rebuiltFileName := strings.ToUpper(fmt.Sprintf("%s%s", trimmedFilename, extension)) 53 | // Find if there is a full stop in the file name 54 | stopCount := strings.Count(rebuiltFileName, ".") 55 | var isoFilename string 56 | 57 | switch stopCount { 58 | case 0: 59 | // Append one as there is no filepath 60 | isoFilename = fmt.Sprintf("%s.", rebuiltFileName) 61 | case 1: 62 | // Not needed, just the semicolon 63 | isoFilename = fmt.Sprintf("%s", rebuiltFileName) 64 | default: 65 | // Ensure all other stops are changed to underscores 66 | isoFilename = fmt.Sprintf("%s", strings.Replace(rebuiltFileName, ".", "_", stopCount-1)) 67 | } 68 | 69 | //rebuild the path uppercase 70 | rebuildPath := strings.ToLower(fmt.Sprintf("%s/%s", filepath.Dir(unsanitisedPath), isoFilename)) 71 | 72 | // strD replacer 73 | replacer := strings.NewReplacer("+", "_", "-", "_", " ", "_", "~", "_") 74 | // Format the final output 75 | isoFormatted := replacer.Replace(rebuildPath) 76 | 77 | return isoFormatted 78 | } 79 | 80 | // This takes care of parsing a URL to identify if it should map to an ISO hosted file. 81 | 82 | // ISOReader - 83 | func isoReader(w http.ResponseWriter, r *http.Request) { 84 | 85 | // Sanitise the URL, there are a number of steps involved with turning the url into something we can use 86 | // Remove the beginning slash 87 | rawURL := strings.TrimLeft(r.URL.String(), "/") 88 | 89 | // Unescape the Http query 90 | isoURL, err := url.QueryUnescape(rawURL) 91 | if err != nil { 92 | w.WriteHeader(http.StatusInternalServerError) 93 | io.WriteString(w, fmt.Sprintf("%s", err.Error())) 94 | log.Error(err) 95 | 96 | return 97 | } 98 | 99 | // Split the URL to find the prefix (first part of the URL) 100 | urlElements := strings.Split(isoURL, "/") 101 | // Ensure the URL can be parsed 102 | if len(urlElements) > 1 { 103 | isoPrefix := urlElements[0] 104 | 105 | isoPath := iso9660PathSanitiser(strings.Replace(isoURL, isoPrefix, "", 1)) 106 | 107 | // We now have the ISO prefix to look up files, and the path to look up in the ISO 108 | // Check for ISO 109 | log.Debugf("Original URL: %s ISO Path: %s", isoURL, isoPath) 110 | if _, ok := isoMapper[isoPrefix]; ok { 111 | file, err := os.Open(isoMapper[isoPrefix]) 112 | if err != nil { 113 | w.WriteHeader(http.StatusInternalServerError) 114 | io.WriteString(w, fmt.Sprintf("%s", err.Error())) 115 | log.Error(err) 116 | return 117 | } 118 | defer file.Close() 119 | r, err := iso9660.NewReader(file) 120 | if err != nil { 121 | log.Error(err) 122 | return 123 | } 124 | for { 125 | 126 | f, err := r.Next() 127 | if err == io.EOF { 128 | w.WriteHeader(http.StatusNotFound) 129 | io.WriteString(w, fmt.Sprintf("Unable to read/find file %s", isoPath)) 130 | log.Error(fmt.Sprintf("Unable to read/find file %s", isoPath)) 131 | return 132 | } 133 | if err != nil { 134 | log.Error(err) 135 | return 136 | } 137 | 138 | if f.Name() == isoPath { 139 | freader := f.Sys().(io.Reader) 140 | buf := new(bytes.Buffer) 141 | buf.ReadFrom(freader) 142 | w.WriteHeader(http.StatusOK) 143 | w.Header().Set("Content-Type", "application/x-binary") 144 | io.WriteString(w, buf.String()) 145 | return 146 | } 147 | } 148 | // isoFile, err := isoMapper[isoPrefix].Open(isoPath) 149 | // if err != nil { 150 | // w.WriteHeader(http.StatusNotFound) 151 | // io.WriteString(w, fmt.Sprintf("%s", err.Error())) 152 | // return 153 | // } 154 | 155 | // fileStat, err := isoFile.Stat() 156 | // if err != nil { 157 | // w.WriteHeader(http.StatusNotFound) 158 | // io.WriteString(w, fmt.Sprintf("Unable to stat file on ISO %s", isoPath)) 159 | // return 160 | // } 161 | // fileBytes = make([]byte, fileStat.Size()) 162 | // _, err = isoFile.Read(fileBytes) 163 | // if err != nil { 164 | // w.WriteHeader(http.StatusNotFound) 165 | // io.WriteString(w, fmt.Sprintf("Unable to read file on ISO %s", isoPath)) 166 | // return 167 | // } 168 | } 169 | 170 | } else { 171 | w.WriteHeader(http.StatusNotFound) 172 | io.WriteString(w, fmt.Sprintf("Unable to find content ISO Prefix %s", isoURL)) 173 | return 174 | } 175 | w.WriteHeader(http.StatusNotFound) 176 | io.WriteString(w, fmt.Sprintf("Unable to find content ISO Prefix %s", isoURL)) 177 | return 178 | } 179 | 180 | // OpenISO will open an iso and add it to out ISO Map for reading at a later point 181 | func OpenISO(isoPath, isoPrefix string) error { 182 | // Check that the file exists 183 | 184 | _, err := os.Stat(isoPath) 185 | // We could use os.IsNotExist() but we may as well capture all errors 186 | if err != nil { 187 | return fmt.Errorf("Error reading file [%s]", isoPath) 188 | } 189 | 190 | return nil 191 | } 192 | -------------------------------------------------------------------------------- /pkg/services/serverImageHTTP.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | log "github.com/sirupsen/logrus" 12 | 13 | "github.com/dustin/go-humanize" 14 | ) 15 | 16 | // WriteCounter counts the number of bytes written to it. It implements to the io.Writer interface 17 | // and we can pass this into io.TeeReader() which will report progress on each write cycle. 18 | type WriteCounter struct { 19 | Total uint64 20 | } 21 | 22 | var data []byte 23 | 24 | func (wc *WriteCounter) Write(p []byte) (int, error) { 25 | n := len(p) 26 | wc.Total += uint64(n) 27 | return n, nil 28 | } 29 | 30 | func tickerProgress(byteCounter uint64) { 31 | // Clear the line by using a character return to go back to the start and remove 32 | // the remaining characters by filling it with spaces 33 | fmt.Printf("\r%s", strings.Repeat(" ", 35)) 34 | 35 | // Return again and print current status of download 36 | // We use the humanize package to print the bytes in a meaningful way (e.g. 10 MB) 37 | fmt.Printf("\rDownloading... %s complete", humanize.Bytes(byteCounter)) 38 | fmt.Println("") 39 | } 40 | 41 | func imageHandler(w http.ResponseWriter, r *http.Request) { 42 | 43 | log.Infof("Incoming image from [%s]", r.RemoteAddr) 44 | 45 | r.ParseMultipartForm(32 << 20) 46 | file, handler, err := r.FormFile("BootyImage") 47 | if handler != nil { 48 | log.Infof("Beginning to recieve image [%s]", handler.Filename) 49 | } 50 | 51 | if err != nil { 52 | log.Errorf("%v", err) 53 | return 54 | } 55 | defer file.Close() 56 | 57 | out, err := os.OpenFile(handler.Filename, os.O_CREATE|os.O_WRONLY, 0644) 58 | if err != nil { 59 | log.Fatalf("%v", err) 60 | } 61 | defer out.Close() 62 | 63 | // Create our progress reporter and pass it to be used alongside our writer 64 | ticker := time.NewTicker(500 * time.Millisecond) 65 | counter := &WriteCounter{} 66 | 67 | go func() { 68 | for ; true; <-ticker.C { 69 | tickerProgress(counter.Total) 70 | } 71 | }() 72 | if _, err = io.Copy(out, io.TeeReader(file, counter)); err != nil { 73 | log.Errorf("%v", err) 74 | } 75 | 76 | log.Infof("Written of image [%s] to disk", handler.Filename) 77 | ticker.Stop() 78 | 79 | w.WriteHeader(http.StatusOK) 80 | } 81 | 82 | func configHandler(w http.ResponseWriter, r *http.Request) { 83 | w.Write(data) 84 | } 85 | 86 | // Serve will start the webserver for BOOTy images 87 | func (c *BootController) serveImageHTTP() error { 88 | 89 | fs := http.FileServer(http.Dir("./images")) 90 | http.HandleFunc("/image", imageHandler) 91 | http.Handle("/images/", http.StripPrefix("/images/", fs)) 92 | log.Println("Plunder OS Image Services --> Starting HTTP :3000") 93 | err := http.ListenAndServe(":3000", nil) 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /pkg/services/serverTFTP.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/hex" 7 | "errors" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | 12 | log "github.com/sirupsen/logrus" 13 | tftp "github.com/thebsdbox/go-tftp/server" 14 | ) 15 | 16 | var iPXEData []byte 17 | 18 | // HandleWrite : writing is disabled in this service 19 | func HandleWrite(filename string) (w io.Writer, err error) { 20 | err = errors.New("Server is read only") 21 | return 22 | } 23 | 24 | // HandleRead : read a ROfs file and send over tftp 25 | func HandleRead(filename string) (r io.Reader, err error) { 26 | r = bytes.NewBuffer(iPXEData) 27 | return 28 | } 29 | 30 | // tftp server 31 | func (c *BootController) serveTFTP() error { 32 | 33 | log.Printf("Opening and caching undionly.kpxe") 34 | f, err := os.Open(*c.PXEFileName) 35 | if err != nil { 36 | log.Warnf("No local undionly.kpxe found, falling back to embedded version which may be out of date") 37 | iPXEData, err = hex.DecodeString(pxeFile) 38 | } else { 39 | // Use bufio.NewReader to get a Reader. 40 | // ... Then use ioutil.ReadAll to read the entire content. 41 | r := bufio.NewReader(f) 42 | 43 | iPXEData, err = ioutil.ReadAll(r) 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | s := tftp.NewServer("", HandleRead, HandleWrite) 49 | err = s.Serve(*c.TFTPAddress + ":69") 50 | if err != nil { 51 | return err 52 | } 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/services/services.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/plunder-app/plunder/pkg/utils" 8 | log "github.com/sirupsen/logrus" 9 | 10 | dhcp "github.com/krolaw/dhcp4" 11 | "github.com/krolaw/dhcp4/conn" 12 | ) 13 | 14 | var dhcpServer = make(chan bool) 15 | var dhcpError = make(chan error, 1) 16 | 17 | var runningDHCP, runningTFTP, runningHTTP bool 18 | 19 | // find BootConfig will look through a Boot controller for a booting configuration identified through a configuration name 20 | func findBootConfigForDeployment(deployment DeploymentConfig) *BootConfig { 21 | 22 | // // First check is to look inside the deployment configuration for a custom configuration 23 | // if deployment.ConfigBoot.Kernel != "" && deployment.ConfigBoot.Initrd != "" { 24 | // // A Custom Kernel and initrd are specified 25 | // log.Debugf("The server [%s] has a custom bootConfig defined", deployment.MAC) 26 | // return &deployment.ConfigBoot 27 | // } 28 | 29 | // Second check is to find a matching controller configuration to adopt 30 | for i := range Controller.BootConfigs { 31 | if Controller.BootConfigs[i].ConfigName == deployment.ConfigName { 32 | // Set the specific deployment configuration to the controller config 33 | return &Controller.BootConfigs[i] 34 | } 35 | } 36 | 37 | // Either there is no custom kernel/initrd/cmdline or a bootconfig doesn't exist as part of the server configuration 38 | return nil 39 | } 40 | 41 | // find BootConfig will look through a Boot controller for a booting configuration identified through a configuration name 42 | func findBootConfigForType(ConfigType string) *BootConfig { 43 | 44 | // Find a matching controller configuration to return 45 | for i := range Controller.BootConfigs { 46 | if Controller.BootConfigs[i].ConfigType == ConfigType { 47 | return &Controller.BootConfigs[i] 48 | } 49 | } 50 | 51 | // No configuration could be found 52 | return nil 53 | } 54 | 55 | // find BootConfig will look through a Boot controller for a booting configuration identified through a configuration name 56 | func (c *BootController) setBootConfig(configName, configType, kernel, initrd, cmdline string) { 57 | newConfig := &BootConfig{ 58 | ConfigName: configName, 59 | ConfigType: configType, 60 | Kernel: kernel, 61 | Initrd: initrd, 62 | Cmdline: cmdline, 63 | } 64 | c.BootConfigs = append(c.BootConfigs, *newConfig) 65 | } 66 | 67 | // StartServices - This will start all of the enabled services 68 | func (c *BootController) StartServices(deployment []byte) error { 69 | log.Infof("Starting Remote Boot Services, press CTRL + c to stop") 70 | 71 | if *c.EnableDHCP == true { 72 | c.handler = &DHCPSettings{} 73 | // DHCP Server address 74 | ip, err := utils.ConvertIP(c.DHCPConfig.DHCPAddress) 75 | if err != nil { 76 | log.Fatalf("DHCP Server -> %v", err) 77 | } 78 | c.handler.IP = ip 79 | 80 | // Start address of DHCP Range 81 | ip, err = utils.ConvertIP(c.DHCPConfig.DHCPStartAddress) 82 | if err != nil { 83 | log.Fatalf("DHCP Start Address -> %v", err) 84 | } 85 | c.handler.Start = ip 86 | 87 | // Additional DHCP options 88 | c.handler.LeaseDuration = 2 * time.Hour //TODO, make time modifiable 89 | c.handler.LeaseRange = c.DHCPConfig.DHCPLeasePool 90 | // Initialise the two maps 91 | c.handler.Leases = make(map[int]Lease, c.DHCPConfig.DHCPLeasePool) 92 | 93 | var options = dhcp.Options{} 94 | 95 | // Subnet 96 | ip, err = utils.ConvertIP(c.DHCPConfig.DHCPSubnet) 97 | if err != nil { 98 | log.Fatalf("DHCP Subnet -> %v", err) 99 | } 100 | options[dhcp.OptionSubnetMask] = ip 101 | 102 | // Gateway / Router 103 | ip, err = utils.ConvertIP(c.DHCPConfig.DHCPGateway) 104 | if err != nil { 105 | log.Fatalf("DHCP Gateway -> %v", err) 106 | } 107 | options[dhcp.OptionRouter] = ip 108 | 109 | // DNS 110 | ip, err = utils.ConvertIP(c.DHCPConfig.DHCPDNS) 111 | if err != nil { 112 | log.Fatalf("DHCP DNS ->%v", err) 113 | } 114 | options[dhcp.OptionDomainNameServer] = ip 115 | 116 | // Set bootname path (used by tftp) 117 | options[dhcp.OptionBootFileName] = []byte(*c.PXEFileName) 118 | 119 | c.handler.Options = options 120 | 121 | log.Debugf("\nServer IP:\t%s\nAdapter:\t%s\nStart Address:\t%s\nPool Size:\t%d\n", c.DHCPConfig.DHCPAddress, *c.AdapterName, c.DHCPConfig.DHCPStartAddress, c.DHCPConfig.DHCPLeasePool) 122 | log.Println("Plunder Services --> Starting DHCP") 123 | 124 | if runningDHCP == false { 125 | newConnection, err := conn.NewUDP4FilterListener(*c.AdapterName, ":67") 126 | if err != nil { 127 | log.Fatalf("%v", err) 128 | } 129 | go func() { 130 | //Close the connection when we're tidying up 131 | defer newConnection.Close() 132 | runningDHCP = true 133 | dhcpError <- dhcp.Serve(newConnection, c.handler) 134 | runningDHCP = false 135 | 136 | }() 137 | 138 | go func() { 139 | select { 140 | case <-dhcpError: 141 | log.Infof("%s\n", dhcpError) 142 | case <-dhcpServer: 143 | newConnection.Close() 144 | } 145 | }() 146 | } 147 | } else { 148 | log.Debugf("Stopping DHCP Server") 149 | if runningDHCP { 150 | dhcpServer <- true 151 | runningDHCP = false 152 | } 153 | 154 | } 155 | 156 | if *c.EnableTFTP == true { 157 | go func() { 158 | log.Println("Plunder Services --> Starting TFTP") 159 | log.Debugf("\nServer IP:\t%s\nPXEFile:\t%s\n", *c.TFTPAddress, *c.PXEFileName) 160 | 161 | err := c.serveTFTP() 162 | if err != nil { 163 | log.Fatalf("%v", err) 164 | } 165 | }() 166 | } 167 | 168 | if *c.EnableHTTP == true { 169 | if len(c.BootConfigs) == 0 { 170 | log.Warn("No Boot settings specified in configuration") 171 | } 172 | 173 | httpAddress = *c.HTTPAddress 174 | 175 | go func() { 176 | log.Println("Plunder Services --> Starting HTTP") 177 | err := c.serveHTTP() 178 | if err != nil { 179 | log.Fatalf("%v", err) 180 | } 181 | }() 182 | 183 | // Use of a Mux allows the redefinition of http paths 184 | serveMux = http.NewServeMux() 185 | 186 | // Parse the boot controller configuration 187 | // err := c.ParseBootController() 188 | for x := range c.BootConfigs { 189 | // // Parse the boot configuration (preload ISOs etc.) 190 | err := c.BootConfigs[x].Parse() 191 | if err != nil { 192 | // Don't quit on error as updated configuration can be uploaded through the API 193 | log.Errorf("%v", err) 194 | } 195 | } 196 | c.generateBootTypeHanders() 197 | 198 | // If a Deployment file is set then update the configuration 199 | if len(deployment) != 0 { 200 | err := UpdateDeploymentConfig(deployment) 201 | if err != nil { 202 | // Don't quit on error as updated configuration can be uploaded through the API 203 | log.Errorf("%v", err) 204 | } 205 | } 206 | } 207 | 208 | go c.serveImageHTTP() 209 | // // Image OS 210 | // go func() { 211 | 212 | // fs := http.FileServer(http.Dir("./images")) 213 | // http.Handle("/images/", http.StripPrefix("/images/", fs)) 214 | // log.Println("Plunder OS Image Services --> Starting HTTP :3000") 215 | // err := http.ListenAndServe(":3000", nil) 216 | // if err != nil { 217 | // log.Fatal(err) 218 | // } 219 | // }() 220 | 221 | // everything has been started correctly 222 | return nil 223 | } 224 | -------------------------------------------------------------------------------- /pkg/services/templateBOOTy.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | 7 | "github.com/plunder-app/BOOTy/pkg/plunderclient/types" 8 | "github.com/vishvananda/netlink" 9 | ) 10 | 11 | //BuildBOOTYconfig - Creates a new presseed configuration using the passed data 12 | func (config *HostConfig) BuildBOOTYconfig() string { 13 | a := types.BootyConfig{} 14 | 15 | // set the required action 16 | a.Action = config.BOOTYAction 17 | 18 | // Default to false if not in configuration 19 | if config.Compressed == nil { 20 | a.Compressed = false 21 | } else { 22 | a.Compressed = *config.Compressed 23 | } 24 | 25 | // Parse the strings 26 | subnet := net.ParseIP(config.Subnet) 27 | ip := net.ParseIP(config.IPAddress) 28 | 29 | // Change into a cidr 30 | cidr := net.IPNet{ 31 | IP: ip, 32 | Mask: subnet.DefaultMask(), 33 | } 34 | addr, _ := netlink.ParseAddr(cidr.String()) 35 | 36 | // Set configuration 37 | if addr != nil { 38 | a.Address = addr.String() 39 | a.Gateway = config.Gateway 40 | } 41 | 42 | // READ 43 | a.DestinationDevice = config.DestinationDevice 44 | a.SourceImage = config.SourceImage 45 | // WRITE 46 | a.DesintationAddress = config.DestinationAddress 47 | a.SourceDevice = config.SourceDevice 48 | 49 | // Default to false if not in configuration 50 | if config.GrowPartition == nil { 51 | a.GrowPartition = 0 52 | } else { 53 | a.GrowPartition = *config.GrowPartition 54 | } 55 | a.LVMRootName = config.LVMRootName 56 | 57 | // Default to false if not in configuration 58 | if config.ShellOnFail == nil { 59 | a.DropToShell = false 60 | } else { 61 | a.DropToShell = *config.ShellOnFail 62 | } 63 | 64 | a.DropToShell = *config.ShellOnFail 65 | a.NameServer = config.NameServer 66 | 67 | b, _ := json.Marshal(a) 68 | return string(b) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/services/templateESXi.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // bootcfgHead const, this is the basis for the configuration that will be modified per use-case for the boot.cfg 9 | // The main modifications with regards to this template is a replace-all on the module and kernel path 10 | const bootcfg67u2 = `bootstate=0 11 | title=Loading Plunder ESXi installer 12 | timeout=5 13 | prefix=http://%s/vsphere 14 | kernelopt=runweasel 15 | build= 16 | updated=0 17 | kernel=b.b00` 18 | 19 | // The Modules list all of the required modules needed to deploy vSphere 20 | const modules67us = `modules=/jumpstrt.gz --- /useropts.gz --- /features.gz --- /k.b00 --- /chardevs.b00 --- /user.b00 --- /procfs.b00 --- /uc_intel.b00 --- /uc_amd.b00 --- /uc_hygon.b00 --- /vmx.v00 --- /vim.v00 --- /sb.v00 --- /s.v00 --- /ata_liba.v00 --- /ata_pata.v00 --- /ata_pata.v01 --- /ata_pata.v02 --- /ata_pata.v03 --- /ata_pata.v04 --- /ata_pata.v05 --- /ata_pata.v06 --- /ata_pata.v07 --- /block_cc.v00 --- /bnxtnet.v00 --- /bnxtroce.v00 --- /brcmfcoe.v00 --- /char_ran.v00 --- /ehci_ehc.v00 --- /elxiscsi.v00 --- /elxnet.v00 --- /hid_hid.v00 --- /i40en.v00 --- /iavmd.v00 --- /igbn.v00 --- /ima_qla4.v00 --- /ipmi_ipm.v00 --- /ipmi_ipm.v01 --- /ipmi_ipm.v02 --- /iser.v00 --- /ixgben.v00 --- /lpfc.v00 --- /lpnic.v00 --- /lsi_mr3.v00 --- /lsi_msgp.v00 --- /lsi_msgp.v01 --- /lsi_msgp.v02 --- /misc_cni.v00 --- /misc_dri.v00 --- /mtip32xx.v00 --- /ne1000.v00 --- /nenic.v00 --- /net_bnx2.v00 --- /net_bnx2.v01 --- /net_cdc_.v00 --- /net_cnic.v00 --- /net_e100.v00 --- /net_e100.v01 --- /net_enic.v00 --- /net_fcoe.v00 --- /net_forc.v00 --- /net_igb.v00 --- /net_ixgb.v00 --- /net_libf.v00 --- /net_mlx4.v00 --- /net_mlx4.v01 --- /net_nx_n.v00 --- /net_tg3.v00 --- /net_usbn.v00 --- /net_vmxn.v00 --- /nfnic.v00 --- /nhpsa.v00 --- /nmlx4_co.v00 --- /nmlx4_en.v00 --- /nmlx4_rd.v00 --- /nmlx5_co.v00 --- /nmlx5_rd.v00 --- /ntg3.v00 --- /nvme.v00 --- /nvmxnet3.v00 --- /nvmxnet3.v01 --- /ohci_usb.v00 --- /pvscsi.v00 --- /qcnic.v00 --- /qedentv.v00 --- /qfle3.v00 --- /qfle3f.v00 --- /qfle3i.v00 --- /qflge.v00 --- /sata_ahc.v00 --- /sata_ata.v00 --- /sata_sat.v00 --- /sata_sat.v01 --- /sata_sat.v02 --- /sata_sat.v03 --- /sata_sat.v04 --- /scsi_aac.v00 --- /scsi_adp.v00 --- /scsi_aic.v00 --- /scsi_bnx.v00 --- /scsi_bnx.v01 --- /scsi_fni.v00 --- /scsi_hps.v00 --- /scsi_ips.v00 --- /scsi_isc.v00 --- /scsi_lib.v00 --- /scsi_meg.v00 --- /scsi_meg.v01 --- /scsi_meg.v02 --- /scsi_mpt.v00 --- /scsi_mpt.v01 --- /scsi_mpt.v02 --- /scsi_qla.v00 --- /shim_isc.v00 --- /shim_isc.v01 --- /shim_lib.v00 --- /shim_lib.v01 --- /shim_lib.v02 --- /shim_lib.v03 --- /shim_lib.v04 --- /shim_lib.v05 --- /shim_vmk.v00 --- /shim_vmk.v01 --- /shim_vmk.v02 --- /smartpqi.v00 --- /uhci_usb.v00 --- /usb_stor.v00 --- /usbcore_.v00 --- /vmkata.v00 --- /vmkfcoe.v00 --- /vmkplexe.v00 --- /vmkusb.v00 --- /vmw_ahci.v00 --- /xhci_xhc.v00 --- /elx_esx_.v00 --- /btldr.t00 --- /esx_dvfi.v00 --- /esx_ui.v00 --- /esxupdt.v00 --- /weaselin.t00 --- /lsu_hp_h.v00 --- /lsu_inte.v00 --- /lsu_lsi_.v00 --- /lsu_lsi_.v01 --- /lsu_lsi_.v02 --- /lsu_lsi_.v03 --- /lsu_smar.v00 --- /native_m.v00 --- /qlnative.v00 --- /rste.v00 --- /vmware_e.v00 --- /vsan.v00 --- /vsanheal.v00 --- /vsanmgmt.v00 --- /tools.t00 --- /xorg.v00 --- /imgdb.tgz --- /imgpayld.tgz` 21 | 22 | // kickstart67u2 const, this is the template for the actual installation of ESXi 23 | const kickstart67u2 = `accepteula 24 | install --firstdisk --overwritevmfs 25 | rootpw %s 26 | reboot 27 | # vmserialnum --esx=PUT IN YOUR LICENSE KEY 28 | 29 | #network configuration 30 | network --bootproto=static --addvmportgroup=1 --ip=%s --netmask=%s --gateway=%s --nameserver=%s --hostname=%s 31 | 32 | # run the following command only on the firstboot 33 | %%firstboot --interpreter=busybox 34 | 35 | 36 | # enable & start remote ESXi Shell (SSH) 37 | vim-cmd hostsvc/enable_ssh 38 | vim-cmd hostsvc/start_ssh 39 | 40 | # enable & start ESXi Shell (TSM) 41 | vim-cmd hostsvc/enable_esx_shell 42 | vim-cmd hostsvc/start_esx_shell 43 | 44 | # enable High Performance 45 | # http://www.virtuallyghetto.com/2012/08/configuring-esxi-power-management.html 46 | esxcli system settings advanced set --option=/Power/CpuPolicy --string-value="High Performance" 47 | 48 | # supress ESXi Shell shell warning - Thanks to Duncan (http://www.yellow-bricks.com/2011/07/21/esxi-5-suppressing-the-localremote-shell-warning/) 49 | esxcli system settings advanced set -o /UserVars/SuppressShellWarning -i 1 50 | 51 | #Disable ipv6 52 | esxcli network ip set --ipv6-enabled=0 53 | 54 | # NTP Configuration (thanks to http://www.virtuallyghetto.com) 55 | cat > /etc/ntp.conf << __NTP_CONFIG__ 56 | restrict default kod nomodify notrap noquerynopeer 57 | restrict 127.0.0.1 58 | server 129.6.15.28 59 | server 129.6.15.29 60 | server 129.6.15.30 61 | 62 | __NTP_CONFIG__ 63 | 64 | /sbin/chkconfig ntpd on 65 | ` 66 | 67 | //BuildESXiConfig - Creates a new presseed configuration using the passed data 68 | func (config *HostConfig) BuildESXiConfig() string { 69 | modules := strings.Replace(modules67us, "/", "", -1) 70 | vSphereConfig := fmt.Sprintf("%s\n%s", fmt.Sprintf(bootcfg67u2, config.RepositoryAddress), modules) 71 | return vSphereConfig 72 | } 73 | 74 | //BuildESXiKickStart - Creates a new presseed configuration using the passed data 75 | func (config *HostConfig) BuildESXiKickStart() string { 76 | 77 | // vSphere Kickststart 78 | vKickStart := fmt.Sprintf(kickstart67u2, config.Password, config.IPAddress, config.Subnet, config.Gateway, config.NameServer, config.ServerName) 79 | 80 | return vKickStart 81 | } 82 | -------------------------------------------------------------------------------- /pkg/services/templateKickstart.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import "fmt" 4 | 5 | // This initial template will be modifiable based upon the build requirements 6 | const kickstartFile = ` 7 | install 8 | cdrom 9 | lang en_US.UTF-8 10 | keyboard us 11 | unsupported_hardware 12 | network --bootproto=dhcp --hostname centos-7.pelmet.loc 13 | rootpw vagrant 14 | firewall --disabled 15 | selinux --permissive 16 | timezone Europe/Prague 17 | unsupported_hardware 18 | bootloader --location=mbr 19 | text 20 | skipx 21 | zerombr 22 | clearpart --all --initlabel 23 | 24 | #Disk partitioning information 25 | part /boot --fstype ext4 --size=2048 26 | part swap --asprimary --size=8192 27 | part / --fstype ext4 --size=1 --grow 28 | 29 | auth --enableshadow --passalgo=sha512 --kickstart 30 | firstboot --disabled 31 | eula --agreed 32 | services --enabled=NetworkManager,sshd 33 | reboot 34 | user --name=vagrant --plaintext --password vagrant --groups=vagrant,wheel 35 | 36 | repo --name=base --baseurl=http://mirror.centos.org/centos/7.3.1611/os/x86_64/ 37 | repo --name=epel-release --baseurl=http://anorien.csc.warwick.ac.uk/mirrors/epel/7/x86_64/ 38 | repo --name=elrepo-kernel --baseurl=http://elrepo.org/linux/kernel/el7/x86_64/ 39 | repo --name=elrepo-release --baseurl=http://elrepo.org/linux/elrepo/el7/x86_64/ 40 | repo --name=elrepo-extras --baseurl=http://elrepo.org/linux/extras/el7/x86_64/ 41 | 42 | %packages --ignoremissing --excludedocs 43 | @Base 44 | @Core 45 | @Development Tools 46 | kernel-ml 47 | kernel-ml-devel 48 | kernel-ml-tools 49 | kernel-ml-tools-libs 50 | kernel-ml-headers 51 | openssh-clients 52 | expect 53 | make 54 | perl 55 | patch 56 | dkms 57 | gcc 58 | bzip2 59 | sudo 60 | openssl-devel 61 | readline-devel 62 | zlib-devel 63 | net-tools 64 | vim 65 | wget 66 | curl 67 | rsync 68 | epel-release 69 | ansible 70 | libselinux-python 71 | -abrt-libs 72 | -abrt-tui 73 | -abrt-cli 74 | -abrt 75 | -abrt-addon-python 76 | -abrt-addon-ccpp 77 | -abrt-addon-kerneloops 78 | -kernel 79 | -kernel-devel 80 | -kernel-tools-libs 81 | -kernel-tools 82 | -kernel-headers 83 | -aic94xx-firmware 84 | -atmel-firmware 85 | -b43-openfwwf 86 | -bfa-firmware 87 | -ipw2100-firmware 88 | -ipw2200-firmware 89 | -ivtv-firmware 90 | -iwl100-firmware 91 | -iwl105-firmware 92 | -iwl135-firmware 93 | -iwl1000-firmware 94 | -iwl2000-firmware 95 | -iwl2030-firmware 96 | -iwl3160-firmware 97 | -iwl3945-firmware 98 | -iwl4965-firmware 99 | -iwl5000-firmware 100 | -iwl5150-firmware 101 | -iwl6000-firmware 102 | -iwl6000g2a-firmware 103 | -iwl6000g2b-firmware 104 | -iwl6050-firmware 105 | -iwl7260-firmware 106 | -libertas-usb8388-firmware 107 | -libertas-sd8686-firmware 108 | -libertas-sd8787-firmware 109 | -ql2100-firmware 110 | -ql2200-firmware 111 | -ql23xx-firmware 112 | -ql2400-firmware 113 | -ql2500-firmware 114 | -rt61pci-firmware 115 | -rt73usb-firmware 116 | -xorg-x11-drv-ati-firmware 117 | -zd1211-firmware 118 | -iprutils 119 | -fprintd-pam 120 | -intltool 121 | 122 | # unnecessary firmware 123 | -aic94xx-firmware 124 | -atmel-firmware 125 | -b43-openfwwf 126 | -bfa-firmware 127 | -ipw2100-firmware 128 | -ipw2200-firmware 129 | -ivtv-firmware 130 | -iwl100-firmware 131 | -iwl1000-firmware 132 | -iwl3945-firmware 133 | -iwl4965-firmware 134 | -iwl5000-firmware 135 | -iwl5150-firmware 136 | -iwl6000-firmware 137 | -iwl6000g2a-firmware 138 | -iwl6050-firmware 139 | -libertas-usb8388-firmware 140 | -ql2100-firmware 141 | -ql2200-firmware 142 | -ql23xx-firmware 143 | -ql2400-firmware 144 | -ql2500-firmware 145 | -rt61pci-firmware 146 | -rt73usb-firmware 147 | -xorg-x11-drv-ati-firmware 148 | -zd1211-firmware 149 | %end 150 | 151 | %post 152 | yum update -y 153 | yum install -y sudo 154 | echo "vagrant ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/vagrant 155 | sed -i "s/^.*requiretty/#Defaults requiretty/" /etc/sudoers 156 | /bin/echo 'UseDNS no' >> /etc/ssh/sshd_config 157 | yum clean all 158 | 159 | /bin/mkdir /home/vagrant/.ssh 160 | /bin/chmod 700 /home/vagrant/.ssh 161 | /bin/echo -e 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key' > /home/vagrant/.ssh/authorized_keys 162 | /bin/chown -R vagrant:vagrant /home/vagrant/.ssh 163 | /bin/chmod 0400 /home/vagrant/.ssh/* 164 | 165 | %end 166 | ` 167 | 168 | // BuildKickStartConfig - Creates a new presseed configuration using the passed data 169 | func (config *HostConfig) BuildKickStartConfig() string { 170 | return fmt.Sprintf("%s%s%s%s%s%s", preseed, preseedDisk, preseedNet, preseedPkg, preseedUsers, preseedCmd) 171 | } 172 | -------------------------------------------------------------------------------- /pkg/services/templateUtils.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "encoding/base64" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // ReadKeyFromFile - will attempt to read an sshkey from a file and populate the struct 11 | func (c *HostConfig) ReadKeyFromFile() (string, error) { 12 | var buffer []byte 13 | if _, err := os.Stat(c.SSHKeyPath); !os.IsNotExist(err) { 14 | buffer, err = ioutil.ReadFile(c.SSHKeyPath) 15 | if err != nil { 16 | // Unable to read the file 17 | return "", err 18 | } 19 | } else { 20 | // File doesn't exist 21 | return "", err 22 | } 23 | 24 | // TrimRight will remove the carriage return from the end of the buffer 25 | singleLine := strings.TrimRight(string(buffer), "\r\n") 26 | return singleLine, nil 27 | } 28 | 29 | // This will attempt to parse an SSH file in the host config and load it as a base64 encoded KEY 30 | func (c *HostConfig) parseSSH() error { 31 | // If a file is specified then lets read it and base64 the results (as long as a key doesn't already exist) 32 | if c.SSHKeyPath != "" && c.SSHKey == "" { 33 | data, err := c.ReadKeyFromFile() 34 | if err != nil { 35 | return err 36 | } 37 | c.SSHKey = base64.StdEncoding.EncodeToString([]byte(data)) 38 | } 39 | return nil 40 | } 41 | 42 | // PopulateFromGlobalConfiguration - This will read a deployment configuration and attempt to fill any missing fields from the global config 43 | func (c *HostConfig) PopulateFromGlobalConfiguration(globalConfig HostConfig) { 44 | // NETWORK CONFIGURATION 45 | 46 | // Inherit the global Gateway 47 | if c.Gateway == "" { 48 | c.Gateway = globalConfig.Gateway 49 | } 50 | 51 | // Inherit the global Subnet 52 | if c.Subnet == "" { 53 | c.Subnet = globalConfig.Subnet 54 | } 55 | 56 | // Inherit the global Name Server (DNS) 57 | if c.NameServer == "" { 58 | c.NameServer = globalConfig.NameServer 59 | } 60 | 61 | if c.Adapter == "" { 62 | c.Adapter = globalConfig.Adapter 63 | } 64 | 65 | // Disk Configuration 66 | 67 | if c.LVMEnable == nil && globalConfig.LVMEnable != nil { 68 | c.LVMEnable = globalConfig.LVMEnable 69 | } else { 70 | disabled := false 71 | c.LVMEnable = &disabled 72 | } 73 | 74 | if c.SwapDisabled == nil && globalConfig.SwapDisabled != nil { 75 | c.SwapDisabled = globalConfig.SwapDisabled 76 | } else { 77 | disabled := false 78 | c.SwapDisabled = &disabled 79 | } 80 | 81 | // REPOSITORY CONFIGURATION 82 | 83 | // Inherit the global Repository address 84 | if c.RepositoryAddress == "" { 85 | c.RepositoryAddress = globalConfig.RepositoryAddress 86 | } 87 | 88 | // Inherit the global Repository Mirror directory (typically /ubuntu) 89 | if c.MirrorDirectory == "" { 90 | c.MirrorDirectory = globalConfig.MirrorDirectory 91 | } 92 | 93 | // USER CONFIGURATION 94 | 95 | // Inherit the global Username 96 | if c.Username == "" { 97 | c.Username = globalConfig.Username 98 | } 99 | 100 | // Inherit the global Password 101 | if c.Password == "" { 102 | c.Password = globalConfig.Password 103 | } 104 | 105 | // Inherit the global SSH Key Path 106 | if c.SSHKeyPath == "" { 107 | c.SSHKeyPath = globalConfig.SSHKeyPath 108 | } 109 | 110 | // Package Configuration 111 | 112 | // Inherit the global package selection 113 | if c.Packages == "" { 114 | c.Packages = globalConfig.Packages 115 | } 116 | 117 | // BOOTy configuration (TODO CAN NOT LEAVE THIS HERE) 118 | 119 | if c.BOOTYAction == "" { 120 | c.BOOTYAction = globalConfig.BOOTYAction 121 | } 122 | 123 | if c.Compressed == nil && globalConfig.Compressed != nil { 124 | c.Compressed = globalConfig.Compressed 125 | } 126 | 127 | if c.GrowPartition == nil && globalConfig.GrowPartition != nil { 128 | c.GrowPartition = globalConfig.GrowPartition 129 | } 130 | 131 | if c.LVMRootName == "" { 132 | c.LVMRootName = globalConfig.LVMRootName 133 | } 134 | 135 | // WRITE to server 136 | if c.DestinationDevice == "" { 137 | c.DestinationDevice = globalConfig.DestinationDevice 138 | } 139 | 140 | if c.SourceImage == "" { 141 | c.SourceImage = globalConfig.SourceImage 142 | } 143 | 144 | // READ from server 145 | if c.DestinationAddress == "" { 146 | c.DestinationAddress = globalConfig.DestinationAddress 147 | } 148 | 149 | if c.SourceDevice == "" { 150 | c.SourceDevice = globalConfig.SourceDevice 151 | } 152 | 153 | // TODO 154 | if c.ShellOnFail == nil && globalConfig.ShellOnFail != nil { 155 | c.ShellOnFail = globalConfig.ShellOnFail 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /pkg/services/types.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | // TYPE DEFINITIONS Below 4 | 5 | // BootController contains the settings that define how the remote boot will 6 | type BootController struct { 7 | AdapterName *string `json:"adapter"` // A physical adapter to bind to e.g. en0, eth0 8 | 9 | // Servers 10 | EnableDHCP *bool `json:"enableDHCP"` // Enable Server 11 | //DHCP Configuration 12 | DHCPConfig dhcpConfig `json:"dhcpConfig,omitempty"` 13 | 14 | // TFTP / HTTP configuration 15 | EnableTFTP *bool `json:"enableTFTP"` // Enable Server 16 | TFTPAddress *string `json:"addressTFTP"` // Should ideally be the IP of the adapter 17 | EnableHTTP *bool `json:"enableHTTP"` // Enable Server 18 | HTTPAddress *string `json:"addressHTTP"` // Should ideally be the IP of the adapter 19 | 20 | // TFTP Configuration 21 | PXEFileName *string `json:"pxePath"` // undionly.kpxe 22 | 23 | // Boot Configuration 24 | BootConfigs []BootConfig `json:"bootConfigs"` // Array of kernel configurations 25 | 26 | handler *DHCPSettings 27 | } 28 | 29 | type dhcpConfig struct { 30 | DHCPAddress string `json:"addressDHCP"` // Should ideally be the IP of the adapter 31 | DHCPStartAddress string `json:"startDHCP"` // The first available DHCP address 32 | DHCPLeasePool int `json:"leasePoolDHCP"` // Size of the IP Address pool 33 | DHCPSubnet string `json:"subnetDHCP"` // Subnet for leases 34 | DHCPGateway string `json:"gatewayDHCP"` // Gateway to advertise 35 | DHCPDNS string `json:"nameserverDHCP"` // DNS server to advertise 36 | } 37 | 38 | // BootConfig defines a named configuration for booting 39 | type BootConfig struct { 40 | ConfigName string `json:"configName"` 41 | ConfigType string `json:"configType"` 42 | 43 | // iPXE file settings - exported 44 | Kernel string `json:"kernelPath"` 45 | Initrd string `json:"initrdPath"` 46 | Cmdline string `json:"cmdline"` 47 | 48 | // ISO Reader settings 49 | ISOPath string `json:"isoPath,omitempty"` 50 | ISOPrefix string `json:"isoPrefix,omitempty"` 51 | } 52 | 53 | // DeploymentConfigurationFile - The bootstraps.Configs is used by other packages to manage use case for Mac addresses 54 | type DeploymentConfigurationFile struct { 55 | GlobalServerConfig HostConfig `json:"globalConfig"` 56 | Configs []DeploymentConfig `json:"deployments"` 57 | } 58 | 59 | // DeploymentConfig - is used to parse the files containing all server configurations 60 | type DeploymentConfig struct { 61 | MAC string `json:"mac"` 62 | ConfigName string `json:"bootConfigName,omitempty"` // To be discovered in the controller BootConfig array 63 | ConfigBoot BootConfig `json:"bootConfig,omitempty"` // Array of kernel configurations 64 | ConfigHost HostConfig `json:"config"` 65 | } 66 | 67 | // HostConfig - Defines how a server will be configured by plunder 68 | type HostConfig struct { 69 | 70 | // Not required for the global configuration 71 | Adapter string `json:"adapter,omitempty"` // Adapter to be configured with networking address 72 | IPAddress string `json:"address,omitempty"` // Allocated IP address for a host (ignored for global) 73 | ServerName string `json:"hostname,omitempty"` // Hostname to be applied to a server 74 | 75 | // Typically shared details 76 | Gateway string `json:"gateway,omitempty"` // Default Gateway 77 | Subnet string `json:"subnet,omitempty"` // Subnet to be used for the host 78 | NameServer string `json:"nameserver,omitempty"` // Set the default nameserver for DNS 79 | NTPServer string `json:"ntpserver,omitempty"` // Time Server to be used 80 | 81 | LVMEnable *bool `json:"lvmEnabled,omitempty"` // Use LVM for the configuration 82 | SwapDisabled *bool `json:"swapDisabled,omitempty"` // Dont create swap partitions 83 | 84 | Username string `json:"username,omitempty"` 85 | Password string `json:"password,omitempty"` 86 | 87 | // RepositoryAddress is required for pre-seed / kickstart 88 | RepositoryAddress string `json:"repoaddress,omitempty"` 89 | // MirrorDirectory is an Ubuntu specific config 90 | MirrorDirectory string `json:"mirrordir,omitempty"` 91 | 92 | // SSHKeyPath will typically be referenced from a file ~/.ssh/id_rsa.pub 93 | SSHKeyPath string `json:"sshkeypath,omitempty"` 94 | // SSHKey is a full SSH Key in base 64 95 | SSHKey string `json:"sshkey,omitempty"` 96 | 97 | // Packages to be installed 98 | Packages string `json:"packages,omitempty"` 99 | 100 | // OS Image provisioning 101 | BOOTYAction string `json:"bootyAction,omitempty"` 102 | Compressed *bool `json:"compressed,omitempty"` 103 | 104 | // Write image to disk from remote address 105 | SourceImage string `json:"sourceImage,omitempty"` 106 | DestinationDevice string `json:"destinationDevice,omitempty"` 107 | 108 | // Read image from disk from remote address 109 | SourceDevice string `json:"sourceDevice,omitempty"` 110 | DestinationAddress string `json:"destinationAddress,omitempty"` 111 | 112 | // Post tasks - Once the image has been deployed 113 | 114 | // Volume modifications (LVM2) 115 | GrowPartition *int `json:"growPartition,omitempty"` 116 | LVMRootName string `json:"lvmRootName,omitempty"` 117 | 118 | // Troubleshooting 119 | ShellOnFail *bool `json:"shellOnFail,omitempty"` 120 | } 121 | -------------------------------------------------------------------------------- /pkg/ssh/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plunder-app/plunder/pkg/ssh 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /pkg/ssh/sshClient.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | log "github.com/sirupsen/logrus" 8 | "golang.org/x/crypto/ssh" 9 | ) 10 | 11 | // StartConnection - 12 | func (c *HostSSHConfig) StartConnection() (*ssh.Client, error) { 13 | var err error 14 | 15 | host := c.Host 16 | if !strings.ContainsAny(c.Host, ":") { 17 | host = host + ":22" 18 | } 19 | 20 | log.Debugf("Beginning connection to [%s] with user [%s] and timeout [%d]", c.Host, c.User, c.ClientConfig.Timeout) 21 | c.Connection, err = ssh.Dial("tcp", host, c.ClientConfig) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return c.Connection, nil 26 | } 27 | 28 | // StopConnection - 29 | func (c *HostSSHConfig) StopConnection() error { 30 | if c.Connection != nil { 31 | return c.Connection.Close() 32 | } 33 | return fmt.Errorf("Connection not established") 34 | } 35 | 36 | // StartSession - 37 | func (c *HostSSHConfig) StartSession() (*ssh.Session, error) { 38 | var err error 39 | c.Connection, err = c.StartConnection() 40 | if err != nil { 41 | return nil, err 42 | } 43 | c.Session, err = c.Connection.NewSession() 44 | if err != nil { 45 | return nil, err 46 | } 47 | return c.Session, err 48 | } 49 | 50 | // StopSession - 51 | func (c *HostSSHConfig) StopSession() { 52 | if c.Session != nil { 53 | c.Session.Close() 54 | } 55 | } 56 | 57 | // To string 58 | func (c HostSSHConfig) String() string { 59 | return c.User + "@" + c.Host 60 | } 61 | 62 | //FindHosts - This will take an array of hosts and find the matching HostSSH Configuration 63 | func FindHosts(parlayHosts []string) ([]HostSSHConfig, error) { 64 | var hostArray []HostSSHConfig 65 | for x := range parlayHosts { 66 | found := false 67 | for y := range Hosts { 68 | 69 | //TODO : Probably needs strings.ToLower() (needs testing) 70 | if parlayHosts[x] == Hosts[y].Host { 71 | hostArray = append(hostArray, Hosts[y]) 72 | found = true 73 | continue 74 | } 75 | } 76 | if found == false { 77 | return nil, fmt.Errorf("Host [%s] has no SSH credentials", parlayHosts[x]) 78 | } 79 | } 80 | return hostArray, nil 81 | } 82 | -------------------------------------------------------------------------------- /pkg/ssh/sshCommand.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "time" 11 | 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // SingleExecute - This will execute a command on a single host 16 | func SingleExecute(cmd, pipefile, pipecmd string, host HostSSHConfig, to int) CommandResult { 17 | var configs []HostSSHConfig 18 | configs = append(configs, host) 19 | result := ParalellExecute(cmd, pipefile, pipecmd, configs, to) 20 | return result[0] 21 | } 22 | 23 | //ParalellExecute - This will execute the same command in paralell across multiple hosts 24 | func ParalellExecute(cmd, pipefile, pipecmd string, hosts []HostSSHConfig, to int) []CommandResult { 25 | var cmdResults []CommandResult 26 | // Run parallel ssh session (max 10) 27 | results := make(chan CommandResult, 10) 28 | var d time.Duration 29 | 30 | // Calculate the timeout 31 | if to == 0 { 32 | // If no timeout then default to one year (TODO) 33 | d = time.Duration(8760) * time.Hour 34 | } else { 35 | d = time.Duration(to) * time.Second 36 | } 37 | 38 | // Set the timeout 39 | timeout := time.After(d) 40 | // Execute command on hosts 41 | for _, host := range hosts { 42 | go func(host HostSSHConfig) { 43 | res := new(CommandResult) 44 | res.Host = host.Host 45 | 46 | if pipefile != "" { 47 | if text, err := host.ExecuteCmdWithStdinFile(cmd, pipefile); err != nil { 48 | // Report any returned values 49 | res.Error = err 50 | res.Result = text 51 | } else { 52 | res.Result = text 53 | } 54 | } else if pipecmd != "" { 55 | if text, err := host.ExecuteCmdWithStdinCmd(cmd, pipecmd); err != nil { 56 | // Report any returned values 57 | res.Error = err 58 | res.Result = text 59 | } else { 60 | res.Result = text 61 | } 62 | } else { 63 | if text, err := host.ExecuteCmd(cmd); err != nil { 64 | // Report any returned values 65 | res.Error = err 66 | res.Result = text 67 | } else { 68 | res.Result = text 69 | } 70 | } 71 | results <- *res 72 | }(host) 73 | } 74 | 75 | for i := 0; i < len(hosts); i++ { 76 | select { 77 | case res := <-results: 78 | // Append the results of a succesfull command 79 | cmdResults = append(cmdResults, res) 80 | case <-timeout: 81 | // In the event that a command times out then append the details 82 | failedCommand := CommandResult{ 83 | Host: hosts[i].Host, 84 | Error: fmt.Errorf("Command Timed out"), 85 | Result: "", 86 | } 87 | cmdResults = append(cmdResults, failedCommand) 88 | 89 | } 90 | } 91 | return cmdResults 92 | } 93 | 94 | // ExecuteCmd - 95 | func (c *HostSSHConfig) ExecuteCmd(cmd string) (string, error) { 96 | if c.Session == nil { 97 | if _, err := c.StartSession(); err != nil { 98 | return "", err 99 | } 100 | } 101 | 102 | b, err := c.Session.CombinedOutput(cmd) 103 | 104 | return string(b), err 105 | } 106 | 107 | // ExecuteCmdWithStdinFile - 108 | func (c *HostSSHConfig) ExecuteCmdWithStdinFile(cmd, filePath string) (string, error) { 109 | if c.Session == nil { 110 | if _, err := c.StartSession(); err != nil { 111 | return "", err 112 | } 113 | } 114 | 115 | // get a stdin pipe 116 | si, err := c.Session.StdinPipe() 117 | if err != nil { 118 | return "", err 119 | } 120 | 121 | // get a stdout pipe 122 | so, err := c.Session.StdoutPipe() 123 | if err != nil { 124 | return "", err 125 | } 126 | 127 | // open file as an io.reader 128 | // Also resolve the absolute path just incase 129 | absPath, _ := filepath.Abs(filePath) 130 | file, err := os.Open(absPath) 131 | if err != nil { 132 | return "", err 133 | } 134 | 135 | // Start a command on our remote session, this should be something that is expecting stdin 136 | if err := c.Session.Start(cmd); err != nil { 137 | return "", err 138 | } 139 | 140 | // do the actual work 141 | n, err := io.Copy(si, file) 142 | if err != nil { 143 | return "", err 144 | } 145 | // Close the stdin as we've finised transmitting the data 146 | si.Close() 147 | 148 | log.Debugf("Copied %d bytes over the stdin pipe", n) 149 | // wait for process to finishe 150 | if err := c.Session.Wait(); err != nil { 151 | return "", err 152 | } 153 | 154 | // Read all the data from the bu 155 | var b []byte 156 | if b, err = ioutil.ReadAll(so); err != nil { 157 | return "", err 158 | 159 | } 160 | return string(b), nil 161 | 162 | } 163 | 164 | // ExecuteCmdWithStdinCmd - 165 | func (c *HostSSHConfig) ExecuteCmdWithStdinCmd(cmd, localCmd string) (string, error) { 166 | if c.Session == nil { 167 | if _, err := c.StartSession(); err != nil { 168 | return "", err 169 | } 170 | } 171 | 172 | // get a stdin pipe 173 | si, err := c.Session.StdinPipe() 174 | if err != nil { 175 | return "", err 176 | } 177 | 178 | // get a stdout pipe 179 | so, err := c.Session.StdoutPipe() 180 | if err != nil { 181 | return "", err 182 | } 183 | 184 | // Start a command on our remote session, this should be something that is expecting stdin 185 | if err := c.Session.Start(cmd); err != nil { 186 | return "", err 187 | } 188 | 189 | // Start our local command that should be exposing something through stdout 190 | localExecCmd := exec.Command("bash", "-c", localCmd) 191 | localso, err := localExecCmd.StdoutPipe() 192 | if err != nil { 193 | return "", err 194 | } 195 | err = localExecCmd.Start() 196 | if err != nil { 197 | return "", err 198 | } 199 | 200 | // do the actual work 201 | n, err := io.Copy(si, localso) 202 | if err != nil { 203 | return "", err 204 | } 205 | 206 | // Close the stdin/stdout as we've finised transmitting the data 207 | si.Close() 208 | localso.Close() 209 | 210 | log.Debugf("Copied %d bytes over the stdin pipe", n) 211 | 212 | // Wait for local process to finish 213 | err = localExecCmd.Wait() 214 | if err != nil { 215 | return "", err 216 | } 217 | 218 | // wait for remote process to finish 219 | if err := c.Session.Wait(); err != nil { 220 | return "", err 221 | } 222 | 223 | // Read all the data from the bu 224 | var b []byte 225 | if b, err = ioutil.ReadAll(so); err != nil { 226 | return "", err 227 | } 228 | return string(b), nil 229 | 230 | } 231 | -------------------------------------------------------------------------------- /pkg/ssh/sshConfig.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "golang.org/x/crypto/ssh" 5 | ) 6 | 7 | // Hosts - The array of all hosts once loaded 8 | var Hosts []HostSSHConfig 9 | 10 | // HostSSHConfig - The struct of an SSH connection 11 | type HostSSHConfig struct { 12 | Host string 13 | User string 14 | Timeout int 15 | ClientConfig *ssh.ClientConfig 16 | Session *ssh.Session 17 | Connection *ssh.Client 18 | } 19 | 20 | // CommandResult - This is returned when running commands against servers 21 | type CommandResult struct { 22 | Host string // Host that the command was being ran against 23 | Error error // Errors that may have been returned 24 | Result string // The CLI results 25 | } 26 | 27 | // SetPassword - Turn a password string into an SSH auth method 28 | func SetPassword(password string) []ssh.AuthMethod { 29 | return []ssh.AuthMethod{ssh.Password(password)} 30 | } 31 | -------------------------------------------------------------------------------- /pkg/ssh/sshImport.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os/user" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/mitchellh/go-homedir" 13 | "github.com/plunder-app/plunder/pkg/services" 14 | "golang.org/x/crypto/ssh" 15 | 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | // cachedGlobalKey caches the content of the gloal SSH key to save on excessing file operations 20 | var cachedGlobalKey ssh.AuthMethod 21 | 22 | // cachedUsername caches the content of the gloal Username on the basis that a lot of key based ops will share the same user 23 | var cachedUsername string 24 | 25 | // The init function will look for the default key and the default user 26 | 27 | func init() { 28 | u, err := user.Current() 29 | if err != nil { 30 | log.Warnf("Failed to find current user, if this is overridden by a deployment configuration this error can be ignored") 31 | } 32 | 33 | // If the above call hasn't errored, then u shouldn't be nil 34 | if u != nil { 35 | cachedUsername = u.Username 36 | } 37 | 38 | cachedGlobalKey, err = findDefaultKey() 39 | if err != nil { 40 | log.Warnf("Failed to find default ssh key, if this is overridden by a deployment configuration this error can be ignored") 41 | } 42 | } 43 | 44 | // AddHost will append additional hosts to the host array that the ssh package will use 45 | func AddHost(address, keypath, username string) error { 46 | sshHost := HostSSHConfig{ 47 | Host: address, 48 | } 49 | 50 | // If a username exists use that, alternatively use the cached entry 51 | if username != "" { 52 | sshHost.User = username 53 | } else if cachedUsername != "" { 54 | sshHost.User = cachedUsername 55 | } else { 56 | return fmt.Errorf("No username data for SSH authentication has been entered or loaded") 57 | } 58 | 59 | // Find additional keys that may exist in the same location 60 | var keys []ssh.AuthMethod 61 | 62 | if keypath != "" { 63 | key, err := findPrivateKey(keypath) 64 | if err != nil { 65 | return err 66 | } 67 | keys = append(keys, key) 68 | } else { 69 | if cachedGlobalKey != nil { 70 | keys = append(keys, cachedGlobalKey) 71 | } else { 72 | return fmt.Errorf("Host [%s] has no key specified", address) 73 | } 74 | } 75 | 76 | // Default timeout for connecting to a host is five seconds (TODO) 77 | sshHost.ClientConfig = &ssh.ClientConfig{ 78 | User: sshHost.User, 79 | Auth: keys, 80 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 81 | Timeout: 2 * time.Second, 82 | } 83 | 84 | Hosts = append(Hosts, sshHost) 85 | 86 | return nil 87 | } 88 | 89 | // ImportHostsFromDeployment - This will parse a deployment (either file or HTTP post) 90 | func ImportHostsFromDeployment(deployment services.DeploymentConfigurationFile) error { 91 | 92 | if len(deployment.Configs) == 0 { 93 | return fmt.Errorf("No deployment configurations found") 94 | } 95 | 96 | // Find keys that are in the same places as the public Key 97 | if deployment.GlobalServerConfig.SSHKeyPath != "" { 98 | var err error 99 | // Find if the private key from the global configuration 100 | cachedGlobalKey, err = findDefaultKey() 101 | if err != nil { 102 | log.Debugf("Failed to find default key, using Public key to find private") 103 | cachedGlobalKey, err = findPrivateKey(deployment.GlobalServerConfig.SSHKeyPath) 104 | if err != nil { 105 | return err 106 | } 107 | } 108 | } else { 109 | log.Debugln("No global configuration has been loaded, will default to local users keys") 110 | } 111 | 112 | // Find a global username to use, in place of an empty config 113 | if deployment.GlobalServerConfig.Username != "" { 114 | cachedUsername = deployment.GlobalServerConfig.Username 115 | } else { 116 | log.Debugf("No global configuration has been loaded, default to user [%s]", cachedUsername) 117 | } 118 | // Parse the deployments 119 | for i := range deployment.Configs { 120 | var sshHost HostSSHConfig 121 | 122 | sshHost.Host = deployment.Configs[i].ConfigHost.IPAddress 123 | 124 | if deployment.Configs[i].ConfigHost.Username != "" { 125 | sshHost.User = deployment.Configs[i].ConfigHost.Username 126 | } else { 127 | sshHost.User = deployment.GlobalServerConfig.Username 128 | } 129 | 130 | // Find additional keys that may exist in the same location 131 | var keys []ssh.AuthMethod 132 | if deployment.Configs[i].ConfigHost.SSHKeyPath != "" { 133 | // Look up default key 134 | key, err := findDefaultKey() 135 | if err != nil { 136 | log.Debugf("Failed to find default key, using Public key to find private") 137 | key, err = findPrivateKey(deployment.Configs[i].ConfigHost.SSHKeyPath) 138 | if err != nil { 139 | return err 140 | } 141 | } 142 | 143 | keys = append(keys, key) 144 | } else { 145 | if cachedGlobalKey != nil { 146 | keys = append(keys, cachedGlobalKey) 147 | } else { 148 | return fmt.Errorf("Host [%s] has no key specified", deployment.Configs[i].ConfigHost.IPAddress) 149 | } 150 | } 151 | 152 | // Default timeout for connecting to a host is five seconds (TODO) 153 | sshHost.ClientConfig = &ssh.ClientConfig{ 154 | User: sshHost.User, 155 | Auth: keys, 156 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 157 | Timeout: 2 * time.Second, 158 | } 159 | 160 | Hosts = append(Hosts, sshHost) 161 | } 162 | 163 | return nil 164 | } 165 | 166 | // ImportHostsFromRawDeployment - This will parse a deployment (either file or HTTP post) 167 | func ImportHostsFromRawDeployment(config []byte) error { 168 | 169 | var deployment services.DeploymentConfigurationFile 170 | 171 | err := json.Unmarshal(config, &deployment) 172 | if err != nil { 173 | return err 174 | } 175 | return ImportHostsFromDeployment(deployment) 176 | 177 | } 178 | 179 | // findDefaultKey - This will look in the users $HOME/.ssh/ for a key to add 180 | func findDefaultKey() (ssh.AuthMethod, error) { 181 | home, err := homedir.Dir() 182 | if err != nil { 183 | return nil, err 184 | } 185 | return findPrivateKey(fmt.Sprintf("%s/.ssh/id_rsa", home)) 186 | } 187 | 188 | // readKeyFile - Reads a public key from a file 189 | func findPrivateKey(publicKey string) (ssh.AuthMethod, error) { 190 | // Typically turn id_rsa.pub -> id_rsa 191 | privateKey := strings.TrimSuffix(publicKey, filepath.Ext(publicKey)) 192 | 193 | b, err := ioutil.ReadFile(privateKey) 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | key, err := ssh.ParsePrivateKey(b) 199 | if err != nil { 200 | return nil, err 201 | } 202 | return ssh.PublicKeys(key), nil 203 | } 204 | 205 | // readKeyFile - Reads a public key from a file 206 | func readKeyFile(keyfile string) (ssh.AuthMethod, error) { 207 | b, err := ioutil.ReadFile(keyfile) 208 | if err != nil { 209 | return nil, err 210 | } 211 | 212 | key, err := ssh.ParsePrivateKey(b) 213 | if err != nil { 214 | return nil, err 215 | } 216 | return ssh.PublicKeys(key), nil 217 | } 218 | 219 | // ReadKeyFiles - will read an array of keys from disk 220 | func ReadKeyFiles(keyFiles []string) ([]ssh.AuthMethod, error) { 221 | methods := []ssh.AuthMethod{} 222 | 223 | for _, keyname := range keyFiles { 224 | pkey, err := readKeyFile(keyname) 225 | if err != nil { 226 | return nil, err 227 | } 228 | if pkey != nil { 229 | methods = append(methods, pkey) 230 | } 231 | } 232 | 233 | return methods, nil 234 | } 235 | -------------------------------------------------------------------------------- /pkg/ssh/sshTransfer.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "time" 8 | 9 | "github.com/pkg/sftp" 10 | ) 11 | 12 | // ParalellDownload - Allow downloading a file over SFTP from multiple hosts in parallel 13 | func ParalellDownload(hosts []HostSSHConfig, source, destination string, to int) []CommandResult { 14 | var cmdResults []CommandResult 15 | // Run parallel ssh session (max 10) 16 | results := make(chan CommandResult, 10) 17 | 18 | var d time.Duration 19 | 20 | // Calculate the timeout 21 | if to == 0 { 22 | // If no timeout then default to one year (TODO) 23 | d = time.Duration(8760) * time.Hour 24 | } else { 25 | d = time.Duration(to) * time.Second 26 | } 27 | 28 | // Set the timeout 29 | timeout := time.After(d) 30 | 31 | // Execute command on hosts 32 | for _, host := range hosts { 33 | go func(host HostSSHConfig) { 34 | res := new(CommandResult) 35 | res.Host = host.Host 36 | 37 | if err := host.DownloadFile(source, destination); err != nil { 38 | res.Error = err 39 | } else { 40 | res.Result = "Download completed" 41 | } 42 | results <- *res 43 | }(host) 44 | } 45 | 46 | for i := 0; i < len(hosts); i++ { 47 | select { 48 | case res := <-results: 49 | // Append the results of a succesfull command 50 | cmdResults = append(cmdResults, res) 51 | case <-timeout: 52 | // In the event that a command times out then append the details 53 | failedCommand := CommandResult{ 54 | Host: hosts[i].Host, 55 | Error: fmt.Errorf("Download Timed out"), 56 | Result: "", 57 | } 58 | cmdResults = append(cmdResults, failedCommand) 59 | 60 | } 61 | } 62 | return cmdResults 63 | } 64 | 65 | // DownloadFile - 66 | func (c HostSSHConfig) DownloadFile(source, destination string) error { 67 | var err error 68 | c.Connection, err = c.StartConnection() 69 | if err != nil { 70 | return err 71 | } 72 | 73 | // New SFTP client 74 | sftp, err := sftp.NewClient(c.Connection) 75 | if err != nil { 76 | return err 77 | } 78 | defer sftp.Close() 79 | 80 | // Open remote source 81 | sftpSource, err := sftp.Open(source) 82 | if err != nil { 83 | return err 84 | } 85 | defer sftpSource.Close() 86 | 87 | // Open local destination 88 | localDestination, err := os.Create(destination) 89 | if err != nil { 90 | return err 91 | } 92 | defer localDestination.Close() 93 | 94 | // 95 | _, err = sftpSource.WriteTo(localDestination) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | // An error here isn't cause for alarm, any new transaction should create a new connection 101 | _ = c.StopConnection() 102 | 103 | return nil 104 | } 105 | 106 | // ParalellUpload - Allow uploading a file over SFTP to multiple hosts in parallel 107 | func ParalellUpload(hosts []HostSSHConfig, source, destination string, to int) []CommandResult { 108 | var cmdResults []CommandResult 109 | // Run parallel ssh session (max 10) 110 | results := make(chan CommandResult, 10) 111 | 112 | var d time.Duration 113 | 114 | // Calculate the timeout 115 | if to == 0 { 116 | // If no timeout then default to one year (TODO) 117 | d = time.Duration(8760) * time.Hour 118 | } else { 119 | d = time.Duration(to) * time.Second 120 | } 121 | 122 | // Set the timeout 123 | timeout := time.After(d) 124 | 125 | // Execute command on hosts 126 | for _, host := range hosts { 127 | go func(host HostSSHConfig) { 128 | res := new(CommandResult) 129 | res.Host = host.Host 130 | 131 | if err := host.UploadFile(source, destination); err != nil { 132 | res.Error = err 133 | } else { 134 | res.Result = "Upload completed" 135 | } 136 | results <- *res 137 | }(host) 138 | } 139 | 140 | for i := 0; i < len(hosts); i++ { 141 | select { 142 | case res := <-results: 143 | // Append the results of a succesfull command 144 | cmdResults = append(cmdResults, res) 145 | case <-timeout: 146 | // In the event that a command times out then append the details 147 | failedCommand := CommandResult{ 148 | Host: hosts[i].Host, 149 | Error: fmt.Errorf("Upload Timed out"), 150 | Result: "", 151 | } 152 | cmdResults = append(cmdResults, failedCommand) 153 | 154 | } 155 | } 156 | return cmdResults 157 | } 158 | 159 | // UploadFile - 160 | func (c HostSSHConfig) UploadFile(source, destination string) error { 161 | var err error 162 | c.Connection, err = c.StartConnection() 163 | if err != nil { 164 | return err 165 | } 166 | // New SFTP client 167 | sftp, err := sftp.NewClient(c.Connection) 168 | if err != nil { 169 | return err 170 | } 171 | defer sftp.Close() 172 | 173 | // Open remote source 174 | sftpDestination, err := sftp.Create(destination) 175 | if err != nil { 176 | return err 177 | } 178 | defer sftpDestination.Close() 179 | 180 | // Open local destination 181 | localSource, err := os.Open(source) 182 | if err != nil { 183 | return err 184 | } 185 | defer localSource.Close() 186 | 187 | // copy source file to destination file 188 | _, err = io.Copy(sftpDestination, localSource) 189 | if err != nil { 190 | return err 191 | } 192 | 193 | // An error here isn't cause for alarm, any new transaction should create a new connection 194 | _ = c.StopConnection() 195 | 196 | return nil 197 | } 198 | -------------------------------------------------------------------------------- /pkg/utils/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plunder-app/plunder/pkg/utils 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /pkg/utils/ipxe.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // Static URL for retrieving the bootloader 13 | const iPXEURL = "https://boot.ipxe.org/undionly.kpxe" 14 | 15 | // This header is used by all configurations 16 | const iPXEHeader = `#!ipxe 17 | dhcp 18 | echo . 19 | echo . 20 | echo . 21 | echo . 22 | echo +-------------------- Plunder ------------------------------- 23 | echo | 24 | echo | address.: ${net0/ip} 25 | echo | mac.....: ${net0/mac} 26 | echo | gateway.: ${net0/gateway} 27 | echo +------------------------------------------------------------ 28 | echo . 29 | echo . 30 | echo . 31 | echo .` 32 | 33 | ////////////////////////////// 34 | // 35 | // Helper Functions 36 | // 37 | ////////////////////////////// 38 | 39 | // IPXEReboot - 40 | func IPXEReboot() string { 41 | script := ` 42 | echo MAC ADDRESS is set to reboot, plunder will reboot the server in 5 seconds 43 | sleep 5 44 | reboot 45 | ` 46 | return iPXEHeader + script 47 | } 48 | 49 | // IPXEAutoBoot - 50 | func IPXEAutoBoot() string { 51 | script := ` 52 | echo Unknown MAC address, PXE boot will keep retrying until configuration changes 53 | :retry_boot 54 | autoboot || goto retry_boot 55 | ` 56 | return iPXEHeader + script 57 | } 58 | 59 | // IPXEPreeseed - This will build an iPXE boot script for Debian/Ubuntu 60 | func IPXEPreeseed(webserverAddress, kernel, initrd, cmdline string) string { 61 | script := ` 62 | kernel http://%s/%s auto=true url=http://%s/${mac:hexhyp}.cfg priority=critical %s netcfg/choose_interface=${netX/mac} 63 | initrd http://%s/%s 64 | boot 65 | ` 66 | // Replace the addresses inline 67 | buildScript := fmt.Sprintf(script, webserverAddress, kernel, webserverAddress, cmdline, webserverAddress, initrd) 68 | 69 | return iPXEHeader + buildScript 70 | } 71 | 72 | // IPXEKickstart - This will build an iPXE boot script for RHEL/CentOS 73 | func IPXEKickstart(webserverAddress, kernel, initrd, cmdline string) string { 74 | script := ` 75 | kernel http://%s/%s auto=true url=http://%s/${mac:hexhyp}.cfg priority=critical %s 76 | initrd http://%s/%s 77 | boot 78 | ` 79 | // Replace the addresses inline 80 | buildScript := fmt.Sprintf(script, webserverAddress, kernel, webserverAddress, cmdline, webserverAddress, initrd) 81 | 82 | return iPXEHeader + buildScript 83 | } 84 | 85 | // IPXEVSphere - This will build an iPXE boot script for VMware vSphere/ESXi 86 | func IPXEVSphere(webserverAddress, kernel, cmdline string) string { 87 | script := ` 88 | kernel http://%s/%s -c http://%s/${mac:hexhyp}.cfg ks=http://%s/${mac:hexhyp}.ks %s 89 | boot 90 | ` 91 | // Replace the addresses inline 92 | buildScript := fmt.Sprintf(script, webserverAddress, kernel, webserverAddress, webserverAddress, cmdline) 93 | 94 | return iPXEHeader + buildScript 95 | } 96 | 97 | // IPXEBOOTy - This will build an iPXE boot script for the BOOTy boot loader 98 | func IPXEBOOTy(webserverAddress, kernel, initrd, cmdline string) string { 99 | script := ` 100 | kernel http://%s/%s BOOTYURL=http://%s %s 101 | initrd http://%s/%s 102 | boot 103 | ` 104 | // Replace the addresses inline 105 | buildScript := fmt.Sprintf(script, webserverAddress, kernel, webserverAddress, cmdline, webserverAddress, initrd) 106 | 107 | return iPXEHeader + buildScript 108 | } 109 | 110 | // IPXEAnyBoot - This will build an iPXE boot script for anything wanting to PXE boot 111 | func IPXEAnyBoot(webserverAddress string, kernel, initrd, cmdline string) string { 112 | script := ` 113 | kernel http://%s/%s auto=true url=http://%s/${mac:hexhyp}.cfg %s 114 | initrd http://%s/%s 115 | boot 116 | ` 117 | // Replace the addresses inline 118 | buildScript := fmt.Sprintf(script, webserverAddress, kernel, webserverAddress, cmdline, webserverAddress, initrd) 119 | 120 | return iPXEHeader + buildScript 121 | } 122 | 123 | // PullPXEBooter - This will attempt to download the iPXE bootloader 124 | func PullPXEBooter() error { 125 | log.Infoln("Beginning of iPXE download... ") 126 | 127 | // Create the file 128 | out, err := os.Create("undionly.kpxe") 129 | if err != nil { 130 | return err 131 | } 132 | defer out.Close() 133 | 134 | // Get the data 135 | resp, err := http.Get(iPXEURL) 136 | if err != nil { 137 | return err 138 | } 139 | defer resp.Body.Close() 140 | 141 | // Writer the body to file 142 | _, err = io.Copy(out, resp.Body) 143 | if err != nil { 144 | return err 145 | } 146 | log.Infoln("Completed") 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /pkg/utils/nic.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | // FindIPAddress - this will find the address associated with an adapter 9 | func FindIPAddress(addrName string) (string, string, error) { 10 | var address string 11 | list, err := net.Interfaces() 12 | if err != nil { 13 | return "", "", err 14 | } 15 | 16 | for _, iface := range list { 17 | addrs, err := iface.Addrs() 18 | if err != nil { 19 | return "", "", err 20 | } 21 | for _, a := range addrs { 22 | if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 23 | if ipnet.IP.To4() != nil { 24 | address = ipnet.IP.String() 25 | // If we're not searching for a specific adapter return the first one 26 | if addrName == "" { 27 | return iface.Name, address, nil 28 | } else 29 | // If this is the correct adapter return the details 30 | if iface.Name == addrName { 31 | return iface.Name, address, nil 32 | } 33 | } 34 | } 35 | } 36 | 37 | } 38 | return "", "", fmt.Errorf("Unknown interface [%s]", addrName) 39 | } 40 | 41 | // FindAllIPAddresses - Will return all IP addresses for a server 42 | func FindAllIPAddresses() ([]net.IP, error) { 43 | var IPS []net.IP 44 | ifaces, err := net.Interfaces() 45 | if err != nil { 46 | return nil, err 47 | } 48 | for _, i := range ifaces { 49 | addrs, err := i.Addrs() 50 | if err != nil { 51 | return nil, err 52 | } 53 | for _, addr := range addrs { 54 | var ip net.IP 55 | switch v := addr.(type) { 56 | case *net.IPNet: 57 | ip = v.IP 58 | case *net.IPAddr: 59 | ip = v.IP 60 | } 61 | if ip != nil { 62 | IPS = append(IPS, net.IP(ip)) 63 | } 64 | // process IP address 65 | } 66 | } 67 | return IPS, nil 68 | } 69 | 70 | //ConvertIP - 71 | func ConvertIP(ipAddress string) ([]byte, error) { 72 | // net.ParseIP has returned IPv6 sized allocations o_O 73 | fixIP := net.ParseIP(ipAddress) 74 | if fixIP == nil { 75 | return nil, fmt.Errorf("Couldn't parse the IP address: %s", ipAddress) 76 | } 77 | if len(fixIP) > 4 { 78 | return fixIP[len(fixIP)-4:], nil 79 | } 80 | return fixIP, nil 81 | } 82 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/hex" 5 | "io/ioutil" 6 | "os" 7 | "os/signal" 8 | "sync" 9 | ) 10 | 11 | //WaitForCtrlC - This function is the loop that will catch a Control-C keypress 12 | func WaitForCtrlC() { 13 | var endWaiter sync.WaitGroup 14 | endWaiter.Add(1) 15 | var signalChannel chan os.Signal 16 | signalChannel = make(chan os.Signal, 1) 17 | signal.Notify(signalChannel, os.Interrupt) 18 | go func() { 19 | <-signalChannel 20 | endWaiter.Done() 21 | }() 22 | endWaiter.Wait() 23 | } 24 | 25 | //FileToHex - this is a helper function to allow embedding files into .go files 26 | func FileToHex(filePath string) (sl string, err error) { 27 | 28 | bs, err := ioutil.ReadFile(filePath) 29 | if err != nil { 30 | return 31 | } 32 | sl = hex.EncodeToString(bs) 33 | return 34 | 35 | } 36 | -------------------------------------------------------------------------------- /plugin/docker/docker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/plunder-app/plunder/pkg/parlay/parlaytypes" 8 | ) 9 | 10 | const pluginInfo = `This plugin is used to managed docker automation` 11 | 12 | type image struct { 13 | 14 | // Image details 15 | ImageNames []string `json:"imageName"` 16 | ImageFiles []string `json:"imageFile"` 17 | 18 | DockerLocalSudo bool `json:"localSudo"` 19 | DockerRemoteSudo bool `json:"remoteSudo"` 20 | } 21 | 22 | type tag struct { 23 | 24 | // A list of sources and target tags 25 | SourceNames []string `json:"sourceNames,omitempty"` 26 | TargetNames []string `json:"targetNames,omitempty"` 27 | 28 | // These two fields are used to change out a tag (e.g. version number) or the repository itself 29 | TargetTag string `json:"imageTag,omitempty"` 30 | TargetRepo string `json:"imageRepo,omitempty"` 31 | } 32 | 33 | // Dummy main function 34 | func main() {} 35 | 36 | // ParlayActionList - This should return an array of actions 37 | func ParlayActionList() []string { 38 | return []string{ 39 | "docker/image", 40 | "docker/tag"} 41 | } 42 | 43 | // ParlayActionDetails - This should return an array of action descriptions 44 | func ParlayActionDetails() []string { 45 | return []string{ 46 | "This action automates the management of docker images", 47 | "This action manages the tagging of docker images"} 48 | } 49 | 50 | // ParlayPluginInfo - returns information about the plugin 51 | func ParlayPluginInfo() string { 52 | return pluginInfo 53 | } 54 | 55 | // ParlayUsage - Returns the json that matches the specific action 56 | // <- action is a string that defines which action the usage information should be 57 | // <- raw - raw JSON that will be manipulated into a correct struct that matches the action 58 | // -> err is any error that has been generated 59 | func ParlayUsage(action string) (raw json.RawMessage, err error) { 60 | 61 | // This example plugin only has the code for "exampleAction/test" however this switch statement 62 | // should handle all exposed actions from the plugin 63 | switch action { 64 | case "docker/image": 65 | a := image{ 66 | ImageFiles: []string{"./my_image.tar.gz", "./my__other_image.tar.gz"}, 67 | ImageNames: []string{"gcr.io/my_image:latest", "gcr.io/my_other_image:latest"}, 68 | } 69 | // In order to turn a struct into an map[string]interface we need to turn it into JSON 70 | 71 | return json.Marshal(a) 72 | case "docker/tag": 73 | a := tag{ 74 | SourceNames: []string{"gcr.io/my_image:latest"}, 75 | TargetNames: []string{"internal_repo/my_image:1.0"}, 76 | } 77 | // In order to turn a struct into an map[string]interface we need to turn it into JSON 78 | 79 | return json.Marshal(a) 80 | default: 81 | return raw, fmt.Errorf("Action [%s] could not be found", action) 82 | } 83 | } 84 | 85 | // ParlayExec - Parses the action and the data that the action will consume 86 | // <- action a string that details the action to be executed 87 | // <- raw - raw JSON that will be manipulated into a correct struct that matches the action 88 | // -> actions are an array of generated actions that the parser will then execute 89 | // -> err is any error that has been generated 90 | func ParlayExec(action, host string, raw json.RawMessage) (actions []parlaytypes.Action, err error) { 91 | 92 | // This example plugin only has the code for "exampleAction/test" however this switch statement 93 | // should handle all exposed actions from the plugin 94 | switch action { 95 | case "docker/image": 96 | var img image 97 | // Unmarshall the JSON into the struct 98 | err = json.Unmarshal(raw, &img) 99 | return img.generateImageActions(host), err 100 | case "docker/tag": 101 | var t tag 102 | // Unmarshall the JSON into the struct 103 | err = json.Unmarshal(raw, &t) 104 | return t.generateTagActions(host) 105 | default: 106 | return 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /plugin/docker/docker_actions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | 7 | "github.com/plunder-app/plunder/pkg/parlay/parlaytypes" 8 | ) 9 | 10 | func (i *image) generateImageActions(host string) []parlaytypes.Action { 11 | var generatedActions []parlaytypes.Action 12 | var a parlaytypes.Action 13 | var dockerRemoteString, dockerLocalString string 14 | 15 | // This should be set to true if sudo (NOPASSWD) is enabled and required on the local host 16 | if i.DockerLocalSudo == true { 17 | dockerLocalString = "sudo docker save" 18 | } else { 19 | dockerLocalString = "docker save" 20 | } 21 | 22 | // This should be set to true if sudo (NOPASSWD) is enabled and required on the remote host 23 | if i.DockerRemoteSudo == true { 24 | dockerRemoteString = "sudo docker" 25 | } else { 26 | dockerRemoteString = "docker" 27 | } 28 | 29 | if len(i.ImageFiles) != 0 { 30 | // If we've specified a file (tarball, or tar+gzip) we cat then pipe over SSH to a docker load 31 | 32 | for y := range i.ImageFiles { 33 | a = parlaytypes.Action{ 34 | ActionType: "command", 35 | Command: fmt.Sprintf("%s load ", dockerRemoteString), 36 | CommandPipeFile: i.ImageFiles[y], 37 | Name: fmt.Sprintf("Upload container image %s to remote docker host", path.Base(i.ImageFiles[y])), 38 | } 39 | generatedActions = append(generatedActions, a) 40 | } 41 | } else if len(i.ImageNames) != 0 { 42 | 43 | // If we've specified a an existing image from the local docker image store then we "save" it (pipe to stdin) 44 | // then we can cat then pipe over SSH to a docker load 45 | for y := range i.ImageNames { 46 | 47 | a = parlaytypes.Action{ 48 | ActionType: "command", 49 | Command: fmt.Sprintf("%s load", dockerRemoteString), 50 | CommandPipeCmd: fmt.Sprintf("%s %s", dockerLocalString, i.ImageNames[y]), 51 | Name: fmt.Sprintf("Upload container image %s to remote docker host", i.ImageNames[y]), 52 | } 53 | generatedActions = append(generatedActions, a) 54 | } 55 | } 56 | 57 | return generatedActions 58 | } 59 | 60 | func (t *tag) generateTagActions(host string) ([]parlaytypes.Action, error) { 61 | 62 | if len(t.SourceNames) != len(t.TargetNames) { 63 | return nil, fmt.Errorf("The number of images to retag doesn't match the number of tags") 64 | } 65 | var generatedActions []parlaytypes.Action 66 | 67 | // Iterate through all of the images and create retagging actions 68 | for y := range t.SourceNames { 69 | // Generate the retag action 70 | var a = parlaytypes.Action{ 71 | ActionType: "command", 72 | Command: fmt.Sprintf("sudo docker tag %s %s", t.SourceNames[y], t.TargetNames[y]), 73 | CommandSudo: "root", 74 | Name: fmt.Sprintf("Retag %s --> %s", t.SourceNames[y], t.TargetNames[y]), 75 | } 76 | 77 | generatedActions = append(generatedActions, a) 78 | } 79 | return generatedActions, nil 80 | } 81 | -------------------------------------------------------------------------------- /plugin/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/plunder-app/plunder/pkg/parlay/parlaytypes" 8 | ) 9 | 10 | const pluginInfo = `This example plugin is used to demonstrate the structure of a plugin` 11 | 12 | // pluginAction - defines the struct that is unique to the 13 | type pluginTestAction struct { 14 | Credentials string `json:"credentials"` 15 | Address string `json:"address"` 16 | } 17 | 18 | // Dummy main function 19 | func main() {} 20 | 21 | // ParlayActionList - This should return an array of actions 22 | func ParlayActionList() []string { 23 | return []string{ 24 | "exampleAction/test", 25 | "exampleAction/demo", 26 | "exampleAction/example"} 27 | } 28 | 29 | // ParlayActionDetails - This should return an array of action descriptions 30 | func ParlayActionDetails() []string { 31 | return []string{ 32 | "This action handles the testing part of the example plugin", 33 | "This action handles the demonstration of the example plugin", 34 | "This action handles an example of the example plugin!"} 35 | } 36 | 37 | // ParlayPluginInfo - returns information about the plugin 38 | func ParlayPluginInfo() string { 39 | return pluginInfo 40 | } 41 | 42 | //ParlayActions - 43 | func ParlayActions(action string, iface interface{}) []parlaytypes.Action { 44 | var actions []parlaytypes.Action 45 | a := parlaytypes.Action{ 46 | Command: "example/test", 47 | } 48 | actions = append(actions, a) 49 | return actions 50 | } 51 | 52 | // ParlayUsage - Returns the json that matches the specific action 53 | // <- action is a string that defines which action the usage information should be 54 | // <- raw - raw JSON that will be manipulated into a correct struct that matches the action 55 | // -> err is any error that has been generated 56 | func ParlayUsage(action string) (raw json.RawMessage, err error) { 57 | 58 | // This example plugin only has the code for "exampleAction/test" however this switch statement 59 | // should handle all exposed actions from the plugin 60 | switch action { 61 | case "exampleAction/test": 62 | a := pluginTestAction{ 63 | Credentials: "AAABBBCCCCDDEEEE", 64 | Address: "172.0.0.1", 65 | } 66 | // In order to turn a struct into an map[string]interface we need to turn it into JSON 67 | 68 | return json.Marshal(a) 69 | default: 70 | return raw, fmt.Errorf("Action [%s] could not be found", action) 71 | } 72 | } 73 | 74 | // ParlayExec - Parses the action and the data that the action will consume 75 | // <- action a string that details the action to be executed 76 | // <- raw - raw JSON that will be manipulated into a correct struct that matches the action 77 | // -> actions are an array of generated actions that the parser will then execute 78 | // -> err is any error that has been generated 79 | func ParlayExec(action, host string, raw json.RawMessage) (actions []parlaytypes.Action, err error) { 80 | 81 | var t pluginTestAction 82 | // Unmarshall the JSON into the struct 83 | json.Unmarshal(raw, &t) 84 | // We can now use the fields as part of the struct 85 | 86 | // This example plugin only has the code for "exampleAction/test" however this switch statement 87 | // should handle all exposed actions from the plugin 88 | switch action { 89 | case "exampleAction/test": 90 | a := parlaytypes.Action{ 91 | Name: "Echo the address", 92 | ActionType: "command", 93 | Command: fmt.Sprintf("echo %s", t.Address), 94 | } 95 | actions = append(actions, a) 96 | 97 | a.Name = "Echo the Credentials" 98 | a.Command = fmt.Sprintf("echo %s", t.Credentials) 99 | actions = append(actions, a) 100 | 101 | return 102 | default: 103 | return 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /plugin/kubeadm/kubeadm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/plunder-app/plunder/pkg/parlay/parlaytypes" 8 | ) 9 | 10 | const pluginInfo = `This plugin is used to managed kubeadm automation` 11 | 12 | // This defines the etcd kubeadm file (should use the kubernetes packages to define at a later point) 13 | const etcdKubeadm = `apiVersion: "kubeadm.k8s.io/%s" 14 | kind: ClusterConfiguration 15 | etcd: 16 | local: 17 | serverCertSANs: 18 | - "%s" 19 | peerCertSANs: 20 | - "%s" 21 | extraArgs: 22 | initial-cluster: %s=https://%s:2380,%s=https://%s:2380,%s=https://%s:2380 23 | initial-cluster-state: new 24 | name: %s 25 | listen-peer-urls: https://%s:2380 26 | listen-client-urls: https://%s:2379 27 | advertise-client-urls: https://%s:2379 28 | initial-advertise-peer-urls: https://%s:2380` 29 | 30 | // This defines the manager kubeadm file (should use the kubernetes packages to define at a later point) 31 | 32 | const managerKubeadm = `apiVersion: kubeadm.k8s.io/v1beta1 33 | kind: ClusterConfiguration 34 | kubernetesVersion: %s 35 | apiServer: 36 | certSANs: 37 | - "%s" 38 | controlPlaneEndpoint: "%s:%d" 39 | etcd: 40 | external: 41 | endpoints: 42 | - https://%s:2379 43 | - https://%s:2379 44 | - https://%s:2379 45 | caFile: /etc/kubernetes/pki/etcd/ca.crt 46 | certFile: /etc/kubernetes/pki/apiserver-etcd-client.crt 47 | keyFile: /etc/kubernetes/pki/apiserver-etcd-client.key` 48 | 49 | type etcdMembers struct { 50 | // Hostnames 51 | Hostname1 string `json:"hostname1,omitempty"` 52 | Hostname2 string `json:"hostname2,omitempty"` 53 | Hostname3 string `json:"hostname3,omitempty"` 54 | 55 | // Addresses 56 | Address1 string `json:"address1,omitempty"` 57 | Address2 string `json:"address2,omitempty"` 58 | Address3 string `json:"address3,omitempty"` 59 | 60 | // Intialise a Certificate Authority 61 | InitCA bool `json:"initCA,omitempty"` 62 | 63 | // Set kubernetes API version 64 | APIVersion string `json:"apiversion,omitempty"` 65 | } 66 | 67 | type managerMembers struct { 68 | // ETCD Nodes 69 | ETCDAddress1 string `json:"etcd01,omitempty"` 70 | ETCDAddress2 string `json:"etcd02,omitempty"` 71 | ETCDAddress3 string `json:"etcd03,omitempty"` 72 | 73 | // Version of Kubernetes 74 | Version string `json:"kubeVersion,omitempty"` 75 | 76 | // Load Balancer details (needed for initialising the first master) 77 | //loadBalancer 78 | 79 | // Stacked - means ETCD nodes are stacked on managers (false by default) 80 | Stacked bool `json:"stacked,omitempty"` 81 | } 82 | 83 | // Dummy main function 84 | func main() {} 85 | 86 | // ParlayActionList - This should return an array of actions 87 | func ParlayActionList() []string { 88 | return []string{ 89 | "kubeadm/etcd", 90 | "kubeadm/master"} 91 | } 92 | 93 | // ParlayActionDetails - This should return an array of action descriptions 94 | func ParlayActionDetails() []string { 95 | return []string{ 96 | "This action automates the provisioning of a the first etcd node and certificates for the remaining two nodes", 97 | "This action handles the configuration of the first master node"} 98 | } 99 | 100 | // ParlayPluginInfo - returns information about the plugin 101 | func ParlayPluginInfo() string { 102 | return pluginInfo 103 | } 104 | 105 | // ParlayUsage - Returns the json that matches the specific action 106 | // <- action is a string that defines which action the usage information should be 107 | // <- raw - raw JSON that will be manipulated into a correct struct that matches the action 108 | // -> err is any error that has been generated 109 | func ParlayUsage(action string) (raw json.RawMessage, err error) { 110 | 111 | // This example plugin only has the code for "exampleAction/test" however this switch statement 112 | // should handle all exposed actions from the plugin 113 | switch action { 114 | case "kubeadm/etcd": 115 | a := etcdMembers{ 116 | Hostname1: "etcd01.local", 117 | Hostname2: "etcd02.local", 118 | Hostname3: "etcd03.local", 119 | InitCA: true, 120 | APIVersion: "v1beta1", 121 | Address1: "10.0.101", 122 | Address2: "10.0.102", 123 | Address3: "10.0.103", 124 | } 125 | // In order to turn a struct into an map[string]interface we need to turn it into JSON 126 | 127 | return json.Marshal(a) 128 | default: 129 | return raw, fmt.Errorf("Action [%s] could not be found", action) 130 | } 131 | } 132 | 133 | // ParlayExec - Parses the action and the data that the action will consume 134 | // <- action a string that details the action to be executed 135 | // <- raw - raw JSON that will be manipulated into a correct struct that matches the action 136 | // -> actions are an array of generated actions that the parser will then execute 137 | // -> err is any error that has been generated 138 | func ParlayExec(action, host string, raw json.RawMessage) (actions []parlaytypes.Action, err error) { 139 | 140 | // This example plugin only has the code for "exampleAction/test" however this switch statement 141 | // should handle all exposed actions from the plugin 142 | switch action { 143 | case "kubeadm/etcd": 144 | var etcdStruct etcdMembers 145 | // Unmarshall the JSON into the struct 146 | err = json.Unmarshal(raw, &etcdStruct) 147 | return etcdStruct.generateActions(), err 148 | default: 149 | return 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /plugin/kubeadm/kubeadm_actions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/plunder-app/plunder/pkg/parlay/parlaytypes" 7 | ) 8 | 9 | func (e *etcdMembers) generateActions() []parlaytypes.Action { 10 | var generatedActions []parlaytypes.Action 11 | var a parlaytypes.Action 12 | if e.InitCA == true { 13 | // Ensure that a new Certificate Authority is generated 14 | // Create action 15 | a = parlaytypes.Action{ 16 | // Generate etcd server certificate 17 | ActionType: "command", 18 | Command: fmt.Sprintf("kubeadm init phase certs etcd-ca"), 19 | CommandSudo: "root", 20 | Name: "Initialise Certificate Authority", 21 | } 22 | generatedActions = append(generatedActions, a) 23 | } 24 | 25 | // Default to < 1.12 API version 26 | if e.APIVersion == "" { 27 | e.APIVersion = "v1beta1" 28 | } 29 | // Generate the configuration directories 30 | a.ActionType = "command" 31 | a.Command = fmt.Sprintf("mkdir -m 777 -p /tmp/%s/ /tmp/%s/ /tmp/%s/", e.Address1, e.Address2, e.Address3) 32 | a.Name = "Generate temporary directories" 33 | generatedActions = append(generatedActions, a) 34 | 35 | // Generate the kubeadm configuration files 36 | 37 | // Node 0 38 | a.Name = "build kubeadm config for node 0" 39 | a.Command = fmt.Sprintf("echo '%s' > /tmp/%s/kubeadmcfg.yaml", e.buildKubeadm(e.APIVersion, e.Hostname1, e.Address1), e.Address1) 40 | generatedActions = append(generatedActions, a) 41 | 42 | // Node 1 43 | a.Name = "build kubeadm config for node 1" 44 | a.Command = fmt.Sprintf("echo '%s' > /tmp/%s/kubeadmcfg.yaml", e.buildKubeadm(e.APIVersion, e.Hostname2, e.Address2), e.Address2) 45 | generatedActions = append(generatedActions, a) 46 | 47 | // Node 2 48 | a.Name = "build kubeadm config for node 2" 49 | a.Command = fmt.Sprintf("echo '%s' > /tmp/%s/kubeadmcfg.yaml", e.buildKubeadm(e.APIVersion, e.Hostname3, e.Address3), e.Address3) 50 | generatedActions = append(generatedActions, a) 51 | 52 | // Add certificate actions 53 | generatedActions = append(generatedActions, e.generateCertificateActions([]string{e.Address3, e.Address2, e.Address1})...) 54 | return generatedActions 55 | } 56 | 57 | func (e *etcdMembers) buildKubeadm(api, host, address string) string { 58 | var kubeadm string 59 | // Generates a kubeadm for setting up the etcd yaml 60 | kubeadm = fmt.Sprintf(etcdKubeadm, api, address, address, e.Hostname1, e.Address1, e.Hostname2, e.Address2, e.Hostname3, e.Address3, host, address, address, address, address) 61 | return kubeadm 62 | } 63 | 64 | // generateCertificateActions - Hosts need adding in backward to the array i.e. host 2 -> host 1 -> host 0 65 | func (e *etcdMembers) generateCertificateActions(hosts []string) []parlaytypes.Action { 66 | var generatedActions []parlaytypes.Action 67 | var a parlaytypes.Action 68 | 69 | a.Command = "mkdir -p /etc/kubernetes/pki" 70 | a.CommandSudo = "root" 71 | a.Name = "Ensure that PKI directory exists" 72 | a.ActionType = "command" 73 | generatedActions = append(generatedActions, a) 74 | 75 | for i, v := range hosts { 76 | // Tidy any existing client certificates 77 | a.ActionType = "command" 78 | a.Command = "find /etc/kubernetes/pki -not -name ca.crt -not -name ca.key -type f -delete" 79 | a.Name = "Remove any existing client certificates before attempting to generate any new ones" 80 | generatedActions = append(generatedActions, a) 81 | 82 | // Generate etcd server certificate 83 | a.ActionType = "command" 84 | a.Command = fmt.Sprintf("kubeadm init phase certs etcd-server --config=/tmp/%s/kubeadmcfg.yaml", v) 85 | a.Name = fmt.Sprintf("Generate etcd server certificate for [%s]", v) 86 | generatedActions = append(generatedActions, a) 87 | 88 | // Generate peer certificate 89 | a.Command = fmt.Sprintf("kubeadm init phase certs etcd-peer --config=/tmp/%s/kubeadmcfg.yaml", v) 90 | a.Name = fmt.Sprintf("Generate peer certificate for [%s]", v) 91 | generatedActions = append(generatedActions, a) 92 | 93 | // Generate health check certificate 94 | a.Command = fmt.Sprintf("kubeadm init phase certs etcd-healthcheck-client --config=/tmp/%s/kubeadmcfg.yaml", v) 95 | a.Name = fmt.Sprintf("Generate health check certificate for [%s]", v) 96 | generatedActions = append(generatedActions, a) 97 | 98 | // Generate api-server client certificate 99 | a.Command = fmt.Sprintf("kubeadm init phase certs apiserver-etcd-client --config=/tmp/%s/kubeadmcfg.yaml", v) 100 | a.Name = fmt.Sprintf("Generate api-server client certificate for [%s]", v) 101 | generatedActions = append(generatedActions, a) 102 | 103 | // These steps are only required for the first two hosts 104 | if i != (len(hosts) - 1) { 105 | // Archive the certificates and the kubeadm configuration in a host specific archive name 106 | a.Command = fmt.Sprintf("tar -cvzf /tmp/%s.tar.gz $(find /etc/kubernetes/pki -type f) /tmp/%s/kubeadmcfg.yaml", v, v) 107 | a.Name = fmt.Sprintf("Archive generated certificates [%s]", v) 108 | generatedActions = append(generatedActions, a) 109 | 110 | // Download the archive files to the local machine 111 | a.ActionType = "download" 112 | a.Source = fmt.Sprintf("/tmp/%s.tar.gz", hosts[i]) 113 | a.Destination = fmt.Sprintf("/tmp/%s.tar.gz", hosts[i]) 114 | a.Name = fmt.Sprintf("Retrieve the certificate bundle for [%s]", v) 115 | generatedActions = append(generatedActions, a) 116 | } else { 117 | // This is the final host, grab the certificates for use by a manager 118 | a.Command = fmt.Sprintf("tar -cvzf /tmp/managercert.tar.gz /etc/kubernetes/pki/etcd/ca.crt /etc/kubernetes/pki/apiserver-etcd-client.crt /etc/kubernetes/pki/apiserver-etcd-client.key") 119 | a.Name = fmt.Sprintf("Archive generated certificates [%s]", v) 120 | generatedActions = append(generatedActions, a) 121 | 122 | // Download the archive files to the local machine 123 | a.ActionType = "download" 124 | a.Source = "/tmp/managercert.tar.gz" 125 | a.Destination = "/tmp/managercert.tar.gz" 126 | a.Name = "Retrieving the Certificates for the manager nodes" 127 | generatedActions = append(generatedActions, a) 128 | } 129 | } 130 | return generatedActions 131 | } 132 | 133 | // At some point the functions for the various kubeadm arease will be split into seperate files to ease management 134 | func (m *managerMembers) generateActions() []parlaytypes.Action { 135 | var generatedActions []parlaytypes.Action 136 | var a parlaytypes.Action 137 | if m.Stacked == false { 138 | // Not implemented yet TODO 139 | return nil 140 | } 141 | 142 | // Upload the initial etcd certificates to the first manager node 143 | a = parlaytypes.Action{ 144 | // Upload etcd server certificate 145 | ActionType: "upload", 146 | Source: "/tmp/managercert.tar.gz", 147 | Destination: "/tmp/managercert.tar.gz", 148 | Name: "Upload etcd server certificate to first manager", 149 | } 150 | generatedActions = append(generatedActions, a) 151 | 152 | // Install the certificates for etcd 153 | a.Name = "Installing the etcd certificates" 154 | a.ActionType = "command" 155 | a.CommandSudo = "root" 156 | a.Command = fmt.Sprintf("tar -xvzf /tmp/managercert.tar.gz -C /") 157 | generatedActions = append(generatedActions, a) 158 | 159 | // Generate the kubeadm configuration file 160 | a.Name = "Generating the Kubeadm file for the first manager node" 161 | a.Command = fmt.Sprintf("echo '%s' > /tmp/kubeadmcfg.yaml", m.buildKubeadm()) 162 | generatedActions = append(generatedActions, a) 163 | 164 | // Initialise the first node 165 | a.Name = "Initialise the first control plane node" 166 | a.Command = "kubeadm init --config /tmp/kubeadmcfg.yaml" 167 | generatedActions = append(generatedActions, a) 168 | 169 | return generatedActions 170 | } 171 | 172 | func (m *managerMembers) buildKubeadm() string { 173 | var kubeadm string 174 | // Generates a kubeadm for setting up the etcd yaml 175 | kubeadm = fmt.Sprintf(managerKubeadm, m.Version, "LB HOSTNAME FIXME", "LB HOSTNAME FIXME", 1000000, m.ETCDAddress1, m.ETCDAddress2, m.ETCDAddress3) 176 | return kubeadm 177 | } 178 | -------------------------------------------------------------------------------- /testing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This script will step through a number of tests agains plunder to ensure that functionality is as expected" 4 | echo "Building plunder with [go build]" 5 | 6 | INSECURE="-k" 7 | PLUNDERURL="https://localhost:60443" 8 | 9 | go build 10 | 11 | retVal=$? 12 | if [ $retVal -ne 0 ]; then 13 | echo "Error at go build" 14 | exit 15 | fi 16 | 17 | echo "Check for no version information" 18 | v=$(./plunder version | grep Version | awk '{ print $2 }') 19 | rm plunder 20 | 21 | if [ -z "$v" ] 22 | then 23 | echo "Version is empty" 24 | else 25 | echo "Version is NOT empty" 26 | fi 27 | 28 | echo "Building plunder with [make build]" 29 | 30 | make build 31 | 32 | retVal=$? 33 | if [ $retVal -ne 0 ]; then 34 | echo "Error at make build" 35 | exit 36 | fi 37 | 38 | echo "Check for version information" 39 | v=$(./plunder version | grep Version | awk '{ print $2 }') 40 | if [ -z "$v" ] 41 | then 42 | echo "Version is empty" 43 | else 44 | echo "Version is NOT empty [$v]" 45 | fi 46 | 47 | echo "Plunder server configuration, temporary output will live in ./testing" 48 | mkdir testing 49 | ./plunder config server -p > ./testing/server_test_config.json 50 | ./plunder config deployment -p > ./testing/deployment_config.json 51 | ./plunder config server -o yaml > ./testing/server_test_config.yaml 52 | ./plunder config deployment -o yaml > ./testing/deployment_config.yaml 53 | echo "Generating API Server certificates in ~./plunderserver.yaml" 54 | ./plunder config apiserver server 55 | 56 | echo "Creating alternative configuration with services enabled" 57 | sed '/enableHTTP/s/false/true/' ./testing/server_test_config.json > ./testing/server_test_http_config.json 58 | 59 | 60 | echo "Examining detected configuration" 61 | echo "Checking for Adapter" 62 | v=$(grep adapter ./testing/server_test_config.json | awk ' {print $2 }' | tr -d '"' | tr -d ',') 63 | if [ -z "$v" ] 64 | then 65 | echo "Adapter is empty" 66 | else 67 | echo "Adapter is NOT empty [$v]" 68 | fi 69 | 70 | echo "Checking for Gateway Address" 71 | v=$(grep gatewayDHCP ./testing/server_test_config.json | awk ' {print $2 }' | tr -d '"' | tr -d ',') 72 | if [ -z "$v" ] 73 | then 74 | echo "Gateway is empty" 75 | else 76 | echo "Gateway is NOT empty [$v]" 77 | fi 78 | 79 | i=$(id -u) 80 | n=$(id -un) 81 | if [[ $i -gt 0 ]] 82 | then 83 | echo "Testing as current user [NAME = $n / ID = $i]" 84 | echo "Starting with disabled configuration" 85 | ./plunder server --config ./testing/server_test_config.json 86 | retVal=$? 87 | if [ $retVal -ne 0 ]; then 88 | echo "Plunder correctly didn't start" 89 | fi 90 | echo "Starting with enabled HTTP configuration (check OSX)" 91 | sudo ./plunder server --config ./testing/server_test_http_config.json & 92 | retVal=$? 93 | if [ $retVal -ne 0 ]; then 94 | echo "Plunder correctly didn't start" 95 | exit 1 96 | fi 97 | echo "Sleeping for 3 seconds to ensure plunder has started" 98 | sleep 3 99 | echo "Print Configuration info"; echo "--------------------------" 100 | curl $INSECURE $PLUNDERURL/config; echo "" 101 | echo "Print Deployments info"; echo "--------------------------" 102 | curl $INSECURE $PLUNDERURL/deployments; echo "" 103 | echo "POST JSON Deployment to Plunder API" 104 | curl $INSECURE -X POST -d "@./testing/deployment_config.json" $PLUNDERURL/deployments 105 | echo "Print (UPDATED) Deployment info"; echo "--------------------------" 106 | curl $INSECURE $PLUNDERURL/deployments; echo "" 107 | echo "POST YAML Deployment to Plunder API" 108 | curl $INSECURE -X POST --data-binary "@./testing/deployment_config.yaml" $PLUNDERURL/deployments -H "Content-type: text/x-yaml" 109 | echo "Print (UPDATED) Deployment info"; echo "--------------------------" 110 | curl $INSECURE $PLUNDERURL/deployments; echo "" 111 | sudo kill -9 $( ps -ef | grep -i plunder | grep -v -e 'sudo' -e 'grep' | awk '{ print $2 }') 112 | wait $! 2>/dev/null 113 | sleep 1 114 | else 115 | echo "Skipping permission tests as running as root" 116 | fi 117 | 118 | echo "The following tests rely on sudo, with NOPASSWD enabled" 119 | 120 | echo "Starting with disabled configuration" 121 | 122 | retVal=$? 123 | if [ $retVal -ne 0 ]; then 124 | echo "Error at make build" 125 | exit 126 | fi 127 | 128 | echo "To remote [./testing/] directory, and [./plunder] binary" 129 | echo "rm -rf ./testing/ ./plunder" --------------------------------------------------------------------------------