[
  {
    "path": ".gitignore",
    "content": "\n.plunderserver.yaml\nplunder\nplunderclient.yaml\n"
  },
  {
    "path": "Dockerfile",
    "content": "# syntax=docker/dockerfile:experimental\n\n# Build BOOTy as an init\nFROM golang:1.14-alpine as dev\nRUN apk add --no-cache git ca-certificates make\nCOPY . /go/src/github.com/plunder-app/plunder\nWORKDIR /go/src/github.com/plunder-app/plunder\nENV GO111MODULE=on\nRUN --mount=type=cache,sharing=locked,id=gomod,target=/go/pkg/mod/cache \\\n    --mount=type=cache,sharing=locked,id=goroot,target=/root/.cache/go-build \\\n    CGO_ENABLED=0 GOOS=linux make build\n\nFROM scratch\nCOPY --from=dev /go/src/github.com/plunder-app/plunder/plunder /\nENTRYPOINT [\"/plunder\"]"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Makefile",
    "content": "\nSHELL := /bin/sh\n\n# The name of the executable (default is current directory name)\nTARGET := plunder\n.DEFAULT_GOAL: $(TARGET)\n\n# These will be provided to the target\nVERSION := 0.5.0\nBUILD := `git rev-parse HEAD`\n\n# Required for the move to go modules for >v0.5.0\nexport GO111MODULE=on\n\n# Operating System Default (LINUX)\nTARGETOS=linux\n\n# Use linker flags to provide version/build settings to the target\nLDFLAGS=-ldflags \"-X=main.Version=$(VERSION) -X=main.Build=$(BUILD) -s\"\n\nREPOSITORY = plndr\nDOCKERREPO ?= $(TARGET)\nDOCKERTAG ?= latest\n\n.PHONY: all build clean install uninstall fmt simplify check run lint vet\n\nall: check install\n\n$(TARGET): $(SRC)\n\t@go build $(LDFLAGS) -o $(TARGET)\n\nbuild: $(TARGET)\n\t@true\n\nclean:\n\t@rm -f $(TARGET)\n\ninstall:\n\t@echo Building and Installing project\n\t@go install $(LDFLAGS)\n\ninstall_plugin:\n\t@make plugins\n\t@echo Installing plugins\n\t-mkdir ~/plugin\n\t-cp -pr ./plugin/*.plugin ~/plugin/\n\nuninstall: clean\n\t@rm -f $$(which ${TARGET})\n\nfmt:\n\t@gofmt -l -w $(SRC)\n\nvet:\n\t@go vet $(SRC)\n\nlint:\n\t@golint $(SRC)\n\n# This is typically only for quick testing\ndockerx86:\n\t@docker buildx build  --platform linux/amd64 --load -t $(REPOSITORY)/$(TARGET):$(DOCKERTAG) -f Dockerfile .\n\t@echo New Multi Architecture Docker image created\n\ndocker:\n\t@docker buildx build  --platform linux/amd64,linux/arm64,linux/arm/v7 --push -t $(REPOSITORY)/$(TARGET):$(DOCKERTAG) -f Dockerfile .\n\t@echo New Multi Architecture Docker image created\n\nplugins:\n\t@echo Building plugins\n\t@GO111Module=off go build -buildmode=plugin -o ./plugin/example.plugin ./plugin/example.go\n\t@GO111Module=off go build -buildmode=plugin -o ./plugin/kubeadm.plugin ./plugin/kubeadm/*\n\t@GO111Module=off go build -buildmode=plugin -o ./plugin/docker.plugin ./plugin/docker/*\n\nrelease_darwin:\n\t@echo Creating Darwin Build\n\t@GOOS=darwin make build\n\t@GOOS=darwin make plugins\n\t@zip -9 -r plunder-darwin-$(VERSION).zip ./plunder ./plugin/*.plugin\n\t@rm plunder\n\t@rm ./plugin/*.plugin\n\nrelease_linux:\n\t@echo Creating Linux Build\n\t@GOOS=linux make build\n\t@GOOS=linux make plugins\n\t@zip -9 -r plunder-linux-$(VERSION).zip ./plunder ./plugin/*.plugin\n\t@rm plunder\n\t@rm ./plugin/*.plugin\n\nsimplify:\n\t@gofmt -s -l -w $(SRC)\n\ncheck:\n\t@test -z $(shell gofmt -l main.go | tee /dev/stderr) || echo \"[WARN] Fix formatting issues with 'make fmt'\"\n\tmake lint\n\tmake vet\n\nrun: install\n\t@$(TARGET)\n"
  },
  {
    "path": "Readme.md",
    "content": "\n# Plunder\n\nThe complete tool for finding **Infrastructure** gold amongst bits of bare-metal!\n\n![Plunder Captain](./image/plunder_captain.png)\n\n## Overview\n\nPlunder 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. \n\nFrom an end-user interaction a plunder control utility has been created: \n\n[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.\n\n### Services\n\n- `DHCP` - Allocating an IP addressing and pointing to a TFTP server\n- `TFTP` - Bootstrapping an Operating system install (uses iPXE)\n- `HTTP` - Provides a services where the bootstrap can pull the components needed for the OS install.\n\nAn 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.\n\n### Automation\n\nFurther 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:\n\n- `Remote command execution` - Over SSH (key configured above)\n- `Scripting engine` - A JSON/YAML language that also supports plugins to extend the capablities of the automation engine.\n\nA small repository of existing deployment maps has been created [https://github.com/plunder-app/maps](https://github.com/plunder-app/maps)\n\n### Additional features\n\n- `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`.\n- `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.\n- `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\n- `VMware deployment support` - Plunder can deploy preseed/kickstart and now vSphere installations.\n- `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.\n- `Logging of remote execution` - Plunder can now store all execution logs in-memory until told to clear them.\n\n## Getting Plunder\n\nPrebuilt binaries for Darwin(MacOS)/Linux and Windows can be found on the [releases](https://github.com/plunder-app/plunder/releases) page.\n\n### Building\n\nIf you wish to build the code yourself then this can be done simply by running:\n\n```\ngo get -u github.com/plunder-app/plunder\n```\nAlternatively 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`.\n\n## Usage!\n\nOne 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. \n\nOne thing to be aware of is that `plunder` doesn't require replacing anything that already exists in the infrastructure.\n\nThe documentation is available [here](./docs/)\n\n### Warning\n\n*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)\n\n*NOTE 2* As DHCP/TFTP and HTTP all bind to low ports < 1024, root access (or sudo) is required to start the plunder services.\n\n# Troubleshooting\n\nPXE 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.\n\n# Roadmap\n\n- Ability to automate deployments over VMware VMTools\n\n- Windows deployments\n\n- Tidier logging\n\n- Stability enhancements\n\n- Additional plugins\n\n  \n"
  },
  {
    "path": "cmd/automate.go",
    "content": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\n\t\"github.com/ghodss/yaml\"\n\t\"github.com/plunder-app/plunder/pkg/parlay\"\n\t\"github.com/plunder-app/plunder/pkg/parlay/parlaytypes\"\n\tparlayplugin \"github.com/plunder-app/plunder/pkg/parlay/plugin\"\n\t\"github.com/plunder-app/plunder/pkg/ssh\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\n// These flags are used to determine a deployment\nvar deploymentSSH, mapFile, logFile, deploymentEndpoint *string\n\n// These flags are used to override SSH configuration\nvar usernameSSH, keypathSSH, addressSSH *string\n\n// These flags are used to determine if a particular deployment, action and specific host need to be used.\nvar deploymentName, actionName, host *string\n\n// These flags are used for management of plugins\nvar pluginPath, pluginAction, pluginActions *string\n\n// This flag determines if a singular action should occur or whether to resume all actions from this point\nvar resume *bool\n\n// UI Json output only, when this is try the UI selections will just create the associated JSON\nvar jsonOutput, yamlOutput *bool\n\nfunc init() {\n\n\t// Global flags for automation\n\tlogFile = plunderAutomate.PersistentFlags().String(\"logfile\", \"\", \"Path to where plunder will write automation logs\")\n\tmapFile = plunderAutomate.PersistentFlags().String(\"map\", \"\", \"Path to a plunder map\")\n\n\t// SSH Deployment flags\n\tdeploymentSSH = plunderAutomate.PersistentFlags().String(\"deployconfig\", \"\", \"Path to a plunder deployment configuration\")\n\tusernameSSH = plunderAutomate.PersistentFlags().String(\"overrideUsername\", \"\", \"(optional) Override Username\")\n\tkeypathSSH = plunderAutomate.PersistentFlags().String(\"overrideKeypath\", \"\", \"(Optional) Override path to a key\")\n\taddressSSH = plunderAutomate.PersistentFlags().String(\"overrideAddress\", \"\", \"(Optional) Override address to automate against\")\n\n\t// Plunder endpoing Deployment flags\n\tdeploymentEndpoint = plunderAutomate.PersistentFlags().String(\"deployendpoint\", \"\", \"URL of plunder server to pull the deployment configuration\")\n\n\t// Deployment control flags\n\tdeploymentName = plunderAutomateSSH.Flags().String(\"deployment\", \"\", \"Automate a specific deployment\")\n\tactionName = plunderAutomateSSH.Flags().String(\"action\", \"\", \"Automate a specific action\")\n\thost = plunderAutomateSSH.Flags().String(\"host\", \"\", \"Automate the deployment for a specific host\")\n\tresume = plunderAutomateSSH.Flags().Bool(\"resume\", false, \"Resume all actions after the one specified by --action\")\n\n\t// Plugin Flags\n\tpluginPath = plunderAutomatePluginUsage.Flags().String(\"plugin\", \"\", \"Path to a specific plugin typically ~./plugin/[X].plugin\")\n\tpluginAction = plunderAutomatePluginUsage.Flags().String(\"action\", \"\", \"Action to retrieve the usage of\")\n\tpluginActions = plunderAutomatePluginActions.Flags().String(\"plugin\", \"\", \"Path to a specific plugin typically ~./plugin/[X].plugin\")\n\n\tjsonOutput = plunderAutomateUI.Flags().Bool(\"json\", false, \"Print the JSON to stdout, no execution of commands\")\n\tyamlOutput = plunderAutomateUI.Flags().Bool(\"yaml\", false, \"Print the YAML to stdout, no execution of commands\")\n\n\tplunderAutomatePlugins.AddCommand(plunderAutomatePluginUsage)\n\tplunderAutomatePlugins.AddCommand(plunderAutomatePluginActions)\n\tplunderAutomatePlugins.AddCommand(plunderAutomatePluginTest)\n\n\t// Automate Subcommands\n\tplunderAutomate.AddCommand(plunderAutomateValidate)\n\tplunderAutomate.AddCommand(plunderAutomateSSH)\n\tplunderAutomate.AddCommand(plunderAutomateVMware)\n\tplunderAutomate.AddCommand(plunderAutomatePlugins)\n\tplunderAutomate.AddCommand(plunderAutomateUI)\n\n\tplunderCmd.AddCommand(plunderAutomate)\n}\n\n// PlunderAutomate\nvar plunderAutomate = &cobra.Command{\n\tUse:   \"automate\",\n\tShort: \"Automate the deployment of a platform/application\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\t\tcmd.Help()\n\t\treturn\n\t},\n}\n\n// plunderAutomatePlugins\nvar plunderAutomatePlugins = &cobra.Command{\n\tUse:   \"plugin\",\n\tShort: \"Automate the deployment of a platform/application\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\t\tparlayplugin.ListPlugins()\n\t\treturn\n\t},\n}\n\n// plunderAutomatePlugins\nvar plunderAutomatePluginUsage = &cobra.Command{\n\tUse:   \"usage\",\n\tShort: \"Display the usage of a plugin action\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\t\tparlayplugin.UsagePlugin(*pluginPath, *pluginAction)\n\t\treturn\n\t},\n}\n\n// plunderAutomatePlugins\nvar plunderAutomatePluginActions = &cobra.Command{\n\tUse:   \"actions\",\n\tShort: \"Display the actions of a particular plugin\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\t\tparlayplugin.ListPluginActions(*pluginActions)\n\t\treturn\n\t},\n}\n\n// plunderAutomatePlugins\nvar plunderAutomatePluginTest = &cobra.Command{\n\tUse:   \"test\",\n\tShort: \"Test the actions of the example plugin\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\n\t\ttest := `{ \"name\": \"Example of test action\", \"type\": \"exampleAction/test\", \"plugin\": { \"credentials\": \"AAABBBCCCCDDEEEE\", \"address\": \"172.0.0.1\" }\t}`\n\t\tvar action parlaytypes.Action\n\t\t_ = json.Unmarshal([]byte(test), &action)\n\n\t\t_, err := parlayplugin.ExecuteActionInPlugin(\"./plugin/example.plugin\", \"127.0.0.1\", \"example/test\", action.Plugin)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"%v\", err)\n\t\t}\n\t\treturn\n\t},\n}\n\n// plunderAutomateSSH\nvar plunderAutomateSSH = &cobra.Command{\n\tUse:   \"ssh\",\n\tShort: \"Automate over ssh\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\t\t// If deploymentPath is not blank then the flag has been used\n\t\tif *deploymentSSH != \"\" {\n\t\t\tlog.Infof(\"Reading deployment configuration from [%s]\", *deploymentSSH)\n\n\t\t\t// Check the actual path from the string\n\t\t\tif _, err := os.Stat(*deploymentSSH); !os.IsNotExist(err) {\n\t\t\t\tconfig, err := ioutil.ReadFile(*deploymentSSH)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\t\t\t\terr = ssh.ImportHostsFromRawDeployment(config)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcmd.Help()\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlog.Fatalf(\"Unable to open [%s]\", *deploymentSSH)\n\t\t\t}\n\n\t\t} else if *deploymentEndpoint != \"\" {\n\t\t\tu, err := url.Parse(*deploymentEndpoint)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\n\t\t\t// TODO - fix dynamic\n\t\t\tu.Path = \"/deployments\"\n\n\t\t\tresp, err := http.Get(u.String())\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\n\t\t\t//var config server.DeploymentConfigurationFile\n\t\t\tdefer resp.Body.Close()\n\t\t\tconfig, err := ioutil.ReadAll(resp.Body)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\t\t\terr = ssh.ImportHostsFromRawDeployment(config)\n\t\t\tif err != nil {\n\t\t\t\tcmd.Help()\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Add a host from the override flags\n\t\tif *addressSSH != \"\" {\n\t\t\terr := ssh.AddHost(*addressSSH, *keypathSSH, *usernameSSH)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\t\t}\n\n\t\t// If there are zero hosts in the ssh Host array then we have no authentication information\n\t\tif len(ssh.Hosts) == 0 {\n\t\t\tcmd.Help()\n\t\t\tlog.Fatalf(\"No Deployment information imported\")\n\t\t}\n\n\t\tlog.Infof(\"Found [%d] ssh configurations\", len(ssh.Hosts))\n\n\t\tif *mapFile != \"\" {\n\t\t\tlog.Infof(\"Reading deployment configuration from [%s]\", *mapFile)\n\n\t\t\tvar deployment parlaytypes.TreasureMap\n\t\t\t// // Check the actual path from the string\n\t\t\tif _, err := os.Stat(*mapFile); !os.IsNotExist(err) {\n\t\t\t\tb, err := ioutil.ReadFile(*mapFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\n\t\t\t\tdeployment, err = parseMapFile(b)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\n\t\t\t\t// If a specific deployment is being used then find it's details\n\t\t\t\tif *deploymentName != \"\" {\n\t\t\t\t\tlog.Infof(\"Looking for deployment [%s]\", *deploymentName)\n\n\t\t\t\t\tfoundDeployment, err := deployment.FindDeployment(*deploymentName, *actionName, *host, *logFile, *resume)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Fatalf(\"%s\", err)\n\t\t\t\t\t}\n\t\t\t\t\terr = parlay.DeploySSH(foundDeployment, *logFile, false, false)\n\t\t\t\t} else {\n\t\t\t\t\t// Parse the entire deployment\n\t\t\t\t\terr = parlay.DeploySSH(&deployment, *logFile, false, false)\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\t\t}\n\n\t\treturn\n\t},\n}\n\n// plunderAutomateValidate\nvar plunderAutomateValidate = &cobra.Command{\n\tUse:   \"validate\",\n\tShort: \"Validate a deployment map\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\n\t\tif *mapFile != \"\" {\n\t\t\tlog.Infof(\"Reading deployment configuration from [%s]\", *mapFile)\n\t\t\t//var err error\n\t\t\tvar deployment parlaytypes.TreasureMap\n\t\t\t// // Check the actual path from the string\n\t\t\tif _, err := os.Stat(*mapFile); !os.IsNotExist(err) {\n\t\t\t\tb, err := ioutil.ReadFile(*mapFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\t\t\t\tdeployment, err = parseMapFile(b)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\t\t\t\tdeploymentCount := len(deployment.Deployments)\n\t\t\t\tif deploymentCount == 0 {\n\t\t\t\t\tlog.Fatalf(\"Zero deployments have been found\")\n\t\t\t\t}\n\t\t\t\tlog.Infof(\"Validating [%d] deployments\", deploymentCount)\n\t\t\t\tfor x := range deployment.Deployments {\n\t\t\t\t\tactionCount := len(deployment.Deployments[x].Actions)\n\t\t\t\t\tif actionCount == 0 {\n\t\t\t\t\t\tlog.Fatalf(\"Zero deployments have been found\")\n\t\t\t\t\t}\n\t\t\t\t\tlog.Infof(\"Validating [%d] actions\", actionCount)\n\t\t\t\t\tfor y := range deployment.Deployments[x].Actions {\n\t\t\t\t\t\terr := parlay.ValidateAction(&deployment.Deployments[x].Actions[y])\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.Warnf(\"Action [%s] Error [%v]\", deployment.Deployments[x].Actions[y].Name, err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlog.Fatalf(\"Unable to open [%s]\", *mapFile)\n\t\t\t}\n\t\t} else {\n\t\t\tcmd.Help()\n\t\t\tlog.Fatalln(\"No Deployment map specified\")\n\t\t}\n\t},\n}\n\n// plunderAutomateVMware\nvar plunderAutomateVMware = &cobra.Command{\n\tUse:   \"vmw\",\n\tShort: \"Automate over VMware tools protocol\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\t\t//var newMap *parlay.TreasureMap\n\t\tif *mapFile != \"\" {\n\t\t\tlog.Infof(\"Reading deployment configuration from [%s]\", *mapFile)\n\t\t\t//var err error\n\t\t\tvar deployment parlaytypes.TreasureMap\n\t\t\t// // Check the actual path from the string\n\t\t\tif _, err := os.Stat(*mapFile); !os.IsNotExist(err) {\n\t\t\t\tb, err := ioutil.ReadFile(*mapFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\t\t\t\tdeployment, err = parseMapFile(b)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\t\t\t\t// If a specific deployment is being used then find it's details\n\t\t\t\tif *deploymentName != \"\" {\n\t\t\t\t\tlog.Infof(\"Looking for deployment [%s]\", *deploymentName)\n\n\t\t\t\t\tfoundDeployment, err := deployment.FindDeployment(*deploymentName, *actionName, *host, *logFile, *resume)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Fatalf(\"%s\", err)\n\t\t\t\t\t}\n\t\t\t\t\terr = parlay.DeploySSH(foundDeployment, *logFile, false, false)\n\t\t\t\t} else {\n\t\t\t\t\t// Parse the entire deployment\n\t\t\t\t\terr = parlay.DeploySSH(&deployment, *logFile, false, false)\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\t\t}\n\t},\n}\n\n// plunderAutomateUI\nvar plunderAutomateUI = &cobra.Command{\n\tUse:   \"ui\",\n\tShort: \"Enable the user interface to manage a deployment\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\n\t\tvar newMap *parlaytypes.TreasureMap\n\t\tif *mapFile != \"\" {\n\t\t\tlog.Infof(\"Reading deployment configuration from [%s]\", *mapFile)\n\t\t\t//var err error\n\t\t\tvar deployment parlaytypes.TreasureMap\n\t\t\t// // Check the actual path from the string\n\t\t\tif _, err := os.Stat(*mapFile); !os.IsNotExist(err) {\n\t\t\t\tb, err := ioutil.ReadFile(*mapFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\t\t\t\tdeployment, err = parseMapFile(b)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\t\t\t\tnewMap, err = parlay.StartUI(&deployment)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\n\t\t// If we're using the UI to build a new map then print to stdout(in either format)\n\t\tif *jsonOutput == true {\n\t\t\tb, _ := json.MarshalIndent(newMap, \"\", \"\\t\")\n\t\t\tfmt.Printf(\"%s\\n\", b)\n\t\t\treturn\n\t\t}\n\n\t\tif *yamlOutput == true {\n\t\t\tb, _ := yaml.Marshal(newMap)\n\t\t\tfmt.Printf(\"%s\\n\", b)\n\t\t\treturn\n\t\t}\n\n\t\tif *deploymentSSH != \"\" {\n\t\t\tlog.Infof(\"Reading deployment configuration from [%s]\", *deploymentSSH)\n\t\t\t// Check the actual path from the string\n\t\t\tif _, err := os.Stat(*deploymentSSH); !os.IsNotExist(err) {\n\t\t\t\tconfig, err := ioutil.ReadFile(*deploymentSSH)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\n\t\t\t\t// Parse all of the hosts in the deployment configuration and update the ssh package with their details\n\t\t\t\terr = ssh.ImportHostsFromRawDeployment(config)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcmd.Help()\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlog.Fatalf(\"Unable to open [%s]\", *deploymentSSH)\n\t\t\t}\n\t\t} else if *deploymentEndpoint != \"\" {\n\t\t\t// Parse the endpoint, this will attempt to pull all of the configuration information and pass it to the SSH package\n\t\t\tu, err := url.Parse(*deploymentEndpoint)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\t\t\tu.Path = \"/deployment\"\n\n\t\t\tresp, err := http.Get(u.String())\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\n\t\t\tdefer resp.Body.Close()\n\t\t\tconfig, err := ioutil.ReadAll(resp.Body)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\t\t\terr = ssh.ImportHostsFromRawDeployment(config)\n\t\t\tif err != nil {\n\t\t\t\tcmd.Help()\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Add a host from the override flags\n\t\tif *addressSSH != \"\" {\n\t\t\terr := ssh.AddHost(*addressSSH, *keypathSSH, *usernameSSH)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\t\t}\n\n\t\t// If there are zero hosts in the ssh Host array then we have no authentication information\n\t\tif len(ssh.Hosts) == 0 {\n\t\t\tcmd.Help()\n\t\t\tlog.Fatalf(\"No Deployment information imported\")\n\t\t}\n\n\t\terr := parlay.DeploySSH(newMap, *logFile, false, false)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"%v\", err)\n\t\t}\n\t},\n}\n\nfunc parseMapFile(b []byte) (deployment parlaytypes.TreasureMap, err error) {\n\n\tjsonBytes, err := yaml.YAMLToJSON(b)\n\tif err == nil {\n\t\t// If there were no errors then the YAML => JSON was succesful, no attempt to unmarshall\n\t\terr = json.Unmarshal(jsonBytes, &deployment)\n\t\tif err != nil {\n\t\t\treturn deployment, fmt.Errorf(\"Unable to parse [%s] as either yaml or json\", *mapFile)\n\t\t}\n\n\t} else {\n\t\t// Couldn't parse the yaml to JSON\n\t\t// Attempt to parse it as JSON\n\t\terr = json.Unmarshal(b, &deployment)\n\t\tif err != nil {\n\t\t\treturn deployment, fmt.Errorf(\"Unable to parse [%s] as either yaml or json\", *mapFile)\n\t\t}\n\t}\n\treturn deployment, nil\n\n}\n"
  },
  {
    "path": "cmd/configGenerator.go",
    "content": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ghodss/yaml\"\n\tbooty \"github.com/plunder-app/BOOTy/pkg/plunderclient/types\"\n\t\"github.com/plunder-app/plunder/pkg/apiserver\"\n\t\"github.com/plunder-app/plunder/pkg/certs\"\n\t\"github.com/plunder-app/plunder/pkg/parlay/parlaytypes\"\n\t\"github.com/plunder-app/plunder/pkg/services\"\n\t\"github.com/plunder-app/plunder/pkg/utils\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// These variables are used to capture input from the CLI\nvar output, detectNic, serverPath, clientPath string\nvar configAPIServerPort int\nvar pretty bool\n\nfunc init() {\n\tplunderCmd.AddCommand(plunderConfig)\n\tplunderConfig.PersistentFlags().StringVarP(&output, \"output\", \"o\", \"json\", \"Ouput type, should be either JSON or YAML\")\n\tplunderConfig.PersistentFlags().BoolVarP(&pretty, \"pretty\", \"p\", false, \"Ouput JSON in a pretty/Human readable format\")\n\tplunderServerConfig.PersistentFlags().StringVarP(&detectNic, \"nic\", \"n\", \"\", \"Build configuration for a particular network interface\")\n\n\t// Persistent above both client functions\n\tplunderAPIConfig.PersistentFlags().IntVar(&configAPIServerPort, \"port\", 60443, \"Port that the plunder API server should use\")\n\n\t// Path for Server\n\tplunderAPIConfigServer.Flags().StringVar(&serverPath, \"path\", \".plunderserver.yaml\", \"Path that the plunder API server config should be written to\")\n\t// Path for Client\n\tplunderAPIConfigClient.Flags().StringVar(&clientPath, \"path\", \"plunderclient.yaml\", \"Path that the plunder API client config should be written to\")\n\n\t// Add sub commands to APIServer\n\tplunderAPIConfig.AddCommand(plunderAPIConfigClient)\n\tplunderAPIConfig.AddCommand(plunderAPIConfigServer)\n\n\t// Add all sub commands to the config sub command\n\tplunderConfig.AddCommand(plunderAPIConfig)\n\tplunderConfig.AddCommand(plunderServerConfig)\n\tplunderConfig.AddCommand(plunderDeploymentConfig)\n\tplunderConfig.AddCommand(PlunderParlayConfig)\n\n\tplunderCmd.AddCommand(plunderGet)\n\n}\n\n// PlunderConfig - This is for intialising a blank or partial configuration\nvar plunderConfig = &cobra.Command{\n\tUse:   \"config\",\n\tShort: \"Initialise a plunder configuration\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\t\tcmd.Help()\n\t\treturn\n\t},\n}\n\n// PlunderServerConfig - This is for intialising a blank or partial configuration\nvar plunderServerConfig = &cobra.Command{\n\tUse:   \"server\",\n\tShort: \"Initialise a plunder configuration\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\t\t// Indent (or pretty-print) the configuration output\n\t\tbc := &services.BootConfig{\n\t\t\tKernel:     \"/kernelPath\",\n\t\t\tInitrd:     \"/initPath\",\n\t\t\tCmdline:    \"cmd=options\",\n\t\t\tConfigName: \"demo config\",\n\t\t\tConfigType: \"default\",\n\t\t}\n\n\t\tdetectServerConfig()\n\n\t\tservices.Controller.BootConfigs = append(services.Controller.BootConfigs, *bc)\n\t\terr := renderOutput(services.Controller, pretty)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"%v\", err)\n\t\t}\n\t\treturn\n\t},\n}\n\n// PlunderDeploymentConfig - This is for intialising a blank or partial configuration\nvar plunderDeploymentConfig = &cobra.Command{\n\tUse:   \"deployment\",\n\tShort: \"Initialise a server configuration\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\t\t// Create an example Global configuration\n\t\tglobalConfig := services.HostConfig{\n\t\t\tGateway:    \"192.168.0.1\",\n\t\t\tNTPServer:  \"192.168.0.1\",\n\t\t\tNameServer: \"192.168.0.1\",\n\t\t\tAdapter:    \"ens192\",\n\t\t\tSubnet:     \"255.255.255.0\",\n\t\t\t// OS Provision\n\t\t\tUsername:          \"user\",\n\t\t\tPassword:          \"pass\",\n\t\t\tPackages:          \"openssh-server cloud-guest-utils\",\n\t\t\tRepositoryAddress: \"192.168.0.1\",\n\t\t\tMirrorDirectory:   \"/ubuntu\",\n\t\t\tSSHKeyPath:        \"/home/deploy/.ssh/id_pub.rsa\",\n\t\t\tSSHKey:            \"ssh-rsa AABBCCDDEE1122334455\",\n\t\t\t// BOOTy\n\t\t\tBOOTYAction:        booty.ReadImage,\n\t\t\tLVMRootName:        \"/dev/ubuntu-vg/root\",\n\t\t\tDestinationDevice:  \"/dev/sda\",\n\t\t\tDestinationAddress: \"http://192.168.0.1/image\",\n\t\t\tSourceImage:        \"http://192.168.0.1/images/ubuntu.img\",\n\t\t\tSourceDevice:       \"/dev/sda\",\n\t\t}\n\n\t\t// Set compressed pointer\n\t\tcompressed := false\n\t\tglobalConfig.Compressed = &compressed\n\n\t\t// Addtional step to create the partition information\n\t\tdefaultPartition := 1\n\t\tglobalConfig.GrowPartition = &defaultPartition\n\n\t\t// Create an example Host configuration\n\t\thostConfig := services.HostConfig{\n\t\t\tIPAddress:  \"192.168.0.2\",\n\t\t\tServerName: \"Server01\",\n\t\t}\n\t\thostDeployConfig := services.DeploymentConfig{\n\t\t\tMAC:        \"00:11:22:33:44:55\",\n\t\t\tConfigHost: hostConfig,\n\t\t\t//ConfigName: \"default\",\n\t\t}\n\n\t\tconfiguration := &services.DeploymentConfigurationFile{\n\t\t\tGlobalServerConfig: globalConfig,\n\t\t}\n\n\t\tconfiguration.Configs = append(configuration.Configs, hostDeployConfig)\n\t\t// Indent (or pretty-print) the configuration output\n\t\terr := renderOutput(configuration, pretty)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"%v\", err)\n\t\t}\n\t\treturn\n\t},\n}\n\n// PlunderParlayConfig - This is for intialising a parlay deployment\nvar PlunderParlayConfig = &cobra.Command{\n\tUse:   \"parlay\",\n\tShort: \"Initialise a parlay configuration\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\n\t\tparlayActionPackage := parlaytypes.Action{\n\t\t\tName:         \"Add package\",\n\t\t\tActionType:   \"pkg\",\n\t\t\tPkgManager:   \"apt\",\n\t\t\tPkgOperation: \"install\",\n\t\t\tPackages:     \"mysql\",\n\t\t}\n\n\t\tparlayActionCommand := parlaytypes.Action{\n\t\t\tName:             \"Run Command\",\n\t\t\tActionType:       \"command\",\n\t\t\tCommand:          \"which uptime\",\n\t\t\tCommandSudo:      \"root\",\n\t\t\tCommandSaveAsKey: \"cmdKey\",\n\t\t}\n\t\tparlayActionUpload := parlaytypes.Action{\n\t\t\tName:        \"Upload File\",\n\t\t\tActionType:  \"upload\",\n\t\t\tSource:      \"./my_file\",\n\t\t\tDestination: \"/tmp/file\",\n\t\t}\n\n\t\tparlayActionDownload := parlaytypes.Action{\n\t\t\tName:        \"Download File\",\n\t\t\tActionType:  \"download\",\n\t\t\tDestination: \"./my_file\",\n\t\t\tSource:      \"/tmp/file\",\n\t\t}\n\n\t\tparlayActionKey := parlaytypes.Action{\n\t\t\tName:       \"Execute key\",\n\t\t\tActionType: \"command\",\n\t\t\tKeyName:    \"cmdKey\",\n\t\t}\n\n\t\tparlayDeployment := parlaytypes.Deployment{\n\t\t\tName:  \"Install MySQL\",\n\t\t\tHosts: []string{\"192.168.0.1\", \"192.168.0.2\"},\n\t\t}\n\n\t\tparlayDeployment.Actions = append(parlayDeployment.Actions, parlayActionPackage)\n\t\tparlayDeployment.Actions = append(parlayDeployment.Actions, parlayActionCommand)\n\t\tparlayDeployment.Actions = append(parlayDeployment.Actions, parlayActionUpload)\n\t\tparlayDeployment.Actions = append(parlayDeployment.Actions, parlayActionDownload)\n\t\tparlayDeployment.Actions = append(parlayDeployment.Actions, parlayActionKey)\n\n\t\tparlayConfig := &parlaytypes.TreasureMap{}\n\t\tparlayConfig.Deployments = []parlaytypes.Deployment{}\n\t\tparlayConfig.Deployments = append(parlayConfig.Deployments, parlayDeployment)\n\n\t\t// Render the output to screen\n\t\terr := renderOutput(parlayConfig, pretty)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"%v\", err)\n\t\t}\n\t\treturn\n\t},\n}\n\n// plunderGet - The Get command will pull any required components (iPXE boot files)\nvar plunderGet = &cobra.Command{\n\tUse:   \"get\",\n\tShort: \"Get any components needed for bootstrapping (internet access required)\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\n\t\terr := utils.PullPXEBooter()\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"%v\", err)\n\t\t}\n\t\treturn\n\t},\n}\n\n// plunderAPIConfig - The Get command will pull any required components (iPXE boot files)\nvar plunderAPIConfig = &cobra.Command{\n\tUse:   \"apiserver\",\n\tShort: \"Generate the configuration for the api server\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\t\tcmd.Help()\n\t\treturn\n\t},\n}\n\n// plunderAPIConfigServer - The Get command will pull any required components (iPXE boot files)\nvar plunderAPIConfigServer = &cobra.Command{\n\tUse:   \"server\",\n\tShort: \"Generate the configuration for the api server\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\n\t\terr := certs.GenerateKeyPair(nil, time.Now(), (24*time.Hour)*365)\n\t\tif err != nil {\n\t\t\tlog.Fatalln(err)\n\t\t}\n\n\t\terr = apiserver.WriteServerConfig(serverPath, \"\", \"\", configAPIServerPort, certs.GetPem(), certs.GetKey())\n\t\tif err != nil {\n\t\t\tlog.Fatalln(err)\n\t\t}\n\t\treturn\n\t},\n}\n\n// plunderAPIConfigServer - The Get command will pull any required components (iPXE boot files)\nvar plunderAPIConfigClient = &cobra.Command{\n\tUse:   \"client\",\n\tShort: \"Generate the configuration for a client for the API server\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\n\t\ts, err := apiserver.OpenServerConfig(serverPath)\n\t\tif err != nil {\n\t\t\tlog.Fatalln(err)\n\t\t}\n\t\thostname, err := os.Hostname()\n\t\tif err != nil {\n\t\t\tlog.Fatalln(err)\n\t\t}\n\t\tapiserver.WriteClientConfig(clientPath, hostname, s)\n\t\treturn\n\t},\n}\n\nfunc renderOutput(data interface{}, pretty bool) error {\n\tvar d []byte\n\tvar err error\n\tswitch strings.ToLower(output) {\n\tcase \"yaml\":\n\t\td, err = yaml.Marshal(data)\n\tcase \"json\":\n\t\tif pretty {\n\t\t\td, err = json.MarshalIndent(data, \"\", \"\\t\")\n\t\t} else {\n\t\t\td, err = json.Marshal(data)\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"Unknown output type [%s]\", output)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\t// Print out the output to STDOUT\n\tfmt.Printf(\"%s\\n\", d)\n\treturn nil\n}\n\nfunc detectServerConfig() error {\n\n\t// Find an example nic to use, that isn't the loopback address\n\tnicName, nicAddr, err := utils.FindIPAddress(detectNic)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Attempt to parse th returned IP address and apply simple incrementation to determin DHCP start range\n\tip := net.ParseIP(nicAddr)\n\tip = ip.To4()\n\tif ip == nil {\n\t\treturn fmt.Errorf(\"error parsing IP address of adapter [%s]\", detectNic)\n\t}\n\tip[3]++\n\n\t// Prepopulate the flags with the found nic information\n\tservices.Controller.AdapterName = &nicName\n\tservices.Controller.HTTPAddress = &nicAddr\n\tservices.Controller.TFTPAddress = &nicAddr\n\n\t*services.Controller.PXEFileName = \"undionly.kpxe\"\n\n\t// DHCP Settings\n\tservices.Controller.DHCPConfig.DHCPAddress = nicAddr\n\tservices.Controller.DHCPConfig.DHCPSubnet = \"255.255.255.0\"\n\tservices.Controller.DHCPConfig.DHCPGateway = nicAddr\n\tservices.Controller.DHCPConfig.DHCPDNS = nicAddr\n\tservices.Controller.DHCPConfig.DHCPLeasePool = 20\n\tservices.Controller.DHCPConfig.DHCPStartAddress = ip.String()\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/plunder.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\n\t\"github.com/plunder-app/plunder/pkg/utils\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\n// Release - this struct contains the release information populated when building plunder\nvar Release struct {\n\tVersion string\n\tBuild   string\n}\n\nvar plunderCmd = &cobra.Command{\n\tUse:   \"plunder\",\n\tShort: \"This is a tool for finding gold amongst bare-metal (and provisioning kubernetes)\",\n}\n\nvar logLevel int\nvar filePath string\n\nfunc init() {\n\tplunderUtilsEncode.Flags().StringVar(&filePath, \"path\", \"\", \"Path to a file to encode\")\n\t// Global flag across all subcommands\n\tplunderCmd.PersistentFlags().IntVar(&logLevel, \"logLevel\", 4, \"Set the logging level [0=panic, 3=warning, 5=debug]\")\n\tplunderCmd.AddCommand(plunderVersion)\n\tplunderCmd.AddCommand(plunderUtils)\n\tplunderUtils.AddCommand(plunderUtilsEncode)\n}\n\n// Execute - starts the command parsing process\nfunc Execute() {\n\tif os.Getenv(\"PLUNDER_LOGLEVEL\") != \"\" {\n\t\ti, err := strconv.ParseInt(os.Getenv(\"PLUNDER_LOGLEVEL\"), 10, 8)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Error parsing environment variable [PLUNDER_LOGLEVEL\")\n\t\t}\n\t\t// We've only parsed to an 8bit integer, however i is still a int64 so needs casting\n\t\tlogLevel = int(i)\n\t} else {\n\t\t// Default to logging anything Info and below\n\t\tlogLevel = int(log.InfoLevel)\n\t}\n\n\tlog.SetLevel(log.Level(logLevel))\n\tif err := plunderCmd.Execute(); err != nil {\n\t\tfmt.Println(err)\n\t\tos.Exit(1)\n\t}\n}\n\nvar plunderVersion = &cobra.Command{\n\tUse:   \"version\",\n\tShort: \"Version and Release information about the plunder tool\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tfmt.Printf(\"Plunder Release Information\\n\")\n\t\tfmt.Printf(\"Version:  %s\\n\", Release.Version)\n\t\tfmt.Printf(\"Build:    %s\\n\", Release.Build)\n\t},\n}\n\nvar plunderUtils = &cobra.Command{\n\tUse:   \"utils\",\n\tShort: \"Additional utilities for Plunder\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tcmd.Help()\n\t},\n}\n\nvar plunderUtilsEncode = &cobra.Command{\n\tUse:   \"encode\",\n\tShort: \"This will encode a file into Hex\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\thex, err := utils.FileToHex(filePath)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"%v\", err)\n\t\t}\n\t\tfmt.Printf(\"%s\", hex)\n\t},\n}\n"
  },
  {
    "path": "cmd/server.go",
    "content": "package cmd\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\n\t\"github.com/plunder-app/plunder/pkg/apiserver\"\n\t\"github.com/plunder-app/plunder/pkg/parlay\"\n\t\"github.com/plunder-app/plunder/pkg/services\"\n\t\"github.com/plunder-app/plunder/pkg/utils\"\n\t\"github.com/spf13/cobra\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n//var controller server.BootController\nvar dhcpSettings services.DHCPSettings\n\nvar apiServerPath, gateway, dns, startAddress, configPath, deploymentPath, defaultKernel, defaultInitrd, defaultCmdLine *string\n\nvar leasecount, port *int\n\nvar anyboot, insecure *bool\n\nfunc init() {\n\n\t// Prepopulate the flags with the found nic information\n\tservices.Controller.AdapterName = PlunderServer.Flags().String(\"adapter\", \"\", \"Name of adapter to use e.g eth0, en0\")\n\n\tservices.Controller.HTTPAddress = PlunderServer.Flags().String(\"addressHTTP\", \"\", \"Address of HTTP to use, if blank will default to [addressDHCP]\")\n\tservices.Controller.TFTPAddress = PlunderServer.Flags().String(\"addressTFTP\", \"\", \"Address of TFTP to use, if blank will default to [addressDHCP]\")\n\n\tservices.Controller.EnableDHCP = PlunderServer.Flags().Bool(\"enableDHCP\", false, \"Enable the DCHP Server\")\n\tservices.Controller.EnableTFTP = PlunderServer.Flags().Bool(\"enableTFTP\", false, \"Enable the TFTP Server\")\n\tservices.Controller.EnableHTTP = PlunderServer.Flags().Bool(\"enableHTTP\", false, \"Enable the HTTP Server\")\n\n\tservices.Controller.PXEFileName = PlunderServer.Flags().String(\"iPXEPath\", \"undionly.kpxe\", \"Path to an iPXE bootloader\")\n\n\t// DHCP Settings\n\tPlunderServer.Flags().StringVar(&services.Controller.DHCPConfig.DHCPAddress, \"addressDHCP\", \"\", \"Address to advertise leases from, ideally will be the IP address of --adapter\")\n\tPlunderServer.Flags().StringVar(&services.Controller.DHCPConfig.DHCPGateway, \"gateway\", \"\", \"Address of Gateway to use, if blank will default to [addressDHCP]\")\n\tPlunderServer.Flags().StringVar(&services.Controller.DHCPConfig.DHCPDNS, \"dns\", \"\", \"Address of DNS to use, if blank will default to [addressDHCP]\")\n\tPlunderServer.Flags().IntVar(&services.Controller.DHCPConfig.DHCPLeasePool, \"leasecount\", 20, \"Amount of leases to advertise\")\n\tPlunderServer.Flags().StringVar(&services.Controller.DHCPConfig.DHCPStartAddress, \"startAddress\", \"\", \"Start advertised address [REQUIRED]\")\n\n\t//HTTP Settings\n\tdefaultKernel = PlunderServer.Flags().String(\"kernel\", \"\", \"Path to a kernel to set as the *default* kernel\")\n\tdefaultInitrd = PlunderServer.Flags().String(\"initrd\", \"\", \"Path to a ramdisk to set as the *default* ramdisk\")\n\tdefaultKernel = PlunderServer.Flags().String(\"cmdline\", \"\", \"Additional command line to pass to the *default* kernel\")\n\n\t// Config File\n\tconfigPath = PlunderServer.Flags().String(\"config\", \"\", \"Path to a plunder server configuration\")\n\tdeploymentPath = PlunderServer.Flags().String(\"deployment\", \"\", \"Path to a plunder deployment configuration\")\n\tPlunderServer.Flags().StringVar(&services.DefaultBootType, \"defaultBoot\", \"\", \"In the event a boot type can't be found default to this\")\n\n\t// API Server configuration\n\tport = PlunderServer.Flags().IntP(\"port\", \"p\", 60443, \"Port that the Plunder API server will listen on\")\n\tinsecure = PlunderServer.Flags().BoolP(\"insecure\", \"i\", false, \"Start the Plunder API server without encryption\")\n\tapiServerPath = PlunderServer.Flags().String(\"path\", \".plunderserver.yaml\", \"Path to configuration for the API Server\")\n\n\tplunderCmd.AddCommand(PlunderServer)\n}\n\n// PlunderServer - This is for intialising a blank or partial configuration\nvar PlunderServer = &cobra.Command{\n\tUse:   \"server\",\n\tShort: \"Start Plunder Services\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tlog.SetLevel(log.Level(logLevel))\n\t\tvar deployment []byte\n\t\t// If deploymentPath is not blank then the flag has been used\n\t\tif *deploymentPath != \"\" {\n\t\t\t// if *anyboot == true {\n\t\t\t// \tlog.Errorf(\"AnyBoot has been enabled, all configuration will be ignored\")\n\t\t\t// }\n\t\t\tlog.Infof(\"Reading deployment configuration from [%s]\", *deploymentPath)\n\t\t\tif _, err := os.Stat(*deploymentPath); !os.IsNotExist(err) {\n\t\t\t\tdeployment, err = ioutil.ReadFile(*deploymentPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// if *anyboot == true {\n\t\t// \tservices.AnyBoot = true\n\t\t// }\n\n\t\t// If configPath is not blank then the flag has been used\n\t\tif *configPath != \"\" {\n\t\t\tlog.Infof(\"Reading configuration from [%s]\", *configPath)\n\n\t\t\t// Check the actual path from the string\n\t\t\tif _, err := os.Stat(*configPath); !os.IsNotExist(err) {\n\t\t\t\tconfigFile, err := ioutil.ReadFile(*configPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\n\t\t\t\t// Read the controller from either a yaml or json format\n\t\t\t\terr = services.ParseControllerData(configFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t\t}\n\n\t\t\t} else {\n\t\t\t\tlog.Fatalf(\"Unable to open [%s]\", *configPath)\n\t\t\t}\n\t\t}\n\n\t\tif *services.Controller.EnableDHCP == false && *services.Controller.EnableHTTP == false && *services.Controller.EnableTFTP == false {\n\t\t\tlog.Warnln(\"All services are currently disabled\")\n\t\t}\n\n\t\t// If we've enabled DHCP, then we need to ensure a start address for the range is populated\n\t\tif *services.Controller.EnableDHCP && services.Controller.DHCPConfig.DHCPStartAddress == \"\" {\n\t\t\tlog.Fatalln(\"A DHCP Start address is required\")\n\t\t}\n\n\t\tif services.Controller.DHCPConfig.DHCPLeasePool == 0 {\n\t\t\tlog.Fatalln(\"At least one available lease is required\")\n\t\t}\n\n\t\tservices.Controller.StartServices(deployment)\n\n\t\t// Run the API server in a seperate go routine\n\t\tgo func() {\n\t\t\terr := apiserver.StartAPIServer(*apiServerPath, *port, *insecure)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\t\t}()\n\n\t\t// Register the packages to the apiserver\n\t\tservices.RegisterToAPIServer()\n\t\tparlay.RegisterToAPIServer()\n\n\t\t// Sit and wait for a control-C\n\t\tutils.WaitForCtrlC()\n\n\t\treturn\n\t},\n}\n"
  },
  {
    "path": "docs/actions.md",
    "content": "#  Actions\n\nWhen 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. \n\n\n\n## Built-in Actions\n\nAll 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.\n\nExample in json and yaml below:\n\n```json\n{\n  \"task\" : \"command\",\n  \"command\" : \"docker run image\",\n  \"name\" : \"Starts the docker image \\\"image\\\"\"\n}\n```\n\n```yaml\n- task: download\n  source: \"/home/user/my_archive.tar.gz\"\n  name: \"Retrieve the home archive\"\n```\n\n### Command\n\nThe **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. \n\nSet 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.\n\n```yaml\n- task: command\n  command: \"sleep 100\"\n  timeout: 99\n  ignoreFail: true\n```\n\n *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*\n\nIf a command requires elevated privileges, the `commandSudo` option allows executing a command as different user, with it's entitled privileges. \n\n**Note**: This requires `NOPASSWD` to be set for the current user.\n\n```yaml\n{\n  \"task\" : \"command\",\n  \"command\" : \"cat /dev/null > /var/log/messages\",\n  \"name\" : \"Concatenate the messages file to clear space\",\n  \"commandSudo\" : \"root\"\n}\n```\n\n#### Using commands between actions deployments\n\nThere 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.\n\nThere are two options to save the output of a command: \n\n- `commandSaveFile` - saves the command output to a path\n- `commandSaveAsKey` - Saves the ouput in-memory under a specified `key`\n\nThese saved ouputs can then be used later through the use of the `key` options:\n\n- `KeyFile` - executes the commands in the file specified under the `path`\n\n- `KeyName` - executes the commands saved in-memory under the specified `key`\n\n  \n\nThe below example will create a command Key under the name `joinKey` (JSON format) :\n\n```json\n{\n  \"name\" : \"Generate a join token\",\n  \"type\" : \"command\",\n  \"command\" : \"kubeadm token create --print-join-command 2>/dev/null\",\n  \"commandSaveAsKey\" : \"joinKey\" \n}\n```\n\n\n\nThis key can now be used in a different deployment with different hosts (YAML format):\n\n```yaml\n- type: \"command\"\n  name: \"Join worker to Kubernetes cluster\"\n  keyName: \"joinKey\"\n  commandSudo : \"root\"\n```\n\n\n\n#### Piping data between commands\n\nIn 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.\n\n\n\nThe 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.\n\n```yaml\n  - type: command\n    command: \"tee /etc/apt/sources.list.d/kubernetes.list\"\n    commandPipeCmd: echo \"deb https://apt.kubernetes.io/ kubernetes-xenial main\"\n    name: Set Kubernetes Repository\n    commandSudo: root\n\n```\n\nThis is useful for a variety of usecases, although it has been found very useful for appending data to existing files that require elevated privilieges. \n\n**Example reasons for piping data to a command**\n\nThe 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.\n\n### Upload / Download of files\n\nBoth of the command types `upload` and `download` have the same set of options:\n\n- `destination` - Where the file will be once the `upload`/`download` has completed\n- `source` - The file that will be either `uploaded`/`downloaded`\n- `name` - Details what the action will be doing\n\n```yaml\n  - type: download\n    destination: ./ubuntu.tar.gz\n    name: Retrieve local copy of ubuntu.tar.gz\n    source: ./ubuntu.tar.gz\n```\n\n\n\n### Plugins\n\nPlugins 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:\n\n```yaml\n  - name: Push kubernetes images for managers\n    plugin:\n      imageName:\n      - k8s.gcr.io/kube-apiserver:v1.14.0\n      - k8s.gcr.io/kube-controller-manager:v1.14.0\n      - k8s.gcr.io/kube-scheduler:v1.14.0\n      localSudo: true\n      remoteSudo: true\n    type: docker/image\n```\n\nThe main differences are:\n\n- `plugin` - Contains all of the specifics that will be passed to the plugin logic\n- `type` - Should be the action defined by the plugin itself.\n\n\n\n\n\n## Additonal configuration\n\n### No Password sudo\n\nTo enable password-less sudo the `/etc/sudoers` file needs modifying (DO NOT DO THIS MANUALLY).\n\nTo edit the sudo file use the following command:\n\n```\nsudo visudo\n```\n\nThen add the following entry to the end of the file, replacing the `username` with the correct entry :\n\n```\nusername     ALL=(ALL) NOPASSWD:ALL\n```\n\nThis can be tested by either opening a new session or logging out and back in and then testing that `sudo <CMD>` doesn't require a password.\n"
  },
  {
    "path": "docs/application_architecture.md",
    "content": "# Application Architecture\n\nThe 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)\n\n## Application Server routine\n\nWhen 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. \n\n### Starting the server (HTTP Enabled)\n\nWe 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.\n\n`plunder server --config ./config.json --deployment ./deployment.json`\n\n1. Plunder starts\n   - parses flags\n   - parses global `config.json` \n2. Plunder will start services enabled in the configuration `controller.StartServices(deployment)` (`cmd\\server.go`)\n3. If a deployment file is passed then it should be parsed `err := UpdateConfiguration(deployment)` (`pkg\\server\\services.go`)\n   - The parsing of this will generate strings that are mapped to urls that are tracked in a map `httpPaths`\n   - The function `UpdateConfiguration(configFile []byte)` (`pkg/server/generator.go`) will generate these in memory by iterating through the file and checking the deployment type.\n4. HTTP Server is started with `err := c.serveHTTP()`(`pkg\\server\\services.go`)\n5. This function will create a number of prebuilt PXE boot strings using the kernels etc. from `config.json`, configurations such as `/preboot.ipxe` etc.\n6. In the event that new configuration is passed to the server then steps 3 are ran again.\n\n### Client connections\n\n1. A Host starts and proceeds to PXE boot, by doing a DHCP request.\n2. 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)\n3. This is passed of TFTP to the booting host which will start iPXE and re-do a DHCP request\n4. This time however the DHCP client will have the option `77` set to `iPXE` which means that it's ready for provisioning. \n5. The DHCP server will look for an existing configuration `deploymentType := FindDeployment(mac)` (`pkg/server/serve_dhcp.go`), which should return `preseed` etc.\n6. 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\n7. If there exists a pre-defined configuration then it will set the DHCP option to that.\n"
  },
  {
    "path": "docs/deployment.md",
    "content": "# Deployment Configuration\n\n## Generating a configuration\nA `./plunder config deployment > deployment.json` will create a blank deployment configuration that can be pre-populated in order to create specific deployments.\n\nA configured deployment should resemble something like the example below:\n\n```json\n{\n        \"globalConfig\": {\n                \"adapter\": \"ens192\",\n                \"gateway\": \"192.168.0.1\",\n                \"subnet\": \"255.255.255.0\",\n                \"nameserver\": \"192.168.0.1\",\n                \"ntpserver\": \"192.168.0.1\",\n                \"username\": \"user\",\n                \"password\": \"pass\",\n                \"repoaddress\": \"192.168.0.1\",\n                \"mirrordir\": \"/ubuntu\",\n                \"sshkeypath\": \"/home/deploy/.ssh/id_pub.rsa\",\n                \"sshkey\": \"ssh-rsa AABBCCDDEE1122334455\",\n                \"packages\": \"nginx openssh-server\"\n        },\n        \"deployments\": [\n                {\n                        \"mac\": \"00:11:22:33:44:55\",\n                        \"bootConfigName\": \"default\",\n                        \"bootConfig\": {\n                                \"configName\": \"\",\n                                \"kernelPath\": \"\",\n                                \"initrdPath\": \"\",\n                                \"cmdline\": \"\"\n                        },\n                        \"config\": {\n                                \"address\": \"192.168.0.2\",\n                                \"hostname\": \"Server01\"\n                        }\n                }\n        ]\n}\n```\n\n## Configuration overview\n\nThe *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. \n\nPlacing the same information into an actual deployment will **override** the configuration inherited from the `globalConfig`.\n\n### Shared Configuration overview\n\n- `gateway` - The gateway a server will be configured to use as default router\n- `subnet` - The network range server will be configured to use\n- `nameserver` - DNS server to resolve hostnames\n- `ntpserver` - The address of a timeserver\n- `adapter` - Which specific adapter will be configured\n- `swapEnabled` - Build the Operating system without swap being created\n- `username` - A default user that will be created\n- `password` - A password for the above user\n- `repoaddress` - The hostname/ip address of the server where the OS packages reside\n- `sshkeypath` - The path to an ssh key that will be added to the image for authenticating\n\n\n\n### Deployment specific\n\n- `address` - A unique network address that will be added to the server\n- `hostname` - A unique hostname to be added to the provisioned server\n\n\n\nAs mentioned above, a lot of fields can be ignored and the entry from the `globalConfig` will be used.\n\n\n\n### Deployments\n\nThe deployment contains things that will make a server unique!\n\n- `mac` - The unqique HW mac address of a server to configure\n\n- `kernelPath` - If a specific kernel should be used (for things like LinuxKit)\n\n- `initrdPath` - If a specific init ramdisk should be used\n\n- `cmdline` - Any arguments that should be passed to the kernel ramdisk\n\n  \n\nThe `deployment` specifies how the server will be provisioned, there are three options:\n\n- `preseed` Ubuntu/Debian pressed deployment\n- `kickstart` CentOS/RHEL deployment\n- `reboot` This is for servers that need to be kept on a reboot loop.\n\n\n\nThe remaining `config` allows updates or overrides to the global confgiguration detailed above.\n\n \n\n### Online updates of deployment configuration\nThe webserver exposes a `/deployment` end point that can be used to provide an online update of the configuration, this has the following benefits:\n\n- Allows automation of updates, through things like an API call\n- 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\n\n*Retrieve the existing configuration*\n\nThe currently active configuration can be retrieved through a simple get on the `/deployment` endpoint \n\ne.g.\n\n`curl -vX <IP ADDRESS>/deployment`\n\n*Updating the configuration*\n\nThe configuration can be updated by `POST`ing the configuration JSON to the same URL.\n\ne.g.\n\n`curl -vX POST deploy01/deployment -d @deployment.json --header \"Content-Type: application/json\"`\n\n## Usage\n\nWith configuration for both the services and the deployments completed, they can both be passed to `plunder` in order for servers to be built.\n\nAs shown below:\n\n```\nsudo ./plunder server --config ./config.json --deployment ./deployment.json --logLevel 5\n[sudo] password for dan: \nINFO[0000] Reading configuration from [./config.json]   \nINFO[0000] Starting Remote Boot Services, press CTRL + c to stop \nDEBU[0000] \nServer IP:\t192.168.1.1\nAdapter:\tens192\nStart Address:\t192.168.1.2\nPool Size:\t100\n \nINFO[0000] RemoteBoot => Starting DHCP                  \nINFO[0000] RemoteBoot => Starting TFTP                  \nDEBU[0000] \nServer IP:\t192.168.1.1\nPXEFile:\tundionly.kpxe\n \nINFO[0000] Opening and caching undionly.kpxe            \nINFO[0000] RemoteBoot => Starting HTTP                  \nINFO[0286] DCHP Message: Discover   \n```\n\n## Next Steps\nServers 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. \n\nFor provisioning applications or a platform details are [here](./provisioning.md).\n"
  },
  {
    "path": "docs/example_architecture.md",
    "content": "# Example architecture\n\nThis 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.\n\n## Infrastructure design\n\nIn 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. \n\nThe 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. \n\n![](../image/simple_architecture.jpg)\n\n## Services Overview\n\nThe services that plunder can expose will bind to the existing operating system in two ways. \n\n#### DHCP\n\nThis will ultimately bind to an adapter, and this adapter should be configured with an address.\n\n#### TFTP \n\nThis will ultimately bind to an IP address.\n\n#### HTTP\n\nThis will also bind to an IP address.\n\n## Example Plunder usage\n\nThe 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.\n\n```\nsudo ./plunder server         \\\n--adapter ens192              \\\n--enableDHCP                  \\\n--enableTFTP                  \\\n--enableHTTP                  \\\n--initrd initrd.gz\t     \t\t  \\\n--kernel kernel            \t  \\\n--cmdline \"console=tty0\" \t\t  \\\n--addressDHCP 192.168.1.1     \\\n--startAddress 192.168.1.130  \\\n--addressTFTP 192.1.1.1 \t\t  \\\n--addressHTTP 192.168.1.1 \t\t\\\n--anyboot  \n```\n\nTo understand the CLI line above, we will break it down into what some of the more hard-to-understand flags actually are doing.\n\n- `--adapter <...>` This specified which adapter DHCP will broadcast from\n- `--enableXXXX` Enable a specific service, in most cases all will be needed unless existing services already exist.\n- `--addressDHCP <x.x.x.x>` This is the address that should be configured on the adapter that you're binding too.\n- `--addressTFTP/HTTP` This can either be the same address as above or an address of an existing service\n- `--startAddress <x.x.x.x>` This is the beginning on the advertised DHCP addresses.\n\n**Note** `sudo` has to be used as binding to an adapter and ports <1024 requires root privileges. \n\n\n## Example Plunder usage with Linuxkit\n\nIf 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.   \n\n```\nsudo ./plunder server\t\t                     \\\n--adapter ens192\t\t\t\t                     \\\n--enableDHCP \t\t\t                \t      \t \\\n--enableTFTP \t\t\t\t\t                       \\\n--enableHTTP \t\t\t\t                         \\ \n--initrd linuxkit/linuxkit-initrd.img        \\\n--kernel linuxkit/linuxkit-kernel            \\\n--cmdline $(cat ./linuxkit/linuxkit-cmdline) \\\n--addressDHCP 192.168.1.1 \t\t               \\\n--startAddress 192.168.1.130 \t               \\\n--addressTFTP 192.1.1.1 \t\t                 \\\n--addressHTTP 192.168.1.1 \t\t               \\\n--anyboot  \n```\n"
  },
  {
    "path": "docs/example_deployment.md",
    "content": "# Example Deployment for off-line Kubernetes\n\n**This example will make use of Plunders User Interface**\n\nIn 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. \n\n## Offline Calico parts\n\n### Download the manifests\n\n```\nwget https://docs.projectcalico.org/v3.5/getting-started/kubernetes/installation/hosted/etcd.yaml\n```\nand\n\n```\nwget https://docs.projectcalico.org/v3.5/getting-started/kubernetes/installation/hosted/calico.yaml\n```\n\n### Download the named images\n\nOne Liner to pull the calico images and etcd image\n\n``` \nfor image in $(cat etcd.yaml | grep image | awk '{ print $2 }') ; do sudo docker pull $image; done\n```\n``` \nfor image in $(cat calico.yaml | grep image | awk '{ print $2 }') ; do sudo docker pull $image; done\n```\n\nAt this point you'll have the images as part of the local docker repository and the two manifests in the local directory.\n\n## Offline Ubuntu packages\n\nOne liner to get the packages needed for the kubernetes hosts to run `kubelet`\n\n```\napt-get download socat ethtool ebtables; tar -cvzf ubuntu_pkg.tar.gz socat* ethtool* ebtables*; rm socat* ethtool* ebtables*\n```\n\nThis command will download everything needed into an archive named `ubuntu_pkg.tar.gz`\n\n## Offline Docker packages \n\nOne 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. \n\n```\napt-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\n```\n\n## Offline Kubernetes packages\n\nOne 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. \n\n```\napt-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*\n```\n\n## Offline Kubernetes images\n\nThe 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.\n\n`kubeadm config images list` - will list all images\n\n`kubeadm config images pull` - will pull them all to the local host\n\nOnce 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. \n\n## Example deployment map\n\nThere is an example deployment map as a `gist` available [https://gist.github.com/thebsdbox/f12b621a9d3943128b6bb16688497cd0](https://gist.github.com/thebsdbox/f12b621a9d3943128b6bb16688497cd0)\n\n## Deployment in action\n\n[![asciicast](https://asciinema.org/a/reh3reEgJQKCOB5e92D96l6tt.png)](https://asciinema.org/a/reh3reEgJQKCOB5e92D96l6tt)\n\n"
  },
  {
    "path": "docs/provisioning.md",
    "content": "# Provisioning Configuration\n\nThe 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.\n\nA Deployment map can contain multiple **deployments**, which in turn will contain one or more **actions** that will be performed on one or more **hosts**.\n\nAlso 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).\n\n### Example deployment map\n\nThis script below (for offline installations) will upload a tarball containing the docker packages and then install them on all remote systems listed under `hosts`.\n\n**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`\n\n#### JSON Example\n\n```json\n{\n\t\"deployments\": [\n\t\t{\n\t\t\t\"name\": \"Upload Docker Packages\",\n\t\t\t\"parallel\": false,\n\t\t\t\"sessions\": 0,\n\t\t\t\"hosts\": [\n\t\t\t\t\"192.168.1.3\",\n\t\t\t\t\"192.168.1.4\",\n\t\t\t\t\"192.168.1.5\"\n\t\t\t],\n\t\t\t\"actions\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Upload Docker Packages\",\n\t\t\t\t\t\"type\": \"upload\",\n\t\t\t\t\t\"source\": \"./docker_pkg.tar.gz\",\n\t\t\t\t\t\"destination\": \"/tmp/docker_pkg.tar.gz\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Extract Docker packages\",\n\t\t\t\t\t\"type\": \"command\",\n\t\t\t\t\t\"command\": \"tar -C /tmp -xvzf /tmp/docker_pkg.tar.gz\"\n\t\t\t\t},\n        {\n\t\t\t\t\t\"name\": \"Install Docker packages\",\n\t\t\t\t\t\"type\": \"command\",\n\t\t\t\t\t\"command\": \"dpkg -i /tmp/docker/*\",\n\t\t\t\t\t\"commandSudo\": \"root\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}\n                                \n```\n#### YAML Example\n\n```yaml\ndeployments:\n- actions:\n  - destination: /tmp/docker_pkg.tar.gz\n    name: Upload Docker Packages\n    source: ./docker_pkg.tar.gz\n    timeout: 0\n    type: upload\n  - command: tar -C /tmp -xvzf /tmp/docker_pkg.tar.gz\n    name: Extract Docker packages\n    timeout: 0\n    type: command\n  - command: dpkg -i /tmp/docker/*\n    commandSudo: root\n    name: Install Docker packages\n    timeout: 0\n    type: command\n  hosts:\n  - 192.168.1.3\n  - 192.168.1.4\n  - 192.168.1.5\n  name: Upload Docker Packages\n  parallel: false\n  parallelSessions: 0\n```\n\nThe above example only covers simple usage of `uploading` and `command` usages.\n\n\n\n## Usage\n\nWhen automating a deployment ssh credentials are required to map a host with the correct credentials. \n\nTo simplify this `plunder` can make use of:\n\n- A `deployment` file as detailed [here](./deployment.md), which parlay will extract the `ssh` information from to allow authentication\n- 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. \n\n**Example**\n\nUsing a map to deploy wordpress (`wordpress.yaml`) and a local deployment file.\n\n`plunder automate ssh --map ./wordpress.yaml --deployconfig ./deployment.json`\n\nUsing a map to deploy wordpress (`wordpress.yaml`) and a deployment endpoint.\n\n`plunder automate ssh --map ./wordpress.yaml --deployendpoint http://localhost`\n\nIt 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.\n\n`/plunder automate --map ./stackedmanager.yaml --overrideAddress 192.168.1.105`\n\nUnder 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.\n\nAdditional Flags:\n\n- The `--deployment` flag now will point to a specific deployment in a map\n- The `--action` flag can be used to point to a specific action in a deployment\n- The `--host` flag will point to a specific host in the deployment\n- The `--resume` will determine if to continue executing all remaining actions\n\n### User Interface\n\nPlunder 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.\n\n**Example**\n\n```\nplunder automate ui --map ./stackedmanager.yaml --deployendpoint http://localhost \nINFO[0000] Reading deployment configuration from [./stackedmanager.yaml] \n? Select deployment(s)  [Use arrows to move, type to filter]\n> [ ]  Reset any Kubernetes configuration (and remove packages)\n  [ ]  Configure host OS for kubernetes nodes\n  [ ]  Deploy Kubernetes Images for 1.14.0\n  [ ]  Initialise Kubernetes Master (1.14)\n  [ ]  Deploy Calico (3.6)\n```\n\nThe UI also provides additional capability to create new maps based upon selected deployments and actions, and also to convert between formats. \n\n- `--json` Print the JSON to stdout, no execution of commands\n- `--yaml` Print the YAML to stdout, no execution of commands\n\n\n**Execution of a map is shown in the screen shot below**\n\n![](../image/parlay.jpg)\n*The above example uses screen, where the output from `plunder` is on the top and `tail -f output` is below*\n\n\n"
  },
  {
    "path": "docs/readme.md",
    "content": "# Plunder Usage\n\nWhen using `plunder` there are a few things that you will need to ensure that a configuration exists for, these things are:\n\n- Service configuration (IP Addresses, adapter names, service enablement)\n- Deployment configuration (MAC Addresses, Package management, networking)\n- Provisioning configuration (File transfer, remote command execution)\n\nMost of the configuration required will be automatically generated by `plunder` through the use of the `plunder config` sub command. \n\nTo view an example architecture and quick usage than look [here](./example_architecture.md).\n\n### Service\nThe services such as DHCP and TFTP etc.. are the basic requirement in order to bootstrap a **blank** bare-metal server or new virtual machine. \n\nService configuration overview and usage is located [here](./service.md).\n\n### Deployment\nOnce a **blank** server boots it will need an Operating System (+ packages) installing, along with setting up networking and credentials. \n\nDeployment configuration overview and usage is located [here](./deployment.md).\n\n### Provisioning\nOnce 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\n\nA Provisioning overview with usage is located [here](./provisioning.md)."
  },
  {
    "path": "docs/service.md",
    "content": "# Service Configuration\n\n## Generating a configuration\n\nA `./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. \n\n### Modifying the configuration\n\n```json\n{\n        \"adapter\": \"en0\",\n        \"enableDHCP\": false,\n        \"dhcpConfig\": {\n                \"addressDHCP\": \"192.168.0.142\",\n                \"startDHCP\": \"192.168.0.143\",\n                \"leasePoolDHCP\": 20,\n                \"gatewayDHCP\": \"192.168.0.142\",\n                \"nameserverDHCP\": \"192.168.0.142\"\n        },\n        \"enableTFTP\": false,\n        \"addressTFTP\": \"192.168.0.142\",\n        \"enableHTTP\": false,\n        \"addressHTTP\": \"192.168.0.142\",\n        \"pxePath\": \"undionly.kpxe\",\n        \"bootConfigs\": [\n                {\n                        \"configName\": \"default\",\n                        \"kernelPath\": \"/kernelPath\",\n                        \"initrdPath\": \"/initPath\",\n                        \"cmdline\": \"cmd=options\",\n                        \"isoPrefix\": \"ubuntu\",\n                        \"isoPath\": \"/path/to/iso\"\n                }\n        ]\n}\n```\n\n*Example generated configuration above*\n\n### Sections\n\nBy **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. \n\n#### Services\n\nThe `enable<service>` will ensure that a particular functionality is enabled within Plunder.\n\nThe `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.\n\n#### DHCP\n\n\nThe `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.\n\n#### Boot Configurations\n\nThe boot configurations are an array of configurations that define various remote booting configurations and are referenced via the `configName`.\n\nThe `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.\n\nFinally, 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. \n\ne.g.\n\n`plunderAddress/isoPrefix/path/to/file`\n\n#### Additional\n\nThe `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. \n\n## Usage\nAt 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)."
  },
  {
    "path": "go.mod",
    "content": "module github.com/plunder-app/plunder\n\ngo 1.12\n\nrequire (\n\tgithub.com/AlecAivazis/survey/v2 v2.0.7 // indirect\n\tgithub.com/c4milo/gotoolkit v0.0.0-20190525173301-67483a18c17a // indirect\n\tgithub.com/ghodss/yaml v1.0.0\n\tgithub.com/gorilla/mux v1.7.4 // indirect\n\tgithub.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214 // indirect\n\tgithub.com/hooklift/iso9660 v1.0.0 // indirect\n\tgithub.com/kr/pty v1.1.8 // indirect\n\tgithub.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 // indirect\n\tgithub.com/mattn/go-colorable v0.1.6 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pkg/sftp v1.11.0 // indirect\n\tgithub.com/plunder-app/BOOTy v0.0.0-20200513203223-f43f6ea742c4\n\tgithub.com/plunder-app/plunder/pkg/apiserver v0.0.0-20200514155151-dfdcaab2e5cd\n\tgithub.com/plunder-app/plunder/pkg/certs v0.0.0-20200514155151-dfdcaab2e5cd\n\tgithub.com/plunder-app/plunder/pkg/parlay v0.0.0-20200514155151-dfdcaab2e5cd\n\tgithub.com/plunder-app/plunder/pkg/parlay/parlaytypes v0.0.0-20200514155151-dfdcaab2e5cd\n\tgithub.com/plunder-app/plunder/pkg/plunderlogging v0.0.0-20200514155151-dfdcaab2e5cd // indirect\n\tgithub.com/plunder-app/plunder/pkg/services v0.0.0-20200514155151-dfdcaab2e5cd\n\tgithub.com/plunder-app/plunder/pkg/ssh v0.0.0-20200514155151-dfdcaab2e5cd\n\tgithub.com/plunder-app/plunder/pkg/utils v0.0.0-20200514155151-dfdcaab2e5cd\n\tgithub.com/sirupsen/logrus v1.6.0\n\tgithub.com/spf13/cobra v1.0.0\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/thebsdbox/go-tftp v0.0.0-20190329154032-a7263f18c49c // indirect\n\tgithub.com/whyrusleeping/go-tftp v0.0.0-20180830013254-3695fa5761ee // indirect\n\tgolang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 // indirect\n\tgolang.org/x/net v0.0.0-20200513185701-a91f0712d120 // indirect\n\tgolang.org/x/text v0.3.2 // indirect\n)\n\nreplace (\n\tgithub.com/plunder-app/plunder/pkg/apiserver => ./pkg/apiserver\n\tgithub.com/plunder-app/plunder/pkg/certs => ./pkg/certs\n\tgithub.com/plunder-app/plunder/pkg/parlay => ./pkg/parlay\n\tgithub.com/plunder-app/plunder/pkg/services => ./pkg/services\n\tgithub.com/plunder-app/plunder/pkg/ssh => ./pkg/ssh\n\tgithub.com/plunder-app/plunder/pkg/utils => ./pkg/utils\ngithub.com/plunder-app/BOOTy => ../../plunder-app/BOOTy\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ngithub.com/AlecAivazis/survey/v2 v2.0.7 h1:+f825XHLse/hWd2tE/V5df04WFGimk34Eyg/z35w/rc=\ngithub.com/AlecAivazis/survey/v2 v2.0.7/go.mod h1:mlizQTaPjnR4jcpwRSaSlkbsRfYFEyKgLQvYTzxxiHA=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=\ngithub.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/c4milo/gotoolkit v0.0.0-20190525173301-67483a18c17a h1:+uvtaGSLJh0YpLLHCQ9F+UVGy4UOS542hsjj8wBjvH0=\ngithub.com/c4milo/gotoolkit v0.0.0-20190525173301-67483a18c17a/go.mod h1:txokOny9wavBtq2PWuHmj1P+eFwpCsj+gQeNNANChfU=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=\ngithub.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=\ngithub.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=\ngithub.com/digineo/go-dhclient v1.0.2/go.mod h1:DPvyqGEW8irJvp2lrnGfQWpjj6VidXX9STLBTfNing4=\ngithub.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=\ngithub.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=\ngithub.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=\ngithub.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=\ngithub.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214 h1:WgfvpuKg42WVLkxNwzfFraXkTXPK36bMqXvMFN67clI=\ngithub.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214/go.mod h1:kj6hFWqfwSjFjLnYW5PK1DoxZ4O0uapwHRmd9jhln4E=\ngithub.com/hooklift/iso9660 v1.0.0 h1:GYN0ejrqTl1qtB+g+ics7xxWHp7J2B1zmr25O9EyG3c=\ngithub.com/hooklift/iso9660 v1.0.0/go.mod h1:sOC47ru8lB0DlU0EZ7BJ0KCP5rDqOvx0c/5K5ADm8H0=\ngithub.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=\ngithub.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=\ngithub.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 h1:t2c2B9g1ZVhMYduqmANSEGVD3/1WlsrEYNPtVoFlENk=\ngithub.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=\ngithub.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/mdlayher/raw v0.0.0-20191004140158-e1402808046b/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=\ngithub.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.11.0 h1:4Zv0OGbpkg4yNuUtH0s8rvoYxRCNyT29NVUo6pgPmxI=\ngithub.com/pkg/sftp v1.11.0/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=\ngithub.com/plunder-app/BOOTy v0.0.0-20200513203223-f43f6ea742c4 h1:XBPLmj1YuM8GlnueA/IMUKQDVVKP7E7NOjx1SlAd/g0=\ngithub.com/plunder-app/BOOTy v0.0.0-20200513203223-f43f6ea742c4/go.mod h1:ItncOB62Q244Txg1v6/S3XZq3KB+9uHcmmZhJF+5Z+A=\ngithub.com/plunder-app/plunder/pkg/parlay v0.0.0-20200513203243-eccb418a5255 h1:FBXZMKgQ+YD3i18EZlSlnSFyhyHE654d+woetczrsC8=\ngithub.com/plunder-app/plunder/pkg/parlay v0.0.0-20200513203243-eccb418a5255/go.mod h1:5UuNaULcTUSFIkrbe+NA/ufh9iyUEtsKqmjlbl88AVw=\ngithub.com/plunder-app/plunder/pkg/parlay/parlaytypes v0.0.0-20200513203243-eccb418a5255 h1:765Djvc0TdpwZFlEmyq1ruUPx69klzHQOMAMCK1KJZM=\ngithub.com/plunder-app/plunder/pkg/parlay/parlaytypes v0.0.0-20200513203243-eccb418a5255/go.mod h1:QtxXmGRkwdtiiH03oveOPcYXucOv/FxJq2a170aXkxg=\ngithub.com/plunder-app/plunder/pkg/parlay/parlaytypes v0.0.0-20200514154439-933b7120d270 h1:p59jcYFaRO6LOJz/OAcdPNSlbgl1QPH4cmlFs69BZ6g=\ngithub.com/plunder-app/plunder/pkg/parlay/parlaytypes v0.0.0-20200514154439-933b7120d270/go.mod h1:QtxXmGRkwdtiiH03oveOPcYXucOv/FxJq2a170aXkxg=\ngithub.com/plunder-app/plunder/pkg/parlay/parlaytypes v0.0.0-20200514155151-dfdcaab2e5cd h1:CA9lXqJAJxwA/NGepdfE/hQapGplV4CNNal3eZ9C/k0=\ngithub.com/plunder-app/plunder/pkg/parlay/parlaytypes v0.0.0-20200514155151-dfdcaab2e5cd/go.mod h1:QtxXmGRkwdtiiH03oveOPcYXucOv/FxJq2a170aXkxg=\ngithub.com/plunder-app/plunder/pkg/plunderlogging v0.0.0-20200513203243-eccb418a5255 h1:gzlti8QQwa02qAGlFktceQucT4HZUZFWIm6h4b03JBo=\ngithub.com/plunder-app/plunder/pkg/plunderlogging v0.0.0-20200513203243-eccb418a5255/go.mod h1:ketI5Vxh8nmxwiWnS644ZaEwVt9M/bpP6ISaFeWpcS0=\ngithub.com/plunder-app/plunder/pkg/plunderlogging v0.0.0-20200514154439-933b7120d270 h1:abUhCq54uc/4Q/ApJMGtWmsSGSLJcni5P8VXyrLZ49o=\ngithub.com/plunder-app/plunder/pkg/plunderlogging v0.0.0-20200514154439-933b7120d270/go.mod h1:ketI5Vxh8nmxwiWnS644ZaEwVt9M/bpP6ISaFeWpcS0=\ngithub.com/plunder-app/plunder/pkg/plunderlogging v0.0.0-20200514155151-dfdcaab2e5cd h1:luKDb4GvbZlnythTGjypGW8EdCyRSSXwUvbr+ZrB2zU=\ngithub.com/plunder-app/plunder/pkg/plunderlogging v0.0.0-20200514155151-dfdcaab2e5cd/go.mod h1:ketI5Vxh8nmxwiWnS644ZaEwVt9M/bpP6ISaFeWpcS0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=\ngithub.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/thebsdbox/go-tftp v0.0.0-20190329154032-a7263f18c49c h1:cYCrFUo78/407dxOlYw2g4pdm4Ly8RSPedsYB+z7h1s=\ngithub.com/thebsdbox/go-tftp v0.0.0-20190329154032-a7263f18c49c/go.mod h1:yXG6GIu/ptjkk0fd++y96R2cahlvxZr4LhMdf0j/L2Q=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=\ngithub.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=\ngithub.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=\ngithub.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=\ngithub.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=\ngithub.com/whyrusleeping/go-tftp v0.0.0-20180830013254-3695fa5761ee h1:P2Wwq5QukiLY/I6+mc7NyLFX/atHAj6pGwiVu6fld98=\ngithub.com/whyrusleeping/go-tftp v0.0.0-20180830013254-3695fa5761ee/go.mod h1:ZemSN4DPuG1ppDttxnu45zl8BenKT9xSjMyapUd+Dd0=\ngithub.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=\ngithub.com/zcalusic/sysinfo v0.0.0-20200228145645-a159d7cc708b/go.mod h1:WGLNaWsjKQ2gXmAHh+MQztgu3FLFAnOFJjFzhpgShCY=\ngo.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=\ngolang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20191003171128-d98b1b443823 h1:Ypyv6BNJh07T1pUSrehkLemqPKXhus2MkfktJ91kRh4=\ngolang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 h1:R4dVlxdmKenVdMRS/tTspEpSTRWINYrHD8ySIU9yCIU=\ngolang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200513112337-417ce2331b5c h1:kISX68E8gSkNYAFRFiDU8rl5RIn1sJYKYb/r2vMLDrU=\ngolang.org/x/sys v0.0.0-20200513112337-417ce2331b5c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\n"
  },
  {
    "path": "hack/comboot_ipxe/Dockerfile",
    "content": "FROM gcc:latest AS IPXE_BUILD\nRUN git clone git://git.ipxe.org/ipxe.git\nRUN sed -i '/COMBOOT/s/\\/\\///g' ipxe/src/config/general.h\nWORKDIR /ipxe/src/\nRUN make bin/undionly.kpxe \n\nFROM scratch\nCOPY --from=IPXE_BUILD /ipxe/src/bin/undionly.kpxe .\n"
  },
  {
    "path": "hack/comboot_ipxe/gen_comboot.ipxe",
    "content": "#!/bin/bash\necho Building latest container for iPXE, with comboot support\ncd comboot_ipxe\ndocker build -t ipxe_comboot .\ndocker run -it -v $(echo $PWD):/tmp/ipxe ipxe_comboot  /bin/sh -c \"cp undionly.kpxe /tmp/ipxe\"\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport \"github.com/plunder-app/plunder/cmd\"\n\n// Version is populated from the Makefile and is tied to the release TAG\nvar Version string\n\n// Build is the last GIT commit\nvar Build string\n\nfunc main() {\n\tcmd.Release.Version = Version\n\tcmd.Release.Build = Build\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "pkg/apiserver/README.md",
    "content": "# API Server documentation\n\nThis 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. \n\n## Using the API Server\n\nThe 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.\n\n### Starting the API Server\n\nThe below example will start the API server on a custom port.\n\n```\nplunder server -p 12345\n```\n\n## Accessing the API Server\n\nThe 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). \n\n## Current issues\n\n### Server configuration\n\nCurrently DHCP can be stopped and started but logging output is buggy, HTTP/TFTP Can be started but can't be stopped or restarted.\n"
  },
  {
    "path": "pkg/apiserver/client.go",
    "content": "package apiserver\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/url\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n//FindFunctionEndpoint - will do a look up to find an exposed dynamic endpoint\nfunc FindFunctionEndpoint(u *url.URL, c *http.Client, f, m string) (*EndPoint, *Response) {\n\t// Create local URL for the API call\n\tnewURL := *u\n\tnewURL.Path = fmt.Sprintf(\"%s/%s/%s\", FunctionPath(), f, m)\n\n\t// Interact with the API server to find the endpoint\n\tresponse, err := ParsePlunderGet(&newURL, c)\n\tif err != nil {\n\n\t\treturn nil, &Response{\n\t\t\tWarning: fmt.Sprintf(\"Unable to find method [%s] for function [%s]\", m, f),\n\t\t\tError:   err.Error(),\n\t\t}\n\t}\n\tvar ep EndPoint\n\terr = json.Unmarshal(response.Payload, &ep)\n\tif err != nil {\n\t\tresponse.Error = err.Error()\n\t\treturn nil, response\n\t}\n\treturn &ep, response\n}\n\n//BuildEnvironmentFromConfig will use the apiserver pkg to parse a configuration file and create a http client with the correct authentication and URL\nfunc BuildEnvironmentFromConfig(path, urlFlag string) (*url.URL, *http.Client, error) {\n\tlog.Debugf(\"Parsing Configuration file [%s]\", path)\n\n\t// Open the configuration\n\tc, err := openClientConfig(path)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\t// Retrieve the certificate\n\tcert, err := c.RetrieveClientCert()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Build the certificate pool from the unencrypted cert\n\tcaCertPool := x509.NewCertPool()\n\tcaCertPool.AppendCertsFromPEM(cert)\n\n\t// Create a HTTPS client and supply the created CA pool\n\tclient := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\tRootCAs: caCertPool,\n\t\t\t},\n\t\t},\n\t}\n\n\t// Build the URL from the configuration\n\tserverURL := c.GetServerAddressURL()\n\n\t// Overwrite the configuration url if\n\tif urlFlag != \"\" {\n\t\tserverURL, err = url.Parse(urlFlag)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\n\treturn serverURL, client, nil\n}\n\n//ParsePlunderGet will attempt to retrieve data from the plunder API server\nfunc ParsePlunderGet(u *url.URL, c *http.Client) (*Response, error) {\n\tvar response Response\n\n\tlog.Debugf(\"Querying the Plunder Server [%s]\", u.String())\n\n\tresp, err := c.Get(u.String())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer resp.Body.Close()\n\tbody, err := ioutil.ReadAll(resp.Body)\n\n\tif resp.StatusCode > 200 {\n\t\treturn nil, fmt.Errorf(resp.Status)\n\t}\n\n\terr = json.Unmarshal(body, &response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &response, nil\n}\n\n//ParsePlunderPatch will attempt to retrieve data from the plunder API server\nfunc ParsePlunderPatch(u *url.URL, c *http.Client, data []byte) (*Response, error) {\n\tvar response Response\n\n\tlog.Debugf(\"Posting [%d] bytes to the Plunder Server [%s]\", len(data), u.String())\n\n\treq, err := http.NewRequest(\"PATCH\", u.String(), bytes.NewBuffer(data))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := c.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer resp.Body.Close()\n\tbody, err := ioutil.ReadAll(resp.Body)\n\n\tif resp.StatusCode > 200 {\n\t\treturn nil, fmt.Errorf(resp.Status)\n\t}\n\n\terr = json.Unmarshal(body, &response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &response, nil\n\n}\n\n//ParsePlunderPost will attempt to retrieve data from the plunder API server\nfunc ParsePlunderPost(u *url.URL, c *http.Client, data []byte) (*Response, error) {\n\tvar response Response\n\n\tlog.Debugf(\"Posting [%d] bytes to the Plunder Server [%s]\", len(data), u.String())\n\n\tresp, err := c.Post(u.String(), \"application/json\", bytes.NewBuffer(data))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer resp.Body.Close()\n\tbody, err := ioutil.ReadAll(resp.Body)\n\n\tif resp.StatusCode > 200 {\n\t\treturn nil, fmt.Errorf(resp.Status)\n\t}\n\n\terr = json.Unmarshal(body, &response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &response, nil\n\n}\n\n//ParsePlunderDelete will attempt to retrieve data from the plunder API server\nfunc ParsePlunderDelete(u *url.URL, c *http.Client) (*Response, error) {\n\tvar response Response\n\n\tlog.Debugf(\"Requesting DELETE method to [%s]\", u.String())\n\n\t// Create request\n\treq, err := http.NewRequest(\"DELETE\", u.String(), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tresp, err := c.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer resp.Body.Close()\n\tbody, err := ioutil.ReadAll(resp.Body)\n\n\tif resp.StatusCode > 200 {\n\t\treturn nil, fmt.Errorf(resp.Status)\n\t}\n\n\terr = json.Unmarshal(body, &response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &response, nil\n\n}\n"
  },
  {
    "path": "pkg/apiserver/config.go",
    "content": "package apiserver\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/url\"\n\n\t\"github.com/ghodss/yaml\"\n)\n\n// ClientConfig is the structure of an expected configuration for pldctl\ntype ClientConfig struct {\n\tAddress    string `json:\"address,omitempty\"`\n\tPort       int    `json:\"port\"`\n\tClientCert string `json:\"cert\"`\n}\n\n// ServerConfig is the structure of an expected configuration for pldctl\ntype ServerConfig struct {\n\tClientConfig\n\tServerKey string `json:\"key\"`\n}\n\n//openClientConfig will open and parse a Plunder server configuration file\nfunc openClientConfig(path string) (*ClientConfig, error) {\n\tvar c ClientConfig\n\t// Create a CA certificate pool and add cert.pem to it\n\tb, err := ioutil.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjsonBytes, err := yaml.YAMLToJSON(b)\n\tif err == nil {\n\t\t// If there were no errors then the YAML => JSON was successful, no attempt to unmarshall\n\t\terr = json.Unmarshal(jsonBytes, &c)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"Unable to parse configuration as either yaml or json\")\n\t\t}\n\t} else {\n\t\t// Couldn't parse the yaml to JSON\n\t\t// Attempt to parse it as JSON\n\t\terr = json.Unmarshal(b, &c)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"Unable to parse configuration as either yaml or json\")\n\t\t}\n\t}\n\treturn &c, nil\n}\n\n//OpenServerConfig will open and parse a Plunder server configuration file\nfunc OpenServerConfig(path string) (*ServerConfig, error) {\n\tvar s ServerConfig\n\t// Create a CA certificate pool and add cert.pem to it\n\tb, err := ioutil.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjsonBytes, err := yaml.YAMLToJSON(b)\n\tif err == nil {\n\t\t// If there were no errors then the YAML => JSON was successful, no attempt to unmarshall\n\t\terr = json.Unmarshal(jsonBytes, &s)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"Unable to parse configuration as either yaml or json\")\n\t\t}\n\t} else {\n\t\t// Couldn't parse the yaml to JSON\n\t\t// Attempt to parse it as JSON\n\t\terr = json.Unmarshal(b, &s)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"Unable to parse configuration as either yaml or json\")\n\t\t}\n\t}\n\treturn &s, nil\n}\n\n// WriteServerConfig - will write out the server configuration for the API Server\nfunc WriteServerConfig(path, hostname, address string, port int, cert, key []byte) error {\n\tvar s ServerConfig\n\n\t// base64 the certificates\n\tencodedKey := base64.StdEncoding.EncodeToString(key)\n\tencodedCert := base64.StdEncoding.EncodeToString(cert)\n\n\t// Add the encoded certificates to the struct\n\ts.ClientCert = encodedCert\n\ts.ServerKey = encodedKey\n\n\t// Add the port for automated startup\n\ts.Port = port\n\n\t// Marshall to yaml\n\tb, err := yaml.Marshal(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = ioutil.WriteFile(path, b, 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// WriteClientConfig - will write out the server configuration for the API Server\nfunc WriteClientConfig(path, address string, s *ServerConfig) error {\n\tvar c ClientConfig\n\n\t// Add the encoded certificates to the struct\n\tc.ClientCert = s.ClientCert\n\n\t// Add the host information for automated startup\n\tc.Port = s.Port\n\tc.Address = address\n\n\t// Marshall client configuration to yaml\n\tb, err := yaml.Marshal(c)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = ioutil.WriteFile(path, b, 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n\n}\n\n//GetServerAddressURL will retrieve a parsed URL\nfunc (c *ClientConfig) GetServerAddressURL() *url.URL {\n\tvar plunderURL url.URL\n\tplunderURL.Scheme = \"https\"\n\t// Build a url\n\tplunderURL.Host = fmt.Sprintf(\"%s:%d\", c.Address, +c.Port)\n\treturn &plunderURL\n}\n\nfunc retrieveCert(cert string) ([]byte, error) {\n\treturn base64.StdEncoding.DecodeString(cert)\n}\n\n// RetrieveKey will decode the base64 certificate\nfunc (s *ServerConfig) RetrieveKey() ([]byte, error) {\n\treturn retrieveCert(s.ServerKey)\n}\n\n// RetrieveClientCert will decode the base64 certificate\nfunc (s *ServerConfig) RetrieveClientCert() ([]byte, error) {\n\treturn retrieveCert(s.ClientCert)\n}\n\n// RetrieveClientCert will decode the base64 certificate\nfunc (c *ClientConfig) RetrieveClientCert() ([]byte, error) {\n\treturn retrieveCert(c.ClientCert)\n}\n"
  },
  {
    "path": "pkg/apiserver/endpoints.go",
    "content": "package apiserver\n\nimport (\n\t\"net/http\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t//\"github.com/gorilla/mux\"\n)\n\n// EndPointManager - Contains all of the dynamically created endpoints\nvar EndPointManager []EndPoint\n\n// EndPoint is the source of truth for handling all of the endpoints exposed through the API Server\n// it also provides a mechanism to interact with the apiserver to find/create api endpoints\ntype EndPoint struct {\n\tName         string `json:\"name\"`\n\tPath         string `json:\"path\"`\n\tFunctionPath string `json:\"functionEndpoint\"`\n\tDescription  string `json:\"description\"`\n\tMethod       string `json:\"method\"`\n}\n\n// AddDynamicEndpoint - will add an endpoint to the api server and link it back to a function\nfunc AddDynamicEndpoint(endpointPattern, path, description, name, method string, epFunc http.HandlerFunc) {\n\tfor i := range EndPointManager {\n\t\tif EndPointManager[i].Name == name && EndPointManager[i].Method == method {\n\t\t\tlog.Warnf(\"Endpoint [%s] already exists with method [%s]\", name, method)\n\t\t}\n\t}\n\t// First we add the endpoint to the Manager so we can query it\n\tEndPointManager = append(EndPointManager, EndPoint{\n\t\tFunctionPath: endpointPattern,\n\t\tPath:         path,\n\t\tDescription:  description,\n\t\tMethod:       method,\n\t\tName:         name,\n\t})\n\t// Then we add the endpoint to the apiServer\n\tendpoints.HandleFunc(endpointPattern, epFunc).Methods(method)\n}\n\n// GetEndpoint - will return the details for an endpoint\nfunc GetEndpoint(name, method string) *EndPoint {\n\tfor i := range EndPointManager {\n\t\tif EndPointManager[i].Name == name && EndPointManager[i].Method == method {\n\t\t\treturn &EndPointManager[i]\n\t\t}\n\t}\n\treturn nil\n}\n\n// FunctionPath - this will return the api server path for any external caller using the package\nfunc FunctionPath() string {\n\treturn \"/api\"\n}\n"
  },
  {
    "path": "pkg/apiserver/go.mod",
    "content": "module github.com/plunder-app/plunder/pkg/apiserver\n\ngo 1.12\n"
  },
  {
    "path": "pkg/apiserver/handlerApiserver.go",
    "content": "package apiserver\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/gorilla/mux\"\n)\n\n// Delete the parlay results from the plunder server\nfunc getAPIFunctionMethod(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tvar rsp Response\n\t// Find the deployment ID\n\tf := mux.Vars(r)[\"function\"]\n\tm := mux.Vars(r)[\"method\"]\n\n\tep := GetEndpoint(f, m)\n\tif ep == nil {\n\t\t// RETREIVE the deployment Logs (TODO)\n\t\trsp.Warning = fmt.Sprintf(\"Unable to find HTTP method [%s] for function [%s]\", m, f)\n\t\trsp.Error = \"Error looking up in API Server\"\n\t} else {\n\t\tjsonData, err := json.Marshal(ep)\n\t\tif err != nil {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\trsp.Warning = \"Error retrieving deployment Configuration\"\n\t\t\trsp.Error = err.Error()\n\t\t} else {\n\t\t\trsp.Payload = jsonData\n\t\t}\n\t}\n\n\tjson.NewEncoder(w).Encode(rsp)\n}\n\n// Delete the parlay results from the plunder server\nfunc getAPIs(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tvar rsp Response\n\n\tjsonData, err := json.Marshal(EndPointManager)\n\tif err != nil {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\trsp.Warning = \"Error retrieving deployment Configuration\"\n\t\trsp.Error = err.Error()\n\t} else {\n\t\trsp.Payload = jsonData\n\t}\n\n\tjson.NewEncoder(w).Encode(rsp)\n}\n"
  },
  {
    "path": "pkg/apiserver/logging.go",
    "content": "package apiserver\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/gorilla/mux\"\n)\n\n// MVP of a streaming logging provider\n\n// The notificationCenter is in charge of handling the various notification managers, whihc\n// in turn will notify all of their subscribers\nvar notificationCenter map[string]*notificationManager\n\n// Notification is what will be sent to subscribers of a manager\ntype Notification struct {\n\tID      string\n\tRawData []byte\n}\n\n// RegisterNotificationManager will create a manager and an endpoint\nfunc RegisterNotificationManager(managerName, endpoint string) error {\n\t// Register the new Manager to the Notification Center\n\tnotificationCenter[managerName] = newNotificationManager()\n\tAddDynamicEndpoint(endpoint,\n\t\tendpoint,\n\t\tfmt.Sprintf(\"Automatically generated notification endpoint for [%s]\", managerName),\n\t\tmanagerName,\n\t\thttp.MethodGet,\n\t\thandleSubscribers(notificationCenter[managerName]))\n\treturn nil\n}\n\n// NotifyManager - This will Notify a Manager that there is a new notification that needs to go to subscribers\nfunc NotifyManager(managerName string, n Notification) error {\n\tmanager := notificationCenter[managerName]\n\tif manager == nil {\n\t\treturn fmt.Errorf(\"Notification Manager [%s], hasn't been registered\", managerName)\n\t}\n\tmanager.notifySubscribers(n)\n\treturn nil\n}\n\n//   --------------  Notication MAGIC below --------------\n\nfunc init() {\n\t// Initialise the notificationCenter map\n\n\tnotificationCenter = make(map[string]*notificationManager)\n\n}\n\ntype unsubscribeFunc func() error\n\ntype subscriber interface {\n\tsubscribe(n chan Notification) (unsubscribeFunc, error)\n}\n\nfunc handleSubscribers(s subscriber) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\t// Subscribe\n\t\tn := make(chan Notification)\n\t\tunsubscribeFn, err := s.subscribe(n)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\t// Set environment for streaming events\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"Cache-Control\", \"no-cache\")\n\t\tw.Header().Set(\"Connection\", \"keep-alive\")\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\n\tLooping:\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-r.Context().Done():\n\t\t\t\tif err := unsubscribeFn(); err != nil {\n\t\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tbreak Looping\n\n\t\t\tdefault:\n\t\t\t\t// Find the log ID\n\t\t\t\tid := mux.Vars(r)[\"id\"]\n\t\t\t\t// retrieve the notification\n\t\t\t\tnewNotification := <-n\n\t\t\t\t// compare the notification ID with that of the URL, optionally retrieve \"all\" notifications\n\t\t\t\tif newNotification.ID == id || id == \"all\" {\n\t\t\t\t\t// if the correct id then send them the data\n\t\t\t\t\tfmt.Fprintf(w, \"%s\\n\", newNotification.RawData)\n\t\t\t\t}\n\n\t\t\t\tw.(http.Flusher).Flush()\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype notifier interface {\n\tNotify(n Notification) error\n}\n\ntype notificationManager struct {\n\tsubscribers   map[chan Notification]struct{}\n\tsubscribersMu *sync.Mutex\n}\n\nfunc newNotificationManager() *notificationManager {\n\treturn &notificationManager{\n\t\tsubscribers:   map[chan Notification]struct{}{},\n\t\tsubscribersMu: &sync.Mutex{},\n\t}\n}\n\nfunc (nc *notificationManager) subscribe(n chan Notification) (unsubscribeFunc, error) {\n\tnc.subscribersMu.Lock()\n\tnc.subscribers[n] = struct{}{}\n\tnc.subscribersMu.Unlock()\n\n\tunsubscribeFn := func() error {\n\t\tnc.subscribersMu.Lock()\n\t\tdelete(nc.subscribers, n)\n\t\tnc.subscribersMu.Unlock()\n\n\t\treturn nil\n\t}\n\n\treturn unsubscribeFn, nil\n}\n\nfunc (nc *notificationManager) notifySubscribers(n Notification) error {\n\t// Lock them until updates are complete\n\tnc.subscribersMu.Lock()\n\tdefer nc.subscribersMu.Unlock()\n\n\tfor c := range nc.subscribers {\n\t\tselect {\n\t\tcase c <- n:\n\t\tdefault:\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/apiserver/loggingHandlers.go",
    "content": "package apiserver\n\n//var map parlay[]\n"
  },
  {
    "path": "pkg/apiserver/server.go",
    "content": "package apiserver\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/gorilla/mux\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar endpoints *mux.Router\n\nfunc init() {\n\t// 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\n\t// evaluated once the API Server starts\n\tendpoints = mux.NewRouter()\n}\n\n//StartAPIServer - will parse a configuration file and passed variables and start the API Server\nfunc StartAPIServer(path string, port int, insecure bool) error {\n\t// Open and Parse the server configuration\n\tconf, err := OpenServerConfig(path)\n\tif err != nil {\n\t\tlog.Warnln(err)\n\t\tif insecure == false {\n\t\t\tlog.Warningln(\"Secure server enabled, but no certificates have been loaded [no communication to API server is possible]\")\n\t\t}\n\t\t// Create a blank server config as one wont be returned by the above OpenFile\n\t\tconf = &ServerConfig{}\n\t}\n\tif port != 0 {\n\t\tconf.Port = port\n\t}\n\n\tlog.Infof(\"Starting API server on port %d\", conf.Port)\n\taddress := fmt.Sprintf(\":%d\", conf.Port)\n\n\t// Add the apiserver end point\n\tAddDynamicEndpoint(\"/api\",\n\t\t\"/api\",\n\t\t\"Endpoint for interacting with the api server\",\n\t\t\"apis\",\n\t\thttp.MethodGet,\n\t\tgetAPIs)\n\n\t// Add the apiserver end point\n\tAddDynamicEndpoint(\"/api/{function}/{method}\",\n\t\t\"/api\",\n\t\t\"Endpoint for interacting with the api server\",\n\t\t\"apiFunctions\",\n\t\thttp.MethodGet,\n\t\tgetAPIFunctionMethod)\n\n\t// Begin the start of a secure endpoint (TODO)\n\tif insecure == false {\n\t\tcert, err := conf.RetrieveClientCert()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tkey, err := conf.RetrieveKey()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcertPair, err := tls.X509KeyPair(cert, key)\n\t\tcfg := &tls.Config{Certificates: []tls.Certificate{certPair}}\n\t\tsrv := &http.Server{\n\t\t\tTLSConfig: cfg,\n\t\t\tAddr:      address,\n\t\t\tHandler:   endpoints,\n\t\t\t// TODO - exposing no timeout will lead to exhausted file descriptors\n\t\t\t//\tReadTimeout:  time.Minute,\n\t\t\t//\tWriteTimeout: time.Minute,\n\t\t}\n\n\t\treturn srv.ListenAndServeTLS(\"\", \"\")\n\n\t}\n\n\t// Start an insecure http server (TODO - warning)\n\treturn http.ListenAndServe(address, endpoints)\n\n}\n"
  },
  {
    "path": "pkg/apiserver/types.go",
    "content": "package apiserver\n\nimport \"encoding/json\"\n\n//Response - This is the wrapper for responses back to a client, if any errors are created then the payload isn't guarenteed\ntype Response struct {\n\tWarning string `json:\"warning,omitempty\"` // when it maybe worked\n\tError   string `json:\"error,omitempty\"`   // when it goes wrong\n\tSuccess string `json:\"success,omitempty\"` // when it goes correct\n\n\tPayload json.RawMessage `json:\"payload,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/certs/certs.go",
    "content": "package certs\n\n// generate-tls-cert generates root, leaf, and client TLS certificates.\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\"\n\t\"io/ioutil\"\n\t\"math/big\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/plunder-app/plunder/pkg/utils\"\n)\n\n// Internal variables to hold the outputs\nvar keyData, pemData []byte\n\n// GenerateKeyPair - (TODO)\nfunc GenerateKeyPair(hosts []string, start time.Time, length time.Duration) error {\n\tca := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(2019),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization:  []string{\"Plunder\"},\n\t\t\tCountry:       []string{\"UK\"},\n\t\t\tProvince:      []string{\"\"},\n\t\t\tLocality:      []string{\"Yorkshire\"},\n\t\t\tStreetAddress: []string{\"\"},\n\t\t\tPostalCode:    []string{\"\"},\n\t\t},\n\t\tNotBefore:             time.Now(),\n\t\tNotAfter:              time.Now().AddDate(10, 0, 0),\n\t\tIsCA:                  true,\n\t\tExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},\n\t\tKeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,\n\t\tBasicConstraintsValid: true,\n\t}\n\n\t// create our private and public key\n\tcaPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// create the CA\n\tcaBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// pem encode\n\tcaPEM := new(bytes.Buffer)\n\tpem.Encode(caPEM, &pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: caBytes,\n\t})\n\n\tcaPrivKeyPEM := new(bytes.Buffer)\n\tpem.Encode(caPrivKeyPEM, &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(caPrivKey),\n\t})\n\n\t// Find all IP addresses on a server\n\tserverAddresses, err := utils.FindAllIPAddresses()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Find the hostname of the server\n\tserverName, err := os.Hostname()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// set up our server certificate\n\tcert := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(2019),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization:  []string{\"Plunder\"},\n\t\t\tCountry:       []string{\"UK\"},\n\t\t\tProvince:      []string{\"\"},\n\t\t\tLocality:      []string{\"Yorkshire\"},\n\t\t\tStreetAddress: []string{\"\"},\n\t\t\tPostalCode:    []string{\"\"},\n\t\t},\n\t\tIPAddresses:  serverAddresses,\n\t\tNotBefore:    time.Now(),\n\t\tNotAfter:     time.Now().AddDate(10, 0, 0),\n\t\tSubjectKeyId: []byte{1, 2, 3, 4, 6},\n\t\tExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},\n\t\tKeyUsage:     x509.KeyUsageDigitalSignature,\n\t\tDNSNames:     []string{serverName},\n\t}\n\n\tcertPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcertBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcertPEM := new(bytes.Buffer)\n\tpem.Encode(certPEM, &pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: certBytes,\n\t})\n\tpemData = certPEM.Bytes()\n\n\tcertPrivKeyPEM := new(bytes.Buffer)\n\tpem.Encode(certPrivKeyPEM, &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(certPrivKey),\n\t})\n\n\tkeyData = certPrivKeyPEM.Bytes()\n\n\treturn nil\n}\n\n// WriteKeyToFile - will write a generated Key to a file path\nfunc WriteKeyToFile(path string) error {\n\n\terr := ioutil.WriteFile(path, keyData, 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// WritePemToFile - will write a generated pem to a file path\nfunc WritePemToFile(path string) error {\n\n\terr := ioutil.WriteFile(path, pemData, 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// GetKey - will return the []byte of the key\nfunc GetKey() []byte {\n\treturn keyData\n}\n\n// GetPem - will return the []byte of the key\nfunc GetPem() []byte {\n\treturn pemData\n}\n"
  },
  {
    "path": "pkg/certs/go.mod",
    "content": "module github.com/plunder-app/plunder/pkg/certs\n\ngo 1.12\n"
  },
  {
    "path": "pkg/go.mod",
    "content": "module github.com/plunder-app/plunder/pkg\n\ngo 1.12\n"
  },
  {
    "path": "pkg/parlay/go.mod",
    "content": "module github.com/plunder-app/plunder/pkg/parlay\n\ngo 1.12\n"
  },
  {
    "path": "pkg/parlay/handler.go",
    "content": "package parlay\n\nimport (\n\t\"encoding/json\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/plunder-app/plunder/pkg/apiserver\"\n\t\"github.com/plunder-app/plunder/pkg/parlay/parlaytypes\"\n\t\"github.com/plunder-app/plunder/pkg/services\"\n\t\"github.com/plunder-app/plunder/pkg/ssh\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar registered bool\n\n// RegisterToAPIServer - will add the endpoints to the API server\nfunc RegisterToAPIServer() {\n\t// Ensure registration only happens once\n\tif registered == true {\n\t\treturn\n\t}\n\n\t// ------------------------------------------------\n\t//        Parlay API registration\n\t// ------------------------------------------------\n\n\tapiserver.AddDynamicEndpoint(\"/parlay\",\n\t\t\"/parlay\",\n\t\t\"Create a parlay automation deployment\",\n\t\t\"parlay\",\n\t\thttp.MethodPost,\n\t\tpostParlay)\n\n\tapiserver.AddDynamicEndpoint(\"/parlay/logs/{id}\",\n\t\t\"/parlay/logs\",\n\t\t\"Retrieve the logs from a parlay deployment\",\n\t\t\"parlayLog\",\n\t\thttp.MethodGet,\n\t\tgetParlay)\n\n\tapiserver.AddDynamicEndpoint(\"/parlay/logs/{id}\",\n\t\t\"/parlay/logs\",\n\t\t\"Delete the cached logs from a specific parlay deployment\",\n\t\t\"parlayLog\",\n\t\thttp.MethodDelete,\n\t\tdelParlay)\n\tregistered = true\n}\n\n// Retrieve a specific plunder deployment configuration\nfunc postParlay(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tvar rsp apiserver.Response\n\n\tif b, err := ioutil.ReadAll(r.Body); err == nil {\n\t\t// Parse the treasure map in the POST data\n\t\tvar m parlaytypes.TreasureMap\n\t\terr := json.Unmarshal(b, &m)\n\t\t// Unable to parse the JSON payload\n\t\tif err != nil {\n\t\t\trsp.Warning = \"Error parsing the parlay actions\"\n\t\t\trsp.Error = err.Error()\n\t\t} else {\n\t\t\t// Parsed succesfully, we will deploy this in a go routine and use GET /parlay/MAC to view progress\n\t\t\t//\n\t\t\terr = ssh.ImportHostsFromDeployment(services.Deployments)\n\t\t\tif err != nil {\n\t\t\t\trsp.Warning = \"Error importing the hosts from deployment\"\n\t\t\t\trsp.Error = err.Error()\n\t\t\t} else {\n\t\t\t\terr = DeploySSH(&m, \"\", true, true)\n\t\t\t\tif err != nil {\n\t\t\t\t\trsp.Warning = \"Error performing the parlay actions\"\n\t\t\t\t\trsp.Error = err.Error()\n\t\t\t\t\tlog.Errorf(\"%s\", err.Error())\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t} else {\n\t\trsp.Warning = \"Error reading HTTP data\"\n\t\trsp.Error = err.Error()\n\n\t}\n\n\tjson.NewEncoder(w).Encode(rsp)\n}\n\n// Retrieve a specific parlay automation\nfunc getParlay(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tvar rsp apiserver.Response\n\t// Find the deployment ID\n\tid := mux.Vars(r)[\"id\"]\n\n\t// We need to revert the mac address back to the correct format (dashes back to colons)\n\ttarget := strings.Replace(id, \"-\", \".\", -1)\n\n\t// Use the mac address to lookup the deployment\n\tlogs, err := GetTargetLogs(target)\n\t// If the deployment exists then process the POST data\n\tif err != nil {\n\t\t// RETREIVE the deployment Logs (TODO)\n\t\trsp.Warning = \"Error reading Parlay Logs\"\n\t\trsp.Error = err.Error()\n\t} else {\n\t\tjsonData, err := json.Marshal(logs)\n\t\tif err != nil {\n\n\t\t\t// RETREIVE the deployment Logs (TODO)\n\t\t\trsp.Warning = \"Error parsing Parlay Logs\"\n\t\t\trsp.Error = err.Error()\n\t\t} else {\n\t\t\trsp.Payload = jsonData\n\t\t}\n\t}\n\n\tjson.NewEncoder(w).Encode(rsp)\n}\n\n// Delete the parlay results from the plunder server\nfunc delParlay(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tvar rsp apiserver.Response\n\t// Find the deployment ID\n\tid := mux.Vars(r)[\"id\"]\n\n\t// We need to revert the mac address back to the correct format (dashes back to colons)\n\ttarget := strings.Replace(id, \"-\", \".\", -1)\n\n\t// Use the mac address to lookup the deployment\n\terr := DeleteTargetLogs(target)\n\t// If the deployment exists then process the POST data\n\tif err != nil {\n\n\t\t// RETREIVE the deployment Logs (TODO)\n\t\trsp.Warning = \"Error reading deleting logs\"\n\t\trsp.Error = err.Error()\n\t}\n\n\tjson.NewEncoder(w).Encode(rsp)\n}\n"
  },
  {
    "path": "pkg/parlay/parlay.go",
    "content": "package parlay\n\ntype actionType string\n\nconst (\n\t//upload - defines that this action will upload a file to a remote system\n\tupload   actionType = \"upload\" //\n\tdownload actionType = \"download\"\n\tcommand  actionType = \"command\"\n\tpkg      actionType = \"package\"\n)\n\n// KeyMap\n\n// Keys are used to store information between sessions and deployments\nvar Keys map[string]string\n\nfunc init() {\n\t// Initialise the map\n\tKeys = make(map[string]string)\n}\n"
  },
  {
    "path": "pkg/parlay/parlay_ui.go",
    "content": "package parlay\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/AlecAivazis/survey/v2\"\n\t\"github.com/plunder-app/plunder/pkg/parlay/parlaytypes\"\n)\n\nfunc contains(v string, a []string) bool {\n\tfor _, i := range a {\n\t\tif strings.Contains(v, i) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// StartUI will enable parlay to provide an easier way of selecting which operations will be performed\nfunc StartUI(m *parlaytypes.TreasureMap) (*parlaytypes.TreasureMap, error) {\n\n\tdeployments := []string{}\n\tfor i := range m.Deployments {\n\t\tdeployments = append(deployments, m.Deployments[i].Name)\n\t}\n\tif len(deployments) == 0 {\n\t\treturn nil, fmt.Errorf(\"No Deployments were found\")\n\t}\n\n\tvar multiQs = []*survey.Question{\n\t\t{\n\t\t\tName: \"letter\",\n\t\t\tPrompt: &survey.MultiSelect{\n\t\t\t\tMessage: \"Select deployment(s)\",\n\t\t\t\tOptions: deployments,\n\t\t\t},\n\t\t},\n\t}\n\tdeploymentAnswers := []string{}\n\n\t// ask the question\n\terr := survey.Ask(multiQs, &deploymentAnswers)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create a new TreasureMap from the answered questions\n\tnewMap, err := m.FindDeployments(deploymentAnswers)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor i := range newMap.Deployments {\n\n\t\t// Ask for Hosts\n\t\tmultiQs[0].Prompt = &survey.MultiSelect{\n\t\t\tMessage: fmt.Sprintf(\"Select Hosts(s) for [%s]\", newMap.Deployments[i].Name),\n\t\t\tOptions: newMap.Deployments[i].Hosts,\n\t\t}\n\n\t\thostAnswers := []string{}\n\t\terr := survey.Ask(multiQs, &hostAnswers)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Ask for Actions\n\t\tactions := []string{}\n\t\tfor y := range newMap.Deployments[i].Actions {\n\t\t\tactions = append(actions, m.Deployments[i].Actions[y].Name)\n\t\t}\n\n\t\tif len(actions) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"No Deployments were found\")\n\t\t}\n\t\tmultiQs[0].Prompt = &survey.MultiSelect{\n\t\t\tMessage: fmt.Sprintf(\"Select Actions(s) for [%s]\", newMap.Deployments[i].Name),\n\t\t\tOptions: actions,\n\t\t}\n\n\t\tdeploymentAnswers := []string{}\n\t\terr = survey.Ask(multiQs, &deploymentAnswers)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tnewMap.Deployments[i].Hosts = hostAnswers\n\t\tfoundActions, err := newMap.Deployments[i].FindActions(deploymentAnswers)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnewMap.Deployments[i].Actions = foundActions\n\t}\n\n\treturn newMap, nil\n}\n"
  },
  {
    "path": "pkg/parlay/parlaytypes/finder.go",
    "content": "package parlaytypes\n\nimport (\n\t\"fmt\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// FindDeployments - This will iterate through a deployment map and build a new deployment map from found deployments\nfunc (m *TreasureMap) FindDeployments(deployment []string) (*TreasureMap, error) {\n\n\tvar newDeploymentList []Deployment\n\n\tfor x := range deployment {\n\t\tfor y := range m.Deployments {\n\t\t\tif m.Deployments[y].Name == deployment[x] {\n\t\t\t\tnewDeploymentList = append(newDeploymentList, m.Deployments[y])\n\t\t\t}\n\t\t}\n\t}\n\t// If this is zero it means that no deployments have been found\n\tif len(m.Deployments) == 0 {\n\t\treturn nil, fmt.Errorf(\"No Deployment(s) have been found\")\n\t}\n\tm.Deployments = newDeploymentList\n\treturn m, nil\n}\n\n// FindHosts - will iterate through the deployment hosts and compare to the array of hosts to return\nfunc (d *Deployment) FindHosts(hosts []string) (*Deployment, error) {\n\n\tvar newHostList []string\n\n\tfor x := range hosts {\n\t\tfor y := range d.Hosts {\n\t\t\tif d.Hosts[y] == hosts[x] {\n\t\t\t\tnewHostList = append(newHostList, d.Hosts[y])\n\t\t\t}\n\t\t}\n\t}\n\t// If this is zero it means that no hosts have been found\n\tif len(d.Hosts) == 0 {\n\t\treturn nil, fmt.Errorf(\"No Host(s) have been found\")\n\t}\n\td.Hosts = newHostList\n\treturn d, nil\n}\n\n// FindActions - will iterate through the deployment actions and compare to the array of actions to return\nfunc (d *Deployment) FindActions(actions []string) ([]Action, error) {\n\tvar newActionList []Action\n\n\tfor x := range actions {\n\t\tfor y := range d.Actions {\n\t\t\tif d.Actions[y].Name == actions[x] {\n\t\t\t\tnewActionList = append(newActionList, d.Actions[y])\n\t\t\t}\n\t\t}\n\t}\n\t// If this is zero it means that no hosts have been found\n\tif len(d.Actions) == 0 {\n\t\treturn nil, fmt.Errorf(\"No Action(s) have been found\")\n\t}\n\treturn newActionList, nil\n}\n\n//FindDeployment - takes a number of flags and builds a new map to be processed\nfunc (m *TreasureMap) FindDeployment(deployment, action, host, logFile string, resume bool) (*TreasureMap, error) {\n\tvar foundMap TreasureMap\n\tif deployment != \"\" {\n\t\tlog.Debugf(\"Looking for deployment [%s]\", deployment)\n\t\tfor x := range m.Deployments {\n\t\t\tif m.Deployments[x].Name == deployment {\n\t\t\t\tfoundMap.Deployments = append(foundMap.Deployments, m.Deployments[x])\n\t\t\t\t// Find a specific action and add or resume from\n\t\t\t\tif action != \"\" {\n\t\t\t\t\t// Clear the slice as we will be possibly adding different actions\n\t\t\t\t\tfoundMap.Deployments[0].Actions = nil\n\t\t\t\t\tfor y := range m.Deployments[x].Actions {\n\t\t\t\t\t\tif m.Deployments[x].Actions[y].Name == action {\n\t\t\t\t\t\t\t// If we're not resuming that just add the action that we want to happen\n\t\t\t\t\t\t\tif resume != true {\n\t\t\t\t\t\t\t\tfoundMap.Deployments[0].Actions = append(foundMap.Deployments[0].Actions, m.Deployments[x].Actions[y])\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Alternatively add all actions from this point\n\t\t\t\t\t\t\t\tfoundMap.Deployments[0].Actions = m.Deployments[x].Actions[y:]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// If this is zero it means that no actions have been found\n\t\t\t\t\tif len(foundMap.Deployments[0].Actions) == 0 {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"No actions have been found, looking for action [%s]\", action)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// If a host is specified act soley on it\n\t\t\t\tif host != \"\" {\n\t\t\t\t\t// Clear the slice as we will be possibly adding different actions\n\t\t\t\t\tfoundMap.Deployments[0].Hosts = nil\n\t\t\t\t\tfor y := range m.Deployments[x].Hosts {\n\n\t\t\t\t\t\tif m.Deployments[x].Hosts[y] == host {\n\t\t\t\t\t\t\tfoundMap.Deployments[0].Hosts = append(foundMap.Deployments[0].Hosts, m.Deployments[x].Hosts[y])\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// If this is zero it means that no hosts have been found\n\t\t\t\t\tif len(foundMap.Deployments[0].Hosts) == 0 {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"No host has been found, looking for host [%s]\", host)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// If this is zero it means that no actions have been found\n\t\tif len(foundMap.Deployments) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"No deployment has been found, looking for deployment [%s]\", deployment)\n\t\t}\n\t} else {\n\t\treturn nil, fmt.Errorf(\"No deployment was specified\")\n\t}\n\treturn &foundMap, nil\n\t//return parlay.DeploySSH(foundMap, logFile, false, false)\n}\n"
  },
  {
    "path": "pkg/parlay/parlaytypes/go.mod",
    "content": "module github.com/plunder-app/plunder/pkg/parlay/parlaytypes\n\ngo 1.12\n"
  },
  {
    "path": "pkg/parlay/parlaytypes/parlaytypes.go",
    "content": "package parlaytypes\n\nimport (\n\t\"encoding/json\"\n)\n\n// TreasureMap - X Marks the spot\n// The treasure maps define the automation that will take place on the hosts defined\ntype TreasureMap struct {\n\t// An array/list of deployments that will take places as part of this \"map\"\n\tDeployments []Deployment `json:\"deployments\"`\n}\n\n// Deployment defines the hosts and the action(s) that should be performed on them\ntype Deployment struct {\n\t// Name of the deployment that is taking place i.e. (Install MySQL)\n\tName string `json:\"name\"`\n\t// An array/list of hosts that these actions should be performed upon\n\tHosts []string `json:\"hosts\"`\n\n\t// Parallel allow multiple actions across multiple hosts in parallel\n\tParallel         bool `json:\"parallel\"`\n\tParallelSessions int  `json:\"parallelSessions\"`\n\n\t// The actions that should be performed\n\tActions []Action `json:\"actions\"`\n}\n\n// Action defines what the instructions that will be executed\ntype Action struct {\n\tName       string `json:\"name\"`\n\tActionType string `json:\"type\"`\n\tTimeout    int    `json:\"timeout\"`\n\n\t// File based operations\n\tSource      string `json:\"source,omitempty\"`\n\tDestination string `json:\"destination,omitempty\"`\n\tFileMove    bool   `json:\"fileMove,omitempty\"`\n\n\t// Package manager operations\n\tPkgManager   string `json:\"packageManager,omitempty\"`\n\tPkgOperation string `json:\"packageOperation,omitempty\"`\n\tPackages     string `json:\"packages,omitempty\"`\n\n\t// Command operations\n\tCommand          string   `json:\"command,omitempty\"`\n\tCommands         []string `json:\"commands,omitempty\"`\n\tCommandLocal     bool     `json:\"commandLocal,omitempty\"`\n\tCommandSaveFile  string   `json:\"commandSaveFile,omitempty\"`\n\tCommandSaveAsKey string   `json:\"commandSaveAsKey,omitempty\"`\n\tCommandSudo      string   `json:\"commandSudo,omitempty\"`\n\n\t// Piping commands, read in a file and send over stdin, or capture stdout from a local command\n\tCommandPipeFile string `json:\"commandPipeFile,omitempty\"`\n\tCommandPipeCmd  string `json:\"commandPipeCmd,omitempty\"`\n\n\t// Ignore any failures\n\tIgnoreFailure bool `json:\"ignoreFail,omitempty\"`\n\n\t// Key operations\n\tKeyFile string `json:\"keyFile,omitempty\"`\n\tKeyName string `json:\"keyName,omitempty\"`\n\n\t//Plugin Spec\n\tPlugin json.RawMessage `json:\"plugin,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/parlay/parser.go",
    "content": "package parlay\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/plunder-app/plunder/pkg/plunderlogging\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/plunder-app/plunder/pkg/parlay/parlaytypes\"\n\tparlayplugin \"github.com/plunder-app/plunder/pkg/parlay/plugin\"\n\t\"github.com/plunder-app/plunder/pkg/ssh\"\n)\n\n// This logger will manage all of the logging for Parlay\nvar logger plunderlogging.Logger\n\n// GetTargetLogs will retrieve the JSON logs\nfunc GetTargetLogs(target string) (*plunderlogging.JSONLog, error) {\n\treturn logger.GetJSONLogs(target)\n}\n\n// DeleteTargetLogs will retrieve the JSON logs\nfunc DeleteTargetLogs(target string) error {\n\treturn logger.DeleteLogs(target)\n}\n\n// DeploySSH - will iterate through a deployment and perform the relevant actions\nfunc DeploySSH(m *parlaytypes.TreasureMap, logFile string, jsonLogging, background bool) error {\n\n\tif len(ssh.Hosts) == 0 {\n\t\tlog.Warnln(\"No hosts credentials have been loaded, only commands with commandLocal = true will work\")\n\t}\n\tif len(m.Deployments) == 0 {\n\t\treturn fmt.Errorf(\"No Deployments in parlay map\")\n\t}\n\n\tfor x := range m.Deployments {\n\t\t// Build new hosts list from imported SSH servers and compare that we have required credentials\n\t\t_, err := ssh.FindHosts(m.Deployments[x].Hosts)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Begin the deployment\n\tif logFile != \"\" {\n\t\t//enable logging\n\t\tlogger.InitLogFile(logFile)\n\t}\n\n\tif jsonLogging {\n\t\tlogger.InitJSON()\n\t}\n\n\tif background {\n\t\tgo startDeployments(m.Deployments)\n\t} else {\n\t\tstartDeployments(m.Deployments)\n\t}\n\n\t// TODO - test original automation\n\n\t// for x := range m.Deployments {\n\t// \t// Build new hosts list from imported SSH servers and compare that we have required credentials\n\t// \thosts, err := ssh.FindHosts(m.Deployments[x].Hosts)\n\t// \tif err != nil {\n\t// \t\treturn err\n\t// \t}\n\n\t// \t// Beggining of deployment work\n\t// \tlog.Infof(\"Beginning Deployment [%s]\\n\", m.Deployments[x].Name)\n\t// \tlogger.WriteLogEntry(\"\", \"\", \"\", fmt.Sprintf(\"Beginning Deployment [%s]\\n\", m.Deployments[x].Name))\n\n\t// \t// Set Restore checkpoint\n\t// \trestore.Deployment = m.Deployments[x].Name\n\t// \trestore.Hosts = m.Deployments[x].Hosts\n\n\t// \tif m.Deployments[x].Parallel == true {\n\t// \t\t// Begin this deployment in parallel across all hosts\n\t// \t\terr = parallelDeployment(m.Deployments[x].Actions, hosts, &logger)\n\t// \t\tif err != nil {\n\t// \t\t\treturn err\n\t// \t\t}\n\t// \t} else {\n\t// \t\t// This work will be sequential, one host after the next\n\t// \t\tfor z := range m.Deployments[x].Hosts {\n\t// \t\t\tvar hostConfig ssh.HostSSHConfig\n\t// \t\t\t// Find the hosts SSH configuration\n\t// \t\t\tfor i := range hosts {\n\t// \t\t\t\tif hosts[i].Host == m.Deployments[x].Hosts[z] {\n\t// \t\t\t\t\thostConfig = hosts[i]\n\t// \t\t\t\t}\n\t// \t\t\t}\n\t// \t\t\t// Set the state of logging actions to in-progress\n\t// \t\t\tlogger.SetLoggingState(hostConfig.Host, \"Running\")\n\t// \t\t\terr = sequentialDeployment(m.Deployments[x].Actions, hostConfig, &logger)\n\t// \t\t\tif err != nil {\n\t// \t\t\t\tlogger.SetLoggingState(hostConfig.Host, \"Failed\")\n\t// \t\t\t\treturn err\n\t// \t\t\t}\n\t// \t\t\t// Set the state of logging actions to completed\n\t// \t\t\tlogger.SetLoggingState(hostConfig.Host, \"Completed\")\n\t// \t\t}\n\t// \t}\n\t// }\n\treturn nil\n}\n\nfunc startDeployments(d []parlaytypes.Deployment) error {\n\tfor x := range d {\n\t\t// Build new hosts list from imported SSH servers and compare that we have required credentials\n\t\thosts, err := ssh.FindHosts(d[x].Hosts)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Beggining of deployment work\n\t\tlog.Infof(\"Beginning Deployment [%s]\\n\", d[x].Name)\n\t\tlogger.WriteLogEntry(\"\", \"\", \"\", fmt.Sprintf(\"Beginning Deployment [%s]\\n\", d[x].Name))\n\n\t\t// Set Restore checkpoint\n\t\trestore.Deployment = d[x].Name\n\t\trestore.Hosts = d[x].Hosts\n\n\t\tif d[x].Parallel == true {\n\t\t\t// Begin this deployment in parallel across all hosts\n\t\t\terr = parallelDeployment(d[x].Actions, hosts, &logger)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\t// This work will be sequential, one host after the next\n\t\t\tfor z := range d[x].Hosts {\n\t\t\t\tvar hostConfig ssh.HostSSHConfig\n\t\t\t\t// Find the hosts SSH configuration\n\t\t\t\tfor i := range hosts {\n\t\t\t\t\tif hosts[i].Host == d[x].Hosts[z] {\n\t\t\t\t\t\thostConfig = hosts[i]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Set the state of logging actions to in-progress\n\t\t\t\tlogger.SetLoggingState(hostConfig.Host, \"Running\")\n\t\t\t\terr = sequentialDeployment(d[x].Actions, hostConfig, &logger)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogger.SetLoggingState(hostConfig.Host, \"Failed\")\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\t// Set the state of logging actions to completed\n\t\t\t\tlogger.SetLoggingState(hostConfig.Host, \"Completed\")\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// Begin host by host deployments as part of each deployment\nfunc sequentialDeployment(action []parlaytypes.Action, hostConfig ssh.HostSSHConfig, logger *plunderlogging.Logger) error {\n\tvar err error\n\n\tfor y := range action {\n\t\tswitch action[y].ActionType {\n\t\tcase \"upload\":\n\t\t\terr = hostConfig.UploadFile(action[y].Source, action[y].Destination)\n\t\t\tif err != nil {\n\t\t\t\t// Set checkpoint\n\t\t\t\trestore.Action = action[y].Name\n\t\t\t\trestore.Host = hostConfig.Host\n\t\t\t\trestore.createCheckpoint()\n\t\t\t\tlogger.WriteLogEntry(hostConfig.Host, action[y].Name, \"\", err.Error())\n\t\t\t\t// Return the error\n\t\t\t\treturn fmt.Errorf(\"Upload task [%s] on host [%s] failed with error [%s]\", action[y].Name, hostConfig.Host, err)\n\t\t\t}\n\t\t\tlog.Infof(\"Upload Task [%s] on node [%s] completed successfully\", action[y].Name, hostConfig.Host)\n\t\tcase \"download\":\n\t\t\terr = hostConfig.DownloadFile(action[y].Source, action[y].Destination)\n\t\t\tif err != nil {\n\t\t\t\t// Set checkpoint\n\t\t\t\trestore.Action = action[y].Name\n\t\t\t\trestore.Host = hostConfig.Host\n\t\t\t\trestore.createCheckpoint()\n\t\t\t\tlogger.WriteLogEntry(hostConfig.Host, action[y].Name, \"\", err.Error())\n\t\t\t\t// Return the error\n\t\t\t\treturn fmt.Errorf(\"Download task [%s] on host [%s] failed with error [%s]\", action[y].Name, hostConfig.Host, err)\n\t\t\t}\n\t\t\tlog.Infof(\"Succesfully Downloaded [%s] to [%s] from [%s]\", action[y].Source, action[y].Destination, hostConfig.Host)\n\t\tcase \"command\":\n\t\t\t// Build out a configuration based upon the action\n\t\t\tcr := parseAndExecute(action[y], &hostConfig)\n\t\t\t// This will end command execution and print the error\n\t\t\tif cr.Error != nil && action[y].IgnoreFailure == false {\n\t\t\t\t// Set checkpoint\n\t\t\t\trestore.Action = action[y].Name\n\t\t\t\trestore.Host = hostConfig.Host\n\t\t\t\trestore.createCheckpoint()\n\n\t\t\t\t// Output error messages\n\t\t\t\tlogger.WriteLogEntry(hostConfig.Host, action[y].Name, cr.Result, cr.Error.Error())\n\t\t\t\t// cr.Result is ommited here TODO\n\t\t\t\treturn fmt.Errorf(\"Command task [%s] on host [%s] failed with error [%s]\", action[y].Name, hostConfig.Host, cr.Error)\n\t\t\t}\n\n\t\t\t// if there is an error and we're set to ignore it then process accordingly\n\t\t\tif cr.Error != nil && action[y].IgnoreFailure == true {\n\t\t\t\tlog.Warnf(\"Command Task [%s] on node [%s] failed (execution will continute)\", action[y].Name, hostConfig.Host)\n\t\t\t\tlog.Debugf(\"Command Results ->\\n%s\", cr.Result)\n\t\t\t\tlogger.WriteLogEntry(hostConfig.Host, action[y].Name, cr.Result, cr.Error.Error())\n\n\t\t\t\t//logger.WriteLogEntry(hostConfig.Host, fmt.Sprintf(\"Command task [%s] on host [%s] has failed (execution will continute)\\n\", action[y].Name, hostConfig.Host))\n\t\t\t}\n\n\t\t\t// No error, task was completed correctly\n\t\t\tif cr.Error == nil {\n\t\t\t\t// Output success Messages\n\t\t\t\tlog.Infof(\"Command Task [%s] on node [%s] completed successfully\", action[y].Name, hostConfig.Host)\n\t\t\t\tlog.Debugf(\"Command Results ->\\n%s\", cr.Result)\n\t\t\t\t//logger.WriteLogEntry(hostConfig.Host, fmt.Sprintf(\"Command task [%s] on host [%s] has completed succesfully\\n\", action[y].Name, hostConfig.Host))\n\t\t\t\t//logger.WriteLogEntry(hostConfig.Host, fmt.Sprintf(\"Command task [%s] Output [%s]\\n\", action[y].Name, cr.Result))\n\t\t\t\tlogger.WriteLogEntry(hostConfig.Host, action[y].Name, cr.Result, \"\")\n\n\t\t\t}\n\t\tcase \"pkg\":\n\n\t\tcase \"key\":\n\n\t\tdefault:\n\t\t\t// Set checkpoint (the actiontype may be modified or spelling issue)\n\t\t\trestore.Action = action[y].Name\n\t\t\trestore.Host = hostConfig.Host\n\t\t\trestore.createCheckpoint()\n\t\t\tpluginActions, err := parlayplugin.ExecuteAction(action[y].ActionType, hostConfig.Host, action[y].Plugin)\n\t\t\tif err != nil {\n\t\t\t\tlogger.WriteLogEntry(hostConfig.Host, action[y].Name, \"\", err.Error())\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tlog.Debugf(\"About to execute [%d] actions\", len(pluginActions))\n\t\t\terr = sequentialDeployment(pluginActions, hostConfig, logger)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// Peform all of the actions in parallel on all hosts in the host array\n// this function will make use of the parallel ssh calls\nfunc parallelDeployment(action []parlaytypes.Action, hosts []ssh.HostSSHConfig, logger *plunderlogging.Logger) error {\n\tfor y := range action {\n\t\tswitch action[y].ActionType {\n\t\tcase \"upload\":\n\n\t\t\t//TODO - Remove or repurpose GENERAL output\n\t\t\tlogger.WriteLogEntry(\"upload\", fmt.Sprintf(\"Uploading file [%s] to Destination [%s] to multiple hosts\\n\", action[y].Source, action[y].Destination), \"\", \"\")\n\n\t\t\tresults := ssh.ParalellUpload(hosts, action[y].Source, action[y].Destination, action[y].Timeout)\n\t\t\t// TODO - Unlikely that this should happen\n\t\t\tif len(results) == 0 {\n\t\t\t\treturn fmt.Errorf(\"No results have been returned from the parallel execution\")\n\t\t\t}\n\t\t\t// Parse the results from the parallel updates\n\t\t\tfor i := range results {\n\t\t\t\tif results[i].Error != nil {\n\t\t\t\t\t// Set checkpoint\n\t\t\t\t\trestore.Action = action[y].Name\n\t\t\t\t\trestore.createCheckpoint()\n\t\t\t\t\tlogger.WriteLogEntry(results[i].Host, action[y].Name, \"\", results[i].Error.Error())\n\t\t\t\t\tlogger.SetLoggingState(results[i].Host, \"Failed\")\n\n\t\t\t\t\t//logger.WriteLogEntry(\"\", fmt.Sprintf(\"[%s] Error uploading file [%s] to Destination [%s] to host [%s]\\n\", time.Now().Format(time.ANSIC), action[y].Source, action[y].Destination, results[i].Host))\n\t\t\t\t\t//logger.WriteLogEntry(\"\", fmt.Sprintf(\"[%s] [%s]\\n\", time.Now().Format(time.ANSIC), results[i].Error()))\n\t\t\t\t\treturn fmt.Errorf(\"Upload task [%s] on host [%s] failed with error [%s]\", action[y].Name, results[i].Host, results[i].Error)\n\t\t\t\t}\n\t\t\t\tlogger.WriteLogEntry(results[i].Host, action[y].Name, fmt.Sprintf(\"Completed uploading file [%s] to path [%s]\", action[y].Source, action[y].Destination), results[i].Error.Error())\n\n\t\t\t\t//logger.WriteLogEntry(\"\", fmt.Sprintf(\"[%s] Completed uploading file [%s] to Destination [%s] to host [%s]\\n\", time.Now().Format(time.ANSIC), action[y].Source, action[y].Destination, results[i].Host))\n\t\t\t\tlog.Infof(\"Succesfully uploaded [%s] to [%s] on [%s]\", action[y].Source, action[y].Destination, results[i].Host)\n\t\t\t}\n\t\tcase \"download\":\n\t\t\tlogger.WriteLogEntry(\"download\", fmt.Sprintf(\"Downloading file [%s] to Destination [%s] from multiple hosts\\n\", action[y].Source, action[y].Destination), \"\", \"\")\n\n\t\t\tresults := ssh.ParalellDownload(hosts, action[y].Source, action[y].Destination, action[y].Timeout)\n\t\t\t// Unlikely that this should happen\n\t\t\tif len(results) == 0 {\n\t\t\t\treturn fmt.Errorf(\"No results have been returned from the parallel execution\")\n\t\t\t}\n\t\t\t// Parse the results from the parallel updates\n\t\t\tfor i := range results {\n\t\t\t\tif results[i].Error != nil {\n\t\t\t\t\t// Set checkpoint\n\t\t\t\t\trestore.Action = action[y].Name\n\t\t\t\t\trestore.createCheckpoint()\n\t\t\t\t\tlogger.WriteLogEntry(results[i].Host, action[y].Name, \"\", results[i].Error.Error())\n\t\t\t\t\tlogger.SetLoggingState(results[i].Host, \"Failed\")\n\n\t\t\t\t\t//logger.WriteLogEntry(\"\", fmt.Sprintf(\"[%s] Error downloading file [%s] to [%s] to host [%s]\\n\", time.Now().Format(time.ANSIC), action[y].Source, action[y].Destination, results[i].Host))\n\t\t\t\t\t//logger.WriteLogEntry(\"\", fmt.Sprintf(\"[%s] [%s]\\n\", time.Now().Format(time.ANSIC), results[i].Error))\n\n\t\t\t\t\treturn fmt.Errorf(\"Download task [%s] on host [%s] failed with error [%s]\", action[y].Name, results[i].Host, results[i].Error)\n\t\t\t\t}\n\t\t\t\tlogger.WriteLogEntry(results[i].Host, action[y].Name, fmt.Sprintf(\"Completed Downloading file [%s] to path [%s]\", action[y].Source, action[y].Destination), results[i].Error.Error())\n\n\t\t\t\t//logger.WriteLogEntry(\"\", fmt.Sprintf(\"[%s] Completed uploading file [%s] to Destination [%s] to host [%s]\\n\", time.Now().Format(time.ANSIC), action[y].Source, action[y].Destination, results[i].Host))\n\t\t\t\tlog.Infof(\"Succesfully uploaded [%s] to [%s] on [%s]\", action[y].Source, action[y].Destination, results[i].Host)\n\t\t\t}\n\t\tcase \"command\":\n\t\t\tlogger.WriteLogEntry(\"command\", fmt.Sprintf(\"Executing command action [%s] to multiple hosts\\n\", action[y].Name), \"\", \"\")\n\t\t\tcommand, err := buildCommand(action[y])\n\t\t\tif err != nil {\n\t\t\t\t// Set checkpoint\n\t\t\t\trestore.Action = action[y].Name\n\t\t\t\trestore.createCheckpoint()\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcrs := ssh.ParalellExecute(command, action[y].CommandPipeFile, action[y].CommandPipeCmd, hosts, action[y].Timeout)\n\t\t\tvar errors bool // This will only be set to true if a command fails\n\t\t\tfor x := range crs {\n\t\t\t\tif crs[x].Error != nil {\n\t\t\t\t\t// Set checkpoint\n\t\t\t\t\trestore.Action = action[y].Name\n\t\t\t\t\trestore.createCheckpoint()\n\t\t\t\t\tlogger.WriteLogEntry(crs[x].Host, action[y].Name, crs[x].Result, crs[x].Error.Error())\n\t\t\t\t\tlogger.SetLoggingState(crs[x].Host, \"Failed\")\n\t\t\t\t\t//log.Errorf(\"Command task [%s] on host [%s] failed with error [%s]\\n\\t[%s]\", action[y].Name, crs[x].Host, crs[x].Result, crs[x].Error.Error())\n\t\t\t\t\terrors = true // An error has been found\n\t\t\t\t\t//logger.WriteLogEntry(\"\", fmt.Sprintf(\"------------  Output  ------------\\n%s\\n----------------------------------\\n\", crs[x].Result))\n\t\t\t\t\treturn fmt.Errorf(\"Command task [%s] on host [%s] failed with error [%s]\\n\\t[%s]\", action[y].Name, crs[x].Host, crs[x].Error, crs[x].Result)\n\t\t\t\t}\n\t\t\t\tlog.Infof(\"Command Task [%s] on node [%s] completed successfully\", action[y].Name, crs[x].Host)\n\t\t\t\tlogger.WriteLogEntry(crs[x].Host, action[y].Name, crs[x].Result, \"\")\n\n\t\t\t\t//logger.WriteLogEntry(\"\", fmt.Sprintf(\"[%s] Command task [%s] on host [%s] has completed succesfully\\n\", time.Now().Format(time.ANSIC), action[y].Name, crs[x].Host))\n\t\t\t\t//logger.WriteLogEntry(\"\", fmt.Sprintf(\"------------  Output  ------------\\n%s\\n----------------------------------\\n\", crs[x].Result))\n\t\t\t}\n\t\t\tif errors == true {\n\t\t\t\treturn fmt.Errorf(\"An error was encountered on command Task [%s]\", action[y].Name)\n\t\t\t}\n\t\tcase \"pkg\":\n\n\t\tcase \"key\":\n\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"Unknown Action [%s]\", action[y].ActionType)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/parlay/parser_builder.go",
    "content": "package parlay\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/plunder-app/plunder/pkg/parlay/parlaytypes\"\n\t\"github.com/plunder-app/plunder/pkg/ssh\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc buildCommand(a parlaytypes.Action) (string, error) {\n\tvar command string\n\n\t// An executable Key takes presedence\n\tif a.KeyName != \"\" {\n\t\tkeycmd := Keys[a.KeyName]\n\t\t// Check that the key exists\n\t\tif keycmd == \"\" {\n\t\t\treturn \"\", fmt.Errorf(\"Unable to find command under key '%s'\", a.KeyName)\n\n\t\t}\n\t\tif a.CommandSudo != \"\" {\n\t\t\t// Add sudo to the Key command\n\t\t\tcommand = fmt.Sprintf(\"sudo -n -u %s %s\", a.CommandSudo, keycmd)\n\t\t} else {\n\t\t\tcommand = keycmd\n\t\t}\n\t} else {\n\t\t// Not using a key, using a shell command\n\t\tif a.CommandSudo != \"\" {\n\t\t\t// Add sudo to the Shell command\n\t\t\tcommand = fmt.Sprintf(\"sudo -n -u %s %s\", a.CommandSudo, a.Command)\n\t\t} else {\n\t\t\tcommand = a.Command\n\t\t}\n\t}\n\treturn command, nil\n}\n\nfunc parseAndExecute(a parlaytypes.Action, h *ssh.HostSSHConfig) ssh.CommandResult {\n\t// This will parse the options passed in the action and execute the required string\n\tvar cr ssh.CommandResult\n\tvar b []byte\n\n\tcommand, err := buildCommand(a)\n\tif err != nil {\n\t\tcr.Error = err\n\t\treturn cr\n\t}\n\n\tif a.CommandLocal == true {\n\t\tlog.Debugf(\"Command [%s]\", command)\n\t\tcmd := exec.Command(\"bash\", \"-c\", command)\n\t\tb, cr.Error = cmd.CombinedOutput()\n\t\tif cr.Error != nil {\n\t\t\treturn cr\n\t\t}\n\t\tcr.Result = strings.TrimRight(string(b), \"\\r\\n\")\n\t} else {\n\t\tlog.Debugf(\"Executing command [%s] on host [%s]\", command, h.Host)\n\t\tcr = ssh.SingleExecute(command, a.CommandPipeFile, a.CommandPipeCmd, *h, a.Timeout)\n\n\t\tcr.Result = strings.TrimRight(cr.Result, \"\\r\\n\")\n\n\t\t// If the command hasn't returned anything, put a filler in\n\t\tif cr.Result == \"\" {\n\t\t\tcr.Result = \"[No Output]\"\n\t\t}\n\t\tif cr.Error != nil {\n\t\t\treturn cr\n\t\t}\n\t}\n\n\t// Save the results into a key to be used at another point\n\tif a.CommandSaveAsKey != \"\" {\n\t\tlog.Debugf(\"Adding new results to key [%s]\", a.CommandSaveAsKey)\n\t\tKeys[a.CommandSaveAsKey] = cr.Result\n\t}\n\n\t// Save the results into a file to be used at another point\n\tif a.CommandSaveFile != \"\" {\n\t\tvar f *os.File\n\t\tf, cr.Error = os.Create(a.CommandSaveFile)\n\t\tif cr.Error != nil {\n\t\t\treturn cr\n\t\t}\n\n\t\tdefer f.Close()\n\n\t\t_, cr.Error = f.WriteString(cr.Result)\n\t\tif cr.Error != nil {\n\t\t\treturn cr\n\t\t}\n\t\tf.Sync()\n\t}\n\n\treturn cr\n}\n"
  },
  {
    "path": "pkg/parlay/plugin/plugin.go",
    "content": "package parlayplugin\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"plugin\"\n\n\t\"github.com/plunder-app/plunder/pkg/parlay/parlaytypes\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// The pluginCache contains a map of action->plugin\nvar pluginCache map[string]string\n\nfunc init() {\n\t// Initialise the map\n\tpluginCache = make(map[string]string)\n}\n\n// Find plugins returns an array of all .plugin files\nfunc findPlugins(pluginDir string) ([]string, error) {\n\tvar plugins []string\n\t// This function will look for all files in a specified directory (defaults to PWD/plugin)\n\tfilepath.Walk(pluginDir, func(path string, f os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !f.IsDir() {\n\t\t\tif filepath.Ext(path) == \".plugin\" {\n\t\t\t\tabsPath, _ := filepath.Abs(path)\n\n\t\t\t\tplugins = append(plugins, absPath)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\treturn plugins, nil\n}\n\nfunc findFunctionInPlugin(pluginPath, functionName string) (plugin.Symbol, error) {\n\n\tplug, err := plugin.Open(pluginPath)\n\tif err != nil {\n\t\tlog.Debugf(\"%v\", err)\n\t\treturn nil, fmt.Errorf(\"Unable to open Plugin [%s]\", pluginPath)\n\n\t}\n\n\tsymbol, err := plug.Lookup(functionName)\n\tif err != nil {\n\t\tlog.Debugf(\"%v\", err)\n\t\treturn nil, fmt.Errorf(\"Unable to read functions from Plugin [%s]\", pluginPath)\n\t}\n\n\treturn symbol, nil\n}\n\nfunc init() {\n\n\tpluginList, err := findPlugins(\"./plugin\")\n\tif err != nil {\n\t\tlog.Errorf(\"%v\", err)\n\t} else {\n\t\tlog.Debugf(\"Found [%d] plugins\", len(pluginList))\n\t\tfor x := range pluginList {\n\t\t\tsymbol, err := findFunctionInPlugin(pluginList[x], \"ParlayActionList\")\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"%v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tpluginExec, ok := symbol.(func() []string)\n\t\t\tif !ok {\n\t\t\t\tlog.Errorf(\"Unable to read functions from Plugin [%s]\", pluginList[x])\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tactions := pluginExec()\n\n\t\t\tfor z := range actions {\n\t\t\t\t// This will give us a mapping of \"action\" => plugin\n\t\t\t\tpluginCache[actions[z]] = pluginList[x]\n\t\t\t}\n\t\t}\n\t}\n}\n\n//ListPlugins -\nfunc ListPlugins() {\n\n\tpluginList, err := findPlugins(\"./plugin\")\n\tif err != nil {\n\t\tlog.Errorf(\"%v\", err)\n\t} else {\n\t\tlog.Debugf(\"Found [%d] plugins\", len(pluginList))\n\t\tfor x := range pluginList {\n\t\t\tsymbol, err := findFunctionInPlugin(pluginList[x], \"ParlayPluginInfo\")\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"%v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tpluginExec, ok := symbol.(func() string)\n\t\t\tif !ok {\n\t\t\t\tlog.Errorf(\"Unable to read functions from Plugin [%s]\", pluginList[x])\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tsanitizedPath := filepath.Base(pluginList[x])\n\t\t\tfmt.Printf(\"%s\\t%s\\n\", sanitizedPath, pluginExec())\n\t\t}\n\t}\n}\n\n//ListPluginActions -\nfunc ListPluginActions(pluginPath string) {\n\n\tsymbol, err := findFunctionInPlugin(pluginPath, \"ParlayActionList\")\n\tif err != nil {\n\t\tlog.Errorf(\"%v\", err)\n\t\treturn\n\t}\n\n\tpluginExec, ok := symbol.(func() []string)\n\tif !ok {\n\t\tlog.Errorf(\"Unable to read functions from Plugin [%s]\", pluginPath)\n\t\treturn\n\t}\n\n\tactions := pluginExec()\n\n\tsymbol, err = findFunctionInPlugin(pluginPath, \"ParlayActionDetails\")\n\tif err != nil {\n\t\tlog.Errorf(\"%v\", err)\n\t\treturn\n\t}\n\n\tpluginExec, ok = symbol.(func() []string)\n\tif !ok {\n\t\tlog.Errorf(\"Unable to read functions from Plugin [%s]\", pluginPath)\n\t\treturn\n\t}\n\n\tdescriptions := pluginExec()\n\n\tif len(actions) != len(descriptions) {\n\t\tlog.Warnf(\"Not all actions have descriptions, contact your plugin provider to have this fixed\")\n\t}\n\n\tfor x := range actions {\n\t\tfmt.Printf(\"%s\\t%s\\n\", actions[x], descriptions[x])\n\t}\n}\n\n//UsagePlugin returns the usage of a plugin function\nfunc UsagePlugin(pluginPath, action string) {\n\n\tsymbol, err := findFunctionInPlugin(pluginPath, \"ParlayUsage\")\n\tif err != nil {\n\t\tlog.Errorf(\"%v\", err)\n\t\treturn\n\t}\n\n\tpluginExec, ok := symbol.(func(string) (json.RawMessage, error))\n\tif !ok {\n\t\tlog.Errorf(\"Unable to read functions from Plugin [%s]\", pluginPath)\n\t\treturn\n\t}\n\tresult, err := pluginExec(action)\n\tif err != nil {\n\t\tlog.Errorf(\"%v\", err)\n\t\treturn\n\t}\n\n\ta := parlaytypes.Action{\n\t\tName:       fmt.Sprintf(\"Example name for action [%s]\", action),\n\t\tActionType: action,\n\t\tPlugin:     result,\n\t}\n\tb, _ := json.MarshalIndent(a, \"\", \"\\t\")\n\tfmt.Printf(\"%s\\n\", b)\n}\n\n// ExecuteAction uses the cache to find an action/plugin mapping\nfunc ExecuteAction(action, host string, raw json.RawMessage) ([]parlaytypes.Action, error) {\n\tif pluginCache[action] == \"\" {\n\t\t// No KeyMap meaning that the action doesn't map to a plugin\n\t\treturn nil, fmt.Errorf(\"Action [%s] does not exist or has no plugin associated with it\", action)\n\t}\n\treturn ExecuteActionInPlugin(pluginCache[action], action, host, raw)\n}\n\n// ExecuteActionInPlugin specifies the plugin and action directly\nfunc ExecuteActionInPlugin(pluginPath, action, host string, raw json.RawMessage) ([]parlaytypes.Action, error) {\n\n\t// Check a function with the name ParlayExec exists\n\tsymbol, err := findFunctionInPlugin(pluginPath, \"ParlayExec\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%v\", err)\n\t}\n\tlog.Debugf(\"Attempting plugin [%s]\", action)\n\t// Check the function has the correct parameters\n\tpluginExec, ok := symbol.(func(string, string, json.RawMessage) ([]parlaytypes.Action, error))\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"Unable to read functions from Plugin [%s]\", pluginPath)\n\t}\n\n\t// Pass the action type and the interface to the plugin\n\treturn pluginExec(action, host, raw)\n}\n"
  },
  {
    "path": "pkg/parlay/restore.go",
    "content": "package parlay\n\nimport (\n\t\"encoding/json\"\n\t\"io/ioutil\"\n\t\"os\"\n\n\t\"github.com/mitchellh/go-homedir\"\n)\n\n//Restore provides a checkpoint to resume from\ntype Restore struct {\n\tDeployment string   `json:\"deployment\"` // Name of deployment to restore from\n\tAction     string   `json:\"action\"`     // Action to restore from\n\tHost       string   `json:\"host\"`       // Single host to start from\n\tHosts      []string `json:\"hosts\"`      // Restart operation on a number of hosts\n}\n\n// restore is an interal struct used for execution restoration\nvar restore Restore\n\nconst restoreFile = \".parlay_restore\"\n\n// restoreFilePath will build a path where a file will be read/writted\nfunc restoreFilePath() (string, error) {\n\thome, err := homedir.Dir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home + \"/\" + restoreFile, nil\n}\n\nfunc (r *Restore) createCheckpoint() error {\n\t// This function will create a checkpoint file that will allow Plunder to restart in the event of failure\n\tpath, err := restoreFilePath()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Marshall the struct to a byte array\n\tb, err := json.Marshal(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// Write the checkpoint file\n\terr = ioutil.WriteFile(path, b, 0644)\n\n\treturn err\n}\n\n//RestoreFromCheckpoint will attempt to find a restoration checkpoint file\nfunc RestoreFromCheckpoint() *Restore {\n\tpath, err := restoreFilePath()\n\tif err != nil {\n\t\treturn nil\n\t}\n\tif _, err := os.Stat(path); !os.IsNotExist(err) {\n\t\tb, err := ioutil.ReadFile(path)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tvar r Restore\n\t\terr = json.Unmarshal(b, &r)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &r\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/parlay/validate.go",
    "content": "package parlay\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/plunder-app/plunder/pkg/parlay/parlaytypes\"\n)\n\n// ValidateAction will parse an action to ensure it is valid\nfunc ValidateAction(action *parlaytypes.Action) error {\n\tswitch action.ActionType {\n\tcase \"upload\":\n\t\t// Validate the upload action\n\t\tif action.Source == \"\" {\n\t\t\treturn fmt.Errorf(\"The Source field can not be blank\")\n\t\t}\n\n\t\tif action.Destination == \"\" {\n\t\t\treturn fmt.Errorf(\"The Destination field can not be blank\")\n\t\t}\n\t\treturn nil\n\tcase \"download\":\n\t\t// Validate the download action\n\t\tif action.Source == \"\" {\n\t\t\treturn fmt.Errorf(\"The Source field can not be blank\")\n\t\t}\n\n\t\tif action.Destination == \"\" {\n\t\t\treturn fmt.Errorf(\"The Destination field can not be blank\")\n\t\t}\n\t\treturn nil\n\tcase \"command\":\n\t\t// Validate the Command action\n\t\tif action.Command == \"\" && action.KeyName == \"\" {\n\t\t\treturn fmt.Errorf(\"Neither a command or a key has been specified to execute\")\n\t\t}\n\t\tif action.Command != \"\" && action.KeyName != \"\" {\n\t\t\treturn fmt.Errorf(\"Unable to use both a Command and a Command Key\")\n\t\t}\n\n\t\treturn nil\n\tcase \"pkg\":\n\t\t// Validate the Package action\n\t\tif action.PkgManager == \"\" {\n\t\t\treturn fmt.Errorf(\"The Package Manager field can not be blank\")\n\t\t} else if action.PkgManager != \"apt\" && action.PkgManager != \"yum\" {\n\t\t\treturn fmt.Errorf(\"Unknown Package Manager [%s]\", action.PkgManager)\n\t\t}\n\n\t\tif action.PkgOperation == \"\" {\n\t\t\treturn fmt.Errorf(\"The Package Operation field can not be blank\")\n\t\t} else if action.PkgOperation != \"install\" && action.PkgOperation != \"remove\" {\n\t\t\treturn fmt.Errorf(\"Unknown Package Operation [%s]\", action.PkgOperation)\n\t\t}\n\n\t\tif action.Packages == \"\" {\n\t\t\treturn fmt.Errorf(\"The Packages field can not be blank\")\n\t\t}\n\t\treturn nil\n\tcase \"key\":\n\t\t// Validate the Key action\n\t\tif action.KeyFile == \"\" {\n\t\t\treturn fmt.Errorf(\"The KeyField field can not be blank\")\n\t\t}\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"Unknown Action [%s]\", action.ActionType)\n\t}\n}\n"
  },
  {
    "path": "pkg/plunderlogging/consolelogger.go",
    "content": "package plunderlogging\n"
  },
  {
    "path": "pkg/plunderlogging/filelogger.go",
    "content": "package plunderlogging\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n)\n\n// FileLogger allows parlay to log output to a file on the local filesystem\ntype FileLogger struct {\n\tenabled bool\n\tf       *os.File\n}\n\nvar fileLogging FileLogger\n\nfunc (l *FileLogger) initFileLogger(logFile string) (err error) {\n\tl.enabled = true\n\tl.f, err = os.Create(logFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// This file based logging function may error, but logging should never break the running of a system, so errors are passed to \"Debug\" logging\nfunc (l *FileLogger) writeEntry(target, entry string) error {\n\tvar fileMutex sync.Mutex\n\tif l.enabled == true {\n\n\t\t// As this may be called by numerous goroutines, we impose a mutex lock on it\n\t\tfileMutex.Lock()\n\t\tdefer fileMutex.Unlock()\n\n\t\t// TODO - Does this produce readable logging output\n\t\t_, err := l.f.WriteString(fmt.Sprintf(\"Target=%s Entry=%s\", target, entry))\n\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (l *FileLogger) setLoggingState(target, state string) error {\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/plunderlogging/go.mod",
    "content": "module github.com/plunder-app/plunder/pkg/plunderlogging\n\ngo 1.12\n"
  },
  {
    "path": "pkg/plunderlogging/jsonlogger.go",
    "content": "package plunderlogging\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// JSONLogger allows parlay to log output to an in-memory jsonStruct\ntype JSONLogger struct {\n\tenabled bool\n\tlogger  map[string]*JSONLog\n}\n\n// JSONLog contains all of the output from a parlay execution\ntype JSONLog struct {\n\tState   string         `json:\"state\"`\n\tEntries []JSONLogEntry `json:\"entries\"`\n}\n\n// JSONLogEntry contains the details a specific action\ntype JSONLogEntry struct {\n\tCreated  time.Time `json:\"created\"`\n\tTaskName string    `json:\"task\"`\n\tErr      string    `json:\"error\"`\n\tEntry    string    `json:\"entry\"`\n}\n\nfunc (j *JSONLogger) initJSONLogger() {\n\tj.enabled = true\n\tj.logger = make(map[string]*JSONLog)\n}\n\nfunc (j *JSONLogger) writeEntry(target, task, entry, err string) {\n\t// Create new entry\n\tnewEntry := JSONLogEntry{\n\t\tCreated:  time.Now(),\n\t\tEntry:    entry,\n\t\tTaskName: task,\n\t\tErr:      err,\n\t}\n\n\t// Check if the logger exists\n\texistingLog, ok := j.logger[target]\n\tif ok {\n\t\t// Update an existing entry\n\t\texistingLog.Entries = append(existingLog.Entries, newEntry)\n\t} else {\n\t\t// Create a new logger\n\t\tnewLog := JSONLog{\n\t\t\tState: \"Running\",\n\t\t}\n\t\t// Append the entry to it\n\t\tnewLog.Entries = append(newLog.Entries, newEntry)\n\t\t// Update the in-memory log store\n\t\tj.logger[target] = &newLog\n\t\tlog.Debugf(\"Creating new logs for target [%s]\", target)\n\n\t}\n}\n\nfunc (j *JSONLogger) deleteLog(target string) error {\n\t// Check if the entry exists\n\t_, ok := j.logger[target]\n\tif ok {\n\t\t// If it does, then we use the in-built function to delete the log entry\n\t\tdelete(j.logger, target)\n\t} else {\n\t\t// Return a warning\n\t\treturn fmt.Errorf(\"In-Memory logging for [%s] either doesn't exist or has already been deleted\", target)\n\t}\n\treturn nil\n}\n\nfunc (j *JSONLogger) setLoggingState(target, state string) error {\n\t// Check if the logger exists\n\texistingLog, ok := j.logger[target]\n\tif ok {\n\t\t// Update an existing entry\n\t\texistingLog.State = state\n\t} else {\n\t\treturn fmt.Errorf(\"In-Memory logging for [%s] either doesn't exist or has already been deleted\", target)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/plunderlogging/logger.go",
    "content": "package plunderlogging\n\nimport \"fmt\"\n\n// Logger - is a stuct that manages the verious types of logger available\ntype Logger struct {\n\tjson JSONLogger\n\tfile FileLogger\n}\n\n// EnableJSONLogging - will enable logging through JSON\nfunc (l *Logger) EnableJSONLogging(e bool) {\n\tl.json.enabled = e\n\tl.json.initJSONLogger()\n}\n\n// EnableFileLogging - will enable logging to a file\nfunc (l *Logger) EnableFileLogging(e bool) {\n\tl.file.enabled = e\n}\n\n// InitLogFile - will initialise file based logging\nfunc (l *Logger) InitLogFile(path string) error {\n\tif l.file.enabled != true {\n\t\treturn l.file.initFileLogger(path)\n\t}\n\t// Dont re-initialise the file\n\treturn nil\n\n}\n\n// InitJSON - will start/initialise the JSON logging functionality\nfunc (l *Logger) InitJSON() {\n\t// Dont re-initialise the json\n\n\tif l.json.enabled != true {\n\t\tl.json.initJSONLogger()\n\t}\n\n}\n\n// target - the entity we're affecting\n// entry - the results of the operation on the target\n\n// WriteLogEntry will capture what is transpiring and where\nfunc (l *Logger) WriteLogEntry(target, task, entry, err string) {\n\tif l.file.enabled {\n\t\tl.file.writeEntry(target, entry)\n\t}\n\tif l.json.enabled {\n\t\tl.json.writeEntry(target, task, entry, err)\n\t}\n\n\t// A logging system shouldnt break anything so any errors are just outputed to STDOUT\n\n}\n\n// SetLoggingState - currently a NOOP (TODO)\nfunc (l *Logger) SetLoggingState(target, state string) {\n\tif l.file.enabled {\n\t\tl.file.setLoggingState(target, state)\n\t}\n\tif l.json.enabled {\n\t\tl.json.setLoggingState(target, state)\n\t}\n\n\t// A logging system shouldnt break anything so any errors are just outputed to STDOUT\n\n}\n\n// GetJSONLogs - returns a pointer to the current JSON Logs\nfunc (l *Logger) GetJSONLogs(target string) (*JSONLog, error) {\n\tif l.json.logger == nil {\n\t\treturn nil, fmt.Errorf(\"JSON Logging hasn't been enabled\")\n\t}\n\t// Check if the logger exists\n\texistingLog, ok := l.json.logger[target]\n\tif ok {\n\t\treturn existingLog, nil\n\t}\n\treturn nil, fmt.Errorf(\"No Logs for Target [%s] exist\", target)\n}\n\n// DeleteLogs - will remove logs for a particular target\nfunc (l *Logger) DeleteLogs(target string) error {\n\tif l.json.logger == nil {\n\t\treturn nil\n\t}\n\treturn l.json.deleteLog(target)\n\n}\n"
  },
  {
    "path": "pkg/services/deployments.go",
    "content": "package services\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/plunder-app/plunder/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// DefaultBootType specifies what a server will default to if no config is found\nvar DefaultBootType string\n\n// This stores the mapping for a url to the data /macaddress.file => data\nvar httpPaths map[string]string\n\nfunc init() {\n\t// Initialise the paths map\n\thttpPaths = make(map[string]string)\n}\n\n// rebuildConfiguration - will parse the entire deployment configuration and update anything that is missing\nfunc rebuildConfiguration(updateConfig *DeploymentConfigurationFile) error {\n\n\t// If HTTP isn't enabled we can't build the multiplexer for URLs\n\tif serveMux == nil {\n\t\treturn fmt.Errorf(\"Deployment HTTP Server isn't enabled, so parsing deployments isn't possible\")\n\t}\n\n\t// If a key is specified then we read it and base64 the file into the SSHKEY string\n\tif updateConfig.GlobalServerConfig.SSHKeyPath != \"\" {\n\t\terr := updateConfig.GlobalServerConfig.parseSSH()\n\t\tif err != nil {\n\t\t\tlog.Errorf(err.Error())\n\t\t}\n\t}\n\n\tlog.Debugf(\"Parsing [%d] Configurations\", len(updateConfig.Configs))\n\tfor i := range updateConfig.Configs {\n\n\t\t// inMemipxeConfig is a custom configuration that matches kernel/initrd & cmdline and is 00:11:22:33:44:55.ipxe\n\t\tvar inMemipxeConfig string\n\n\t\t// inMemipxeConfig is a custom configuration that is specific to the boot type [preseed/kickstart/vsphere] and is 00:11:22:33:44:55.cfg\n\t\tvar inMemBootConfig string\n\n\t\t// imMemESXiKickstart is a custom configuration specific to vSphere for it's kickstart\n\t\tvar imMemESXiKickstart string\n\n\t\t// inMemBOOTyConfig is a custom configuration that matches kernel/initrd & cmdline and is 00:11:22:33:44:55.bty\n\t\tvar inMemBOOTyConfig string\n\n\t\t// We need to move all \":\" to \"-\" to make life a little easier for filesystems and internet standards\n\t\tdashMac := strings.Replace(updateConfig.Configs[i].MAC, \":\", \"-\", -1)\n\n\t\t// Find the deployment configuration for this host, either custom or inherit from the controller\n\t\tbootConfig := findBootConfigForDeployment(updateConfig.Configs[i])\n\n\t\t// If there is no deployment configuration under this name return an error\n\t\tif bootConfig == nil {\n\t\t\terrorString := fmt.Errorf(\"Host [%s] uses unknown config [%s], stopping config update\", updateConfig.Configs[i].MAC, updateConfig.Configs[i].ConfigName)\n\t\t\tlog.Errorln(errorString)\n\t\t\treturn errorString\n\t\t}\n\n\t\t// Ensure this entry has the correct mapping\n\t\tupdateConfig.Configs[i].ConfigBoot = *bootConfig\n\n\t\t// This will populate anything missing from the global configuration\n\t\tupdateConfig.Configs[i].ConfigHost.PopulateFromGlobalConfiguration(updateConfig.GlobalServerConfig)\n\n\t\t// If a key is specified then we read it and base64 the file into the SSHKEY string\n\t\tif updateConfig.Configs[i].ConfigHost.SSHKeyPath != \"\" {\n\t\t\terr := updateConfig.Configs[i].ConfigHost.parseSSH()\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(err.Error())\n\t\t\t}\n\t\t} else {\n\t\t\tlog.Errorf(\"This server [%s] will be deployed with no SSH Key\", updateConfig.Configs[i].ConfigHost.ServerName)\n\t\t}\n\n\t\t// Look for understood config types\n\t\tswitch updateConfig.Configs[i].ConfigBoot.ConfigType {\n\t\tcase \"preseed\":\n\t\t\tinMemipxeConfig = utils.IPXEPreeseed(httpAddress, bootConfig.Kernel, bootConfig.Initrd, bootConfig.Cmdline)\n\t\t\tlog.Debugf(\"Generating preseed ipxeConfig for configName [%s]\", dashMac)\n\t\t\tinMemBootConfig = updateConfig.Configs[i].ConfigHost.BuildPreeSeedConfig()\n\n\t\tcase \"kickstart\":\n\t\t\tinMemipxeConfig = utils.IPXEKickstart(httpAddress, bootConfig.Kernel, bootConfig.Initrd, bootConfig.Cmdline)\n\t\t\tlog.Debugf(\"Generating kickstart ipxeConfig for configName [%s]\", dashMac)\n\t\t\tinMemBootConfig = updateConfig.Configs[i].ConfigHost.BuildKickStartConfig()\n\n\t\tcase \"vsphere\":\n\t\t\tinMemipxeConfig = utils.IPXEVSphere(httpAddress, bootConfig.Kernel, bootConfig.Cmdline)\n\t\t\tlog.Debugf(\"Generating vsphere ipxeConfig for configName [%s]\", dashMac)\n\t\t\tinMemBootConfig = updateConfig.Configs[i].ConfigHost.BuildESXiConfig()\n\t\t\timMemESXiKickstart = updateConfig.Configs[i].ConfigHost.BuildESXiKickStart()\n\n\t\tcase \"booty\":\n\t\t\tinMemipxeConfig = utils.IPXEBOOTy(httpAddress, bootConfig.Kernel, bootConfig.Initrd, bootConfig.Cmdline)\n\t\t\tlog.Debugf(\"Generating booty ipxeConfig for configName [%s]\", dashMac)\n\t\t\tinMemBOOTyConfig = updateConfig.Configs[i].ConfigHost.BuildBOOTYconfig()\n\n\t\tdefault:\n\t\t\tlog.Debugf(\"Generating default ipxeConfig for configName [%s]\", updateConfig.Configs[i].ConfigBoot.ConfigName)\n\t\t\tinMemipxeConfig = utils.IPXEAnyBoot(httpAddress, bootConfig.Kernel, bootConfig.Initrd, bootConfig.Cmdline)\n\t\t}\n\n\t\t// Build the configuration that is passed to iPXE on boot\n\t\tif inMemipxeConfig != \"\" {\n\t\t\tpath := fmt.Sprintf(\"/%s.ipxe\", dashMac)\n\t\t\tif _, ok := httpPaths[path]; !ok {\n\t\t\t\t// Only create the handler if one doesn't exist\n\t\t\t\tserveMux.HandleFunc(path, rootHandler)\n\t\t\t}\n\n\t\t\thttpPaths[path] = inMemipxeConfig\n\t\t}\n\n\t\t// Build a boot configuration that is passed to a kernel\n\t\tif inMemBootConfig != \"\" {\n\t\t\tpath := fmt.Sprintf(\"/%s.cfg\", dashMac)\n\t\t\tif _, ok := httpPaths[path]; !ok {\n\t\t\t\t// Only create the handler if one doesn't exist\n\t\t\t\tserveMux.HandleFunc(path, rootHandler)\n\t\t\t}\n\t\t\thttpPaths[path] = inMemBootConfig\n\t\t}\n\n\t\t// Build a vSphere kickstart configuration that is passed to an installer\n\t\tif imMemESXiKickstart != \"\" {\n\t\t\tpath := fmt.Sprintf(\"/%s.ks\", dashMac)\n\t\t\tif _, ok := httpPaths[path]; !ok {\n\t\t\t\t// Only create the handler if one doesn't exist\n\t\t\t\tserveMux.HandleFunc(path, rootHandler)\n\t\t\t}\n\t\t\thttpPaths[path] = imMemESXiKickstart\n\t\t}\n\n\t\t// Build a BOOTy configuration that is passed to an installer\n\t\tif inMemBOOTyConfig != \"\" {\n\t\t\tpath := fmt.Sprintf(\"/%s.bty\", dashMac)\n\t\t\tif _, ok := httpPaths[path]; !ok {\n\t\t\t\t// Only create the handler if one doesn't exist\n\t\t\t\tserveMux.HandleFunc(path, rootHandler)\n\t\t\t}\n\t\t\thttpPaths[path] = inMemBOOTyConfig\n\t\t}\n\n\t}\n\tif len(updateConfig.Configs) == 0 {\n\t\t// No changes, leave as is (with a warning)\n\t\tlog.Warnln(\"No deployment configuration, any existing configuration will remain\")\n\t} else {\n\t\t// Updated configuration has been parsed, update internal deployment configuration\n\t\tlog.Infoln(\"Updating of deployment configuration complete\")\n\t\tDeployments = *updateConfig\n\t}\n\n\treturn nil\n}\n\n// UpdateDeploymentConfig will read a configuration string and build the iPXE files needed\nfunc UpdateDeploymentConfig(rawDeploymentConfig []byte) error {\n\t// Read through the deployment configuration\n\tlog.Infoln(\"Updating the Deployment Configuration\")\n\t// Work out if it is a YAML/JSON or unknown\n\tupdateConfig, err := ParseDeployment(rawDeploymentConfig)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn rebuildConfiguration(updateConfig)\n\n}\n\n// AddDeployment - This function will add a new deployment to the deployment configuration\nfunc AddDeployment(rawDeployment []byte) error {\n\n\tvar newDeployment DeploymentConfig\n\n\terr := json.Unmarshal(rawDeployment, &newDeployment)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Unable to parse deployment configuration\")\n\t}\n\t// Find the original deployment via it's mac address\n\tfor i := range Deployments.Configs {\n\t\t// Compare this deployment to the one we're looking for\n\t\tif Deployments.Configs[i].MAC == newDeployment.MAC {\n\t\t\treturn fmt.Errorf(\"Duplicate entry for MAC address [%s]\", newDeployment.MAC)\n\t\t}\n\t}\n\t// We will now duplicate our configuration\n\tupdateConfig := Deployments\n\t// We will need to create space to copy the existing configurations over\n\tupdateConfig.Configs = make([]DeploymentConfig, len(Deployments.Configs))\n\t// Copy our existing configurations into the new configuration\n\tcopy(updateConfig.Configs, Deployments.Configs)\n\t// Append our new configuration into our new copy\n\tupdateConfig.Configs = append(updateConfig.Configs, newDeployment)\n\n\t// Remove the deployment from the unleased addresses\n\tcontroller.DelUnLeased(newDeployment.MAC)\n\n\t// Parse the new configuration\n\treturn rebuildConfiguration(&updateConfig)\n}\n\n// GetDeployment - This function will add a new deployment to the deployment configuration\nfunc GetDeployment(macAddress string) *DeploymentConfig {\n\t// Iterate through all the deployments\n\tfor i := range Deployments.Configs {\n\t\tif macAddress == Deployments.Configs[i].MAC {\n\t\t\treturn &Deployments.Configs[i]\n\t\t}\n\t}\n\treturn nil\n}\n\n// UpdateDeployment - This function will add a new deployment to the deployment configuration\nfunc UpdateDeployment(macAddress string, rawDeployment []byte) error {\n\n\tvar newDeployment DeploymentConfig\n\n\terr := json.Unmarshal(rawDeployment, &newDeployment)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Unable to parse deployment configuration\")\n\t}\n\n\t// if no ID or specific MAC address was passed then assume the mac address of the deployment\n\tif macAddress == \"\" {\n\t\tmacAddress = newDeployment.MAC\n\t}\n\n\t// We will now duplicate our configuration\n\tupdateConfig := Deployments\n\t// We will need to create space to copy the existing configurations over\n\tupdateConfig.Configs = make([]DeploymentConfig, len(Deployments.Configs))\n\t// Copy our existing configurations into the new configuration\n\tcopy(updateConfig.Configs, Deployments.Configs)\n\n\t// Find the original deployment via it's mac address\n\tfor i := range updateConfig.Configs {\n\t\t// Compare this deployment to the one we're looking for\n\t\tif updateConfig.Configs[i].MAC == macAddress {\n\t\t\t// Remove the old matching configuration\n\t\t\tupdateConfig.Configs = append(updateConfig.Configs[:i], updateConfig.Configs[i+1:]...)\n\t\t\t// Append our new configuration into our new copy\n\t\t\tupdateConfig.Configs = append(updateConfig.Configs, newDeployment)\n\n\t\t\t// Parse the new configuration\n\t\t\treturn rebuildConfiguration(&updateConfig)\n\t\t}\n\t}\n\treturn fmt.Errorf(\"Unable to find existing deployment for MAC address [%s]\", macAddress)\n}\n\n// DeleteDeploymentMac - This function will delete a deployment based upon it's mac Address\nfunc DeleteDeploymentMac(macAddress string, rawDeployment []byte) error {\n\n\t// We will now duplicate our configuration\n\tupdateConfig := Deployments\n\t// We will need to create space to copy the existing configurations over\n\tupdateConfig.Configs = make([]DeploymentConfig, len(Deployments.Configs))\n\t// Copy our existing configurations into the new configuration\n\tcopy(updateConfig.Configs, Deployments.Configs)\n\n\t// Find the original deployment via it's mac address\n\tfor i := range updateConfig.Configs {\n\t\t// Compare this deployment to the one we're looking for\n\t\tif updateConfig.Configs[i].MAC == macAddress {\n\n\t\t\t// Remove http Handler (if it exists)\n\t\t\t_, ok := httpPaths[fmt.Sprintf(\"%s.ipxe\", updateConfig.Configs[i].MAC)]\n\t\t\tif ok {\n\t\t\t\tdelete(httpPaths, fmt.Sprintf(\"%s.ipxe\", updateConfig.Configs[i].MAC))\n\t\t\t}\n\n\t\t\t// Remove the old matching configuration\n\t\t\tupdateConfig.Configs = append(updateConfig.Configs[:i], updateConfig.Configs[i+1:]...)\n\t\t\t// Parse the new configuration\n\t\t\treturn rebuildConfiguration(&updateConfig)\n\t\t}\n\t}\n\treturn fmt.Errorf(\"Unable to find existing deployment for Address [%s]\", macAddress)\n\n}\n\n// DeleteDeploymentAddress - This function will delete a deployment based upon it's IP Address\nfunc DeleteDeploymentAddress(address string, rawDeployment []byte) error {\n\n\t// We will now duplicate our configuration\n\tupdateConfig := Deployments\n\t// We will need to create space to copy the existing configurations over\n\tupdateConfig.Configs = make([]DeploymentConfig, len(Deployments.Configs))\n\t// Copy our existing configurations into the new configuration\n\tcopy(updateConfig.Configs, Deployments.Configs)\n\n\t// Find the original deployment via it's mac address\n\tfor i := range updateConfig.Configs {\n\t\t// Compare this deployment to the one we're looking for\n\t\tif updateConfig.Configs[i].ConfigHost.IPAddress == address {\n\n\t\t\t// Remove http Handler (if it exists)\n\t\t\t_, ok := httpPaths[fmt.Sprintf(\"%s.ipxe\", updateConfig.Configs[i].MAC)]\n\t\t\tif ok {\n\t\t\t\tdelete(httpPaths, fmt.Sprintf(\"%s.ipxe\", updateConfig.Configs[i].MAC))\n\t\t\t}\n\n\t\t\t// Remove the old matching configuration\n\t\t\tupdateConfig.Configs = append(updateConfig.Configs[:i], updateConfig.Configs[i+1:]...)\n\t\t\t// Parse the new configuration\n\t\t\treturn rebuildConfiguration(&updateConfig)\n\t\t}\n\t}\n\treturn fmt.Errorf(\"Unable to find existing deployment for Address [%s]\", address)\n\n}\n\n// UpdateGlobalDeploymentConfig - This allows updating of the global configuration independently\nfunc UpdateGlobalDeploymentConfig(rawDeployment []byte) error {\n\tvar globalDeploymentConfig HostConfig\n\terr := json.Unmarshal(rawDeployment, &globalDeploymentConfig)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Unable to parse deployment configuration\")\n\t}\n\t// Update the deployments with the new configuration\n\tDeployments.GlobalServerConfig = globalDeploymentConfig\n\treturn nil\n}\n\n//FindDeploymentConfigFromMac - this will return the deployment configuration, allowing the DHCP server to return the correct DHCP options\nfunc FindDeploymentConfigFromMac(mac string) string {\n\n\t// AnyBoot will just boot the specified kernel/initrd\n\t// if AnyBoot == true {\n\t// \treturn \"anyboot\"\n\t// }\n\n\tif len(Deployments.Configs) == 0 {\n\t\t// No configurations have been loaded\n\t\tlog.Warnln(\"Attempted to perform Mac Address lookup, however no configurations have been loaded\")\n\t\treturn \"\"\n\t}\n\tfor i := range Deployments.Configs {\n\t\tlog.Debugf(\"Comparing [%s] to [%s]\", mac, strings.ToLower(Deployments.Configs[i].MAC))\n\t\tif mac == strings.ToLower(Deployments.Configs[i].MAC) {\n\t\t\treturn Deployments.Configs[i].ConfigName\n\t\t}\n\t}\n\treturn DefaultBootType\n}\n"
  },
  {
    "path": "pkg/services/go.mod",
    "content": "module github.com/plunder-app/plunder/pkg/services\n\ngo 1.12\n"
  },
  {
    "path": "pkg/services/handler.go",
    "content": "package services\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/plunder-app/plunder/pkg/apiserver\"\n)\n\n// RegisterToAPIServer - will add the endpoints to the API server\nfunc RegisterToAPIServer() {\n\n\t// ------------------------------------------------\n\t//        Server configuration API registration\n\t// ------------------------------------------------\n\n\tapiserver.AddDynamicEndpoint(\"/config\",\n\t\t\"/config\",\n\t\t\"Allows the retrieving of Plunder Server configuration\",\n\t\t\"config\",\n\t\thttp.MethodGet,\n\t\tgetConfig)\n\n\tapiserver.AddDynamicEndpoint(\"/config\",\n\t\t\"/config\",\n\t\t\"Allows the creation of Plunder Server configuration\",\n\t\t\"config\",\n\t\thttp.MethodPost,\n\t\tpostConfig)\n\n\tapiserver.AddDynamicEndpoint(\"/config/boot/{id}\",\n\t\t\"/config/boot\",\n\t\t\"Allows the creation of Plunder Server Boot configuration\",\n\t\t\"configBoot\",\n\t\thttp.MethodPost,\n\t\tpostBootConfig)\n\n\tapiserver.AddDynamicEndpoint(\"/config/boot/{id}\",\n\t\t\"/config/boot\",\n\t\t\"Performs the deletion of Plunder Server Boot configuration\",\n\t\t\"configBoot\",\n\t\thttp.MethodDelete,\n\t\tdeleteBootConfig)\n\n\t// ------------------------------------------------\n\t//    DHCP configuration API registration\n\t// ------------------------------------------------\n\n\tapiserver.AddDynamicEndpoint(\"/dhcp/{id}\",\n\t\t\"/dhcp\",\n\t\t\"Allows the retrieval of DHCP information\",\n\t\t\"dhcp\",\n\t\thttp.MethodGet,\n\t\tgetDHCP)\n\n\t// ------------------------------------------------\n\t//    Deployment configuration API registration\n\t// ------------------------------------------------\n\n\tapiserver.AddDynamicEndpoint(\"/deployments\",\n\t\t\"/deployments\",\n\t\t\"Allows the retrieving of Plunder Server deployments\",\n\t\t\"deployments\",\n\t\thttp.MethodGet,\n\t\tgetDeployments)\n\n\tapiserver.AddDynamicEndpoint(\"/deployments\",\n\t\t\"/deployments\",\n\t\t\"Allows the creation of Plunder Server deployments\",\n\t\t\"deployments\",\n\t\thttp.MethodPost,\n\t\tpostDeployments)\n\n\tapiserver.AddDynamicEndpoint(\"/deployment\",\n\t\t\"/deployment\",\n\t\t\"Allows the creation of a specific Plunder deployment\",\n\t\t\"deployment\",\n\t\thttp.MethodPost,\n\t\tpostDeployment)\n\n\tapiserver.AddDynamicEndpoint(\"/deployment\",\n\t\t\"/deployment\",\n\t\t\"Allows the patching of a Plunder Server deployment\",\n\t\t\"deployment\",\n\t\thttp.MethodPatch,\n\t\tupdateDeployment)\n\n\tapiserver.AddDynamicEndpoint(\"/deployment/{id}\",\n\t\t\"/deployment\",\n\t\t\"Allows the retrieval of specific information about a deployment\",\n\t\t\"deploymentID\",\n\t\thttp.MethodGet,\n\t\tgetSpecificDeployment)\n\n\tapiserver.AddDynamicEndpoint(\"/deployment/{id}\",\n\t\t\"/deployment\",\n\t\t\"Allows the patching of Plunder Server deployments\",\n\t\t\"deploymentID\",\n\t\thttp.MethodPatch,\n\t\tupdateDeployment)\n\n\tapiserver.AddDynamicEndpoint(\"/deployment/{id}\",\n\t\t\"/deployment\",\n\t\t\"Allows the deletion of a Plunder Server deployment\",\n\t\t\"deploymentID\",\n\t\thttp.MethodDelete,\n\t\tdeleteDeployment)\n\n\tapiserver.AddDynamicEndpoint(\"/deployment/mac/{id}\",\n\t\t\"/deployment/mac\",\n\t\t\"Allows the deletion of a Plunder Server deployment based upon its MAC address\",\n\t\t\"deploymentMac\",\n\t\thttp.MethodDelete,\n\t\tdeleteDeploymentMac)\n\n\tapiserver.AddDynamicEndpoint(\"/deployment/address/{id}\",\n\t\t\"/deployment/address\",\n\t\t\"Allows the deletion of a Plunder Server deployment based upon its network address\",\n\t\t\"deploymentAddress\",\n\t\thttp.MethodDelete,\n\t\tdeleteDeploymentAddress)\n\n}\n\nfunc getConfig(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tvar rsp apiserver.Response\n\tjsonData, err := json.Marshal(Controller)\n\tif err != nil {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\trsp.Warning = \"Error retrieving Server Configuration\"\n\t\trsp.Error = err.Error()\n\t} else {\n\t\trsp.Payload = jsonData\n\t}\n\tjson.NewEncoder(w).Encode(rsp)\n}\n\n// Apply the plunder server global configuration\n\nfunc postConfig(w http.ResponseWriter, r *http.Request) {\n\tif b, err := ioutil.ReadAll(r.Body); err == nil {\n\t\tvar rsp apiserver.Response\n\t\t// This function needs to parse both the data and then evaluate the state of running services\n\t\terr := ParseControllerData(b)\n\n\t\tif err != nil {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\trsp.Warning = \"Error updating Server Configuration\"\n\t\t\trsp.Error = err.Error()\n\t\t}\n\t\tjson.NewEncoder(w).Encode(rsp)\n\t\tController.StartServices(nil)\n\t}\n}\n\n// Apply a Specific Boot Configuration\nfunc postBootConfig(w http.ResponseWriter, r *http.Request) {\n\tif b, err := ioutil.ReadAll(r.Body); err == nil {\n\t\tvar rsp apiserver.Response\n\t\t// This function needs to parse both the data and then evaluate the state of running services\n\t\tvar newBoot BootConfig\n\t\terr := json.Unmarshal(b, &newBoot)\n\t\tif err != nil {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\trsp.Warning = \"Error updating Server Configuration\"\n\t\t\trsp.Error = err.Error()\n\n\t\t} else {\n\t\t\tfor x := range Controller.BootConfigs {\n\t\t\t\tif Controller.BootConfigs[x].ConfigName == newBoot.ConfigName {\n\t\t\t\t\t// Found a duplicate\n\t\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\t\trsp.Warning = \"Error duplicate Server Configuration\"\n\t\t\t\t\trsp.Error = fmt.Sprintf(\"Boot Configuration [%s] already exists\", Controller.BootConfigs[x].ConfigName)\n\t\t\t\t\tjson.NewEncoder(w).Encode(rsp)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t// // Parse the boot configuration (preload ISOs etc.)\n\t\t\terr = newBoot.Parse()\n\t\t\t// err = Controller.ParseBootController()\n\t\t\tif err != nil {\n\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\trsp.Warning = \"Error updating Server Configuration\"\n\t\t\t\trsp.Error = err.Error()\n\t\t\t} else {\n\t\t\t\t// Add the Boot configuration to the controller\n\t\t\t\tController.BootConfigs = append(Controller.BootConfigs, newBoot)\n\t\t\t\t// Generate the handlers (this can probably GO soon)\n\t\t\t\tController.generateBootTypeHanders()\n\t\t\t}\n\t\t\t// // Parse the boot configuration (preload ISOs etc.)\n\t\t\t// err = Controller.ParseBootController()\n\t\t\t// if err != nil {\n\t\t\t// \tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t// \trsp.Warning = \"Error updating Server Configuration\"\n\t\t\t// \trsp.Error = err.Error()\n\t\t\t// }\n\t\t}\n\n\t\tjson.NewEncoder(w).Encode(rsp)\n\t}\n}\n\n// Apply a Specific Boot Configuration\nfunc deleteBootConfig(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t// Find the deployment ID\n\tid := mux.Vars(r)[\"id\"]\n\tvar rsp apiserver.Response\n\n\t// We need to revert the mac address back to the correct format (dashes back to colons)\n\terr := Controller.DeleteBootControllerConfig(id)\n\tif err != nil {\n\n\t\tif err != nil {\n\t\t\trsp.Warning = \"Error updating Deployment Configuration\"\n\t\t\trsp.Error = err.Error()\n\t\t\trsp.Payload = nil\n\t\t}\n\t}\n\n\tjson.NewEncoder(w).Encode(rsp)\n}\n\nfunc getDeployments(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tvar rsp apiserver.Response\n\tjsonData, err := json.Marshal(Deployments)\n\tif err != nil {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\trsp.Warning = \"Error retrieving deployment Configuration\"\n\t\trsp.Error = err.Error()\n\t} else {\n\t\trsp.Payload = jsonData\n\t}\n\tjson.NewEncoder(w).Encode(rsp)\n}\n\n// Apply the plunder global deployment configuration\n\nfunc postDeployments(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tif b, err := ioutil.ReadAll(r.Body); err == nil {\n\t\terr := UpdateDeploymentConfig(b)\n\t\tvar rsp apiserver.Response\n\n\t\tif err != nil {\n\t\t\trsp.Warning = \"Error updating Deployment Configuration\"\n\t\t\trsp.Error = err.Error()\n\t\t\trsp.Payload = nil\n\t\t}\n\t\tjson.NewEncoder(w).Encode(rsp)\n\t}\n}\n\n// Retrieve a specific plunder deployment configuration\n\nfunc getSpecificDeployment(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tvar rsp apiserver.Response\n\t// Find the deployment ID\n\tid := mux.Vars(r)[\"id\"]\n\t// We need to revert the mac address back to the correct format (dashes back to colons)\n\tmac := strings.Replace(id, \"-\", \":\", -1)\n\n\tdeployment := GetDeployment(mac)\n\n\tif deployment != nil {\n\t\tjsonData, err := json.Marshal(deployment)\n\t\tif err != nil {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\trsp.Warning = \"Error retrieving deployment Configuration\"\n\t\t\trsp.Error = err.Error()\n\t\t} else {\n\t\t\trsp.Payload = jsonData\n\t\t}\n\n\t} else {\n\t\trsp.Error = fmt.Sprintf(\"Unable to find %s\", mac)\n\t}\n\n\tjson.NewEncoder(w).Encode(rsp)\n\n}\n\n// Retrieve a specific plunder deployment configuration\nfunc postDeployment(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tif b, err := ioutil.ReadAll(r.Body); err == nil {\n\t\terr := AddDeployment(b)\n\t\tvar rsp apiserver.Response\n\n\t\tif err != nil {\n\t\t\trsp.Warning = \"Error updating Deployment Configuration\"\n\t\t\trsp.Error = err.Error()\n\t\t\trsp.Payload = nil\n\t\t}\n\t\tjson.NewEncoder(w).Encode(rsp)\n\t}\n\n}\n\n// Retrieve a specific plunder deployment configuration\nfunc updateDeployment(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tvar rsp apiserver.Response\n\n\t// Find the deployment ID\n\tid := mux.Vars(r)[\"id\"]\n\n\t// Are we updating the deployment \"global\"\n\tif id == \"global\" {\n\t\tif b, err := ioutil.ReadAll(r.Body); err == nil {\n\t\t\terr := UpdateGlobalDeploymentConfig(b)\n\n\t\t\tif err != nil {\n\t\t\t\trsp.Warning = \"Error updating Global Configuration\"\n\t\t\t\trsp.Error = err.Error()\n\t\t\t\trsp.Payload = nil\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// We need to revert the mac address back to the correct format (dashes back to colons)\n\t\tmac := strings.Replace(id, \"-\", \":\", -1)\n\n\t\tif b, err := ioutil.ReadAll(r.Body); err == nil {\n\t\t\terr := UpdateDeployment(mac, b)\n\n\t\t\tif err != nil {\n\t\t\t\trsp.Warning = \"Error updating Deployment Configuration\"\n\t\t\t\trsp.Error = err.Error()\n\t\t\t\trsp.Payload = nil\n\t\t\t}\n\t\t}\n\t}\n\tjson.NewEncoder(w).Encode(rsp)\n}\n\n// Retrieve a specific plunder deployment configuration\nfunc deleteDeployment(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t// Find the deployment ID\n\tid := mux.Vars(r)[\"id\"]\n\tvar rsp apiserver.Response\n\n\tif b, err := ioutil.ReadAll(r.Body); err == nil {\n\t\t// Try the Mac address first\n\n\t\t// We need to revert the mac address back to the correct format (dashes back to colons)\n\t\terr := DeleteDeploymentMac(strings.Replace(id, \"-\", \":\", -1), b)\n\t\tif err != nil {\n\n\t\t\t// We need to revert the ip address back to the correct format (dashes back to periods)\n\t\t\terr = DeleteDeploymentAddress(strings.Replace(id, \"-\", \".\", -1), b)\n\t\t\tif err != nil {\n\t\t\t\trsp.Warning = \"Error updating Deployment Configuration\"\n\t\t\t\trsp.Error = err.Error()\n\t\t\t\trsp.Payload = nil\n\t\t\t}\n\t\t}\n\t}\n\tjson.NewEncoder(w).Encode(rsp)\n\n}\n\n// Retrieve a specific plunder deployment configuration\nfunc deleteDeploymentMac(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t// Find the deployment ID\n\tid := mux.Vars(r)[\"id\"]\n\tvar rsp apiserver.Response\n\n\tif b, err := ioutil.ReadAll(r.Body); err == nil {\n\t\t// We need to revert the mac address back to the correct format (dashes back to colons)\n\t\terr := DeleteDeploymentMac(strings.Replace(id, \"-\", \":\", -1), b)\n\t\tif err != nil {\n\t\t\trsp.Warning = \"Error updating Deployment Configuration\"\n\t\t\trsp.Error = err.Error()\n\t\t\trsp.Payload = nil\n\t\t}\n\n\t}\n\tjson.NewEncoder(w).Encode(rsp)\n\n}\n\n// Retrieve a specific plunder deployment configuration\nfunc deleteDeploymentAddress(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t// Find the deployment ID\n\tid := mux.Vars(r)[\"id\"]\n\tvar rsp apiserver.Response\n\n\tif b, err := ioutil.ReadAll(r.Body); err == nil {\n\t\t// We need to revert the mac address back to the correct format (dashes back to colons)\n\t\terr = DeleteDeploymentAddress(strings.Replace(id, \"-\", \".\", -1), b)\n\t\tif err != nil {\n\t\t\trsp.Warning = \"Error updating Deployment Configuration\"\n\t\t\trsp.Error = err.Error()\n\t\t\trsp.Payload = nil\n\t\t}\n\n\t}\n\tjson.NewEncoder(w).Encode(rsp)\n\n}\n\nfunc getDHCP(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tvar rsp apiserver.Response\n\n\t// Find the deployment ID\n\tid := mux.Vars(r)[\"id\"]\n\n\tif id == \"leases\" {\n\n\t\tjsonData, err := json.Marshal(Controller.GetLeases())\n\t\tif err != nil {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\trsp.Warning = \"Error retrieving allocated leases\"\n\t\t\trsp.Error = err.Error()\n\t\t} else {\n\t\t\trsp.Payload = jsonData\n\t\t}\n\t}\n\t// Are we updating the deployment \"global\"\n\tif id == \"unleased\" {\n\n\t\tjsonData, err := json.Marshal(Controller.GetUnLeased())\n\n\t\tif err != nil {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\trsp.Warning = \"Error retrieving allocated leases\"\n\t\t\trsp.Error = err.Error()\n\t\t} else {\n\t\t\trsp.Payload = jsonData\n\t\t}\n\t}\n\n\tjson.NewEncoder(w).Encode(rsp)\n}\n"
  },
  {
    "path": "pkg/services/server.go",
    "content": "package services\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/ghodss/yaml\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// This is needed by other functions to build strings\nvar httpAddress string\n\n// Controller contains all the \"current\" settings for booting servers\nvar Controller BootController\n\n// Deployments - contains an accessible \"current\" configuration for all deployments\nvar Deployments DeploymentConfigurationFile\n\n// ParseControllerData will read in a byte array and attempt to parse it as yaml or json\nfunc ParseControllerData(b []byte) error {\n\n\tjsonBytes, err := yaml.YAMLToJSON(b)\n\tif err == nil {\n\t\t// If there were no errors then the YAML => JSON was successful, no attempt to unmarshall\n\t\terr = json.Unmarshal(jsonBytes, &Controller)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Unable to parse configuration as either yaml or json\")\n\t\t}\n\n\t} else {\n\t\t// Couldn't parse the yaml to JSON\n\t\t// Attempt to parse it as JSON\n\t\terr = json.Unmarshal(b, &Controller)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Unable to parse configuration as either yaml or json\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// Parse will read through a new configuration and implement the configuration if possible\nfunc (b *BootConfig) Parse() error {\n\tif isoMapper == nil {\n\t\t// Ensure it is initialised before trying to use it\n\t\tisoMapper = make(map[string]string)\n\t}\n\n\tif b.ISOPrefix == \"\" || b.ISOPath == \"\" {\n\t\tlog.Debugf(\"No ISO is being parsed for configuration %s\", b.ConfigName)\n\t} else {\n\t\t// Atempt to open the ISO and add it to the map for usage later\n\t\terr := OpenISO(b.ISOPath, b.ISOPrefix)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"Error parsing ISO [%v]\", err)\n\t\t\treturn err\n\t\t}\n\n\t\t// Create the prefix\n\t\turlPrefix := fmt.Sprintf(\"/%s/\", b.ISOPrefix)\n\n\t\t// Only create the handler if one doesn't exist\n\t\tif _, ok := isoMapper[b.ISOPrefix]; !ok {\n\t\t\tlog.Debugf(\"Adding handler %s\", urlPrefix)\n\t\t\tserveMux.HandleFunc(urlPrefix, isoReader)\n\n\t\t\t// Add the iso path to the correct prefix\n\t\t\tisoMapper[b.ISOPrefix] = b.ISOPath\n\t\t}\n\n\t\tlog.Debugf(\"Updating handler %s for config %s\", urlPrefix, b.ConfigName)\n\t}\n\tlog.Infof(\"Boot Config [%s] of type [%s] parsed succesfully\", b.ConfigName, b.ConfigType)\n\t// No errors and BootConfig is applied\n\treturn nil\n}\n\n// // ParseBootController - will iterate through the boot controller and see if any changes need applying\n// // this is mainly for the dynamic loading of ISOs\n// func (c *BootController) ParseBootController() error {\n\n// \tfor i := range c.BootConfigs {\n// \t\t// If either the prefix or path are blank then iterate over, both need to be set in order to load the ISO\n// \t\tif c.BootConfigs[i].ISOPrefix == \"\" || c.BootConfigs[i].ISOPath == \"\" {\n// \t\t\tlog.Debugf(\"No ISO is being parsed for configuration %s\", c.BootConfigs[i].ConfigName)\n// \t\t} else {\n// \t\t\t// Atempt to open the ISO and add it to the map for usage later\n// \t\t\terr := OpenISO(c.BootConfigs[i].ISOPath, c.BootConfigs[i].ISOPrefix)\n// \t\t\tif err != nil {\n// \t\t\t\tlog.Errorf(\"Error parsing ISO [%v]\", err)\n// \t\t\t\treturn err\n// \t\t\t}\n\n// \t\t\t// Create the prefix\n// \t\t\turlPrefix := fmt.Sprintf(\"/%s/\", c.BootConfigs[i].ISOPrefix)\n\n// \t\t\t// Only create the handler if one doesn't exist\n// \t\t\tif _, ok := isoMapper[c.BootConfigs[i].ISOPrefix]; !ok {\n// \t\t\t\tlog.Debugf(\"Adding handler %s\", urlPrefix)\n\n// \t\t\t\tserveMux.HandleFunc(urlPrefix, isoReader)\n// \t\t\t}\n\n// \t\t\tlog.Debugf(\"Updating handler %s for config %s\", urlPrefix, c.BootConfigs[i].ConfigName)\n\n// \t\t}\n// \t}\n// \t// Parse the boot controllers for new configuration changes\n// \tc.generateBootTypeHanders()\n// \treturn nil\n// }\n\n// DeleteBootControllerConfig - will iterate through the boot controller and see if any changes need applying\n// this is mainly for the dynamic loading of ISOs\nfunc (c *BootController) DeleteBootControllerConfig(configName string) error {\n\n\tfor i := range c.BootConfigs {\n\t\tif c.BootConfigs[i].ConfigName == configName {\n\t\t\t// Remove the mapping to an ISO path\n\t\t\tif isoMapper != nil {\n\t\t\t\t// Ensure it is initialised before trying to remove boot config\n\t\t\t\tisoMapper[c.BootConfigs[i].ISOPrefix] = \"\"\n\t\t\t}\n\t\t\tc.BootConfigs = append(c.BootConfigs[:i], c.BootConfigs[i+1:]...)\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn fmt.Errorf(\"Unable to find boot configuration %s\", configName)\n}\n\n// ParseDeployment will read in a byte array and attempt to parse it as yaml or json\nfunc ParseDeployment(b []byte) (*DeploymentConfigurationFile, error) {\n\n\tvar deployment DeploymentConfigurationFile\n\n\tjsonBytes, err := yaml.YAMLToJSON(b)\n\tif err == nil {\n\t\t// If there were no errors then the YAML => JSON was successful, no attempt to unmarshall\n\t\terr = json.Unmarshal(jsonBytes, &deployment)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"Unable to parse configuration as either yaml or json\\n %s\", err.Error())\n\t\t}\n\n\t} else {\n\t\t// Couldn't parse the yaml to JSON\n\t\t// Attempt to parse it as JSON\n\t\terr = json.Unmarshal(b, &deployment)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"Unable to parse configuration as either yaml or json\\n %s\", err.Error())\n\t\t}\n\t}\n\treturn &deployment, nil\n}\n"
  },
  {
    "path": "pkg/services/serverDHCP.go",
    "content": "package services\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\tdhcp \"github.com/krolaw/dhcp4\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Lease defines a lease that is allocated to a client\ntype Lease struct {\n\tMAC    string    `json:\"mac\"`  // Client's Physical Address\n\tExpiry time.Time `json:\"time\"` // When the lease expires\n}\n\n// DHCPSettings -\ntype DHCPSettings struct {\n\tIP      net.IP       // Server IP to use\n\tOptions dhcp.Options // Options to send to DHCP Clients\n\tStart   net.IP       // Start of IP range to distribute\n\n\tLeaseRange    int           // Number of IPs to distribute (starting from start)\n\tLeaseDuration time.Duration // Lease period\n\n\tLeases   map[int]Lease // Map to keep track of leases\n\tUnLeased []Lease       // Map to keep track of unleased devices, and when they were seen\n}\n\n// Discover - Is the discovering of a DHCP server on the network and the typical result is an lease \"offer\"\n// Request - The Request is typically the acceptance of a DHCP lease\n// Release - A Release is the client notifying that server that the lease is no longer required\n\n//ServeDHCP - Is the function that is called when ever plunder recieves DHCP packets.\nfunc (h *DHCPSettings) ServeDHCP(p dhcp.Packet, msgType dhcp.MessageType, options dhcp.Options) (d dhcp.Packet) {\n\tmac := strings.ToLower(p.CHAddr().String())\n\tlog.Debugf(\"DCHP Message Type: [%v] from MAC Address [%s]\", msgType, mac)\n\n\t// Retrieve teh deployment type\n\tdeploymentType := FindDeploymentConfigFromMac(mac)\n\t// Convert the : in the mac address to dashes to make life easier\n\tdashMac := strings.Replace(mac, \":\", \"-\", -1)\n\n\t// These packets typicallty will be in one of a number of phases:\n\tswitch msgType {\n\tcase dhcp.Discover:\n\n\t\t// Look for an existing license\n\t\tfree := -1\n\t\tfor i, v := range h.Leases { // Find previous lease\n\t\t\tif v.MAC == mac {\n\t\t\t\tfree = i\n\t\t\t\tgoto reply\n\t\t\t}\n\t\t}\n\n\t\t// Look for a free lease\n\t\tif free = h.freeLease(); free == -1 {\n\t\t\t// No leases available\n\t\t\treturn\n\t\t}\n\treply:\n\t\t//TODO - work out why this is here\n\t\th.Options[dhcp.OptionVendorClassIdentifier] = h.IP\n\n\t\t// if DHCP option \"OptionUserClass\" is set to iPXE then we know that it's default booted to the correct bootloader\n\t\tif string(options[dhcp.OptionUserClass]) == \"iPXE\" {\n\t\t\t// This will ensure that the leasing table is kept updated for when a server was last seen\n\t\t\th.leaseHander(deploymentType, mac)\n\n\t\t\t// TODO - This can be removed and left in the REQUEST section only\n\n\t\t\t// if an entry doesnt exist then drop it to a default type, if not then it has its own specific\n\t\t\tif httpPaths[fmt.Sprintf(\"%s.ipxe\", dashMac)] == \"\" {\n\t\t\t\th.Options[dhcp.OptionBootFileName] = []byte(\"http://\" + h.IP.String() + \"/\" + deploymentType + \".ipxe\")\n\t\t\t} else {\n\t\t\t\th.Options[dhcp.OptionBootFileName] = []byte(\"http://\" + h.IP.String() + \"/\" + dashMac + \".ipxe\")\n\t\t\t}\n\n\t\t}\n\n\t\tipLease := dhcp.IPAdd(h.Start, free)\n\t\tlog.Debugf(\"Allocated IP [%s] for [%s]\", ipLease.String(), mac)\n\n\t\treturn dhcp.ReplyPacket(p, dhcp.Offer, h.IP, ipLease, h.LeaseDuration,\n\t\t\th.Options.SelectOrderOrAll(options[dhcp.OptionParameterRequestList]))\n\n\tcase dhcp.Request:\n\n\t\tif server, ok := options[dhcp.OptionServerIdentifier]; ok && !net.IP(server).Equal(h.IP) {\n\t\t\treturn nil // Message not for this dhcp server\n\t\t}\n\t\treqIP := net.IP(options[dhcp.OptionRequestedIPAddress])\n\t\tif reqIP == nil {\n\t\t\treqIP = net.IP(p.CIAddr())\n\t\t}\n\n\t\tif len(reqIP) == 4 && !reqIP.Equal(net.IPv4zero) {\n\t\t\tif leaseNum := dhcp.IPRange(h.Start, reqIP) - 1; leaseNum >= 0 && leaseNum < h.LeaseRange {\n\t\t\t\tif l, exists := h.Leases[leaseNum]; !exists || l.MAC == p.CHAddr().String() {\n\n\t\t\t\t\t// Specify the new lease\n\t\t\t\t\th.Leases[leaseNum] = Lease{\n\t\t\t\t\t\tMAC:    p.CHAddr().String(),\n\t\t\t\t\t\tExpiry: time.Now().Add(h.LeaseDuration),\n\t\t\t\t\t}\n\n\t\t\t\t\t// if DHCP option \"OptionUserClass\" is set to iPXE then we know that it's default booted to the correct bootloader\n\t\t\t\t\tif string(options[dhcp.OptionUserClass]) == \"iPXE\" {\n\t\t\t\t\t\t// Only Print out this notification if it's from the iPXE Boot loader\n\t\t\t\t\t\tlog.Infof(\"Mac address [%s] is assigned a [%s] deployment type\", mac, deploymentType)\n\t\t\t\t\t}\n\n\t\t\t\t\t// if an entry doesnt exist then drop it to a default type, if not then it has its own specific\n\t\t\t\t\tif httpPaths[fmt.Sprintf(\"/%s.ipxe\", dashMac)] == \"\" {\n\t\t\t\t\t\th.Options[dhcp.OptionBootFileName] = []byte(\"http://\" + h.IP.String() + \"/\" + deploymentType + \".ipxe\")\n\t\t\t\t\t} else {\n\t\t\t\t\t\th.Options[dhcp.OptionBootFileName] = []byte(\"http://\" + h.IP.String() + \"/\" + dashMac + \".ipxe\")\n\t\t\t\t\t}\n\n\t\t\t\t\treturn dhcp.ReplyPacket(p, dhcp.ACK, h.IP, reqIP, h.LeaseDuration,\n\t\t\t\t\t\th.Options.SelectOrderOrAll(options[dhcp.OptionParameterRequestList]))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn dhcp.ReplyPacket(p, dhcp.NAK, h.IP, nil, 0, nil)\n\n\tcase dhcp.Release, dhcp.Decline:\n\t\tfor i, v := range h.Leases {\n\t\t\tif v.MAC == mac {\n\t\t\t\tlog.Debugf(\"Releasing lease for [%s]\", mac)\n\t\t\t\tdelete(h.Leases, i)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// leaseHandler() will take care of adding and removing leases based upon use-case\nfunc (h *DHCPSettings) leaseHander(deploymentType, mac string) {\n\tif deploymentType == \"\" || deploymentType == \"autoBoot\" || deploymentType == \"reboot\" {\n\t\t// Create a lease for an un-used server (dont by default)\n\t\tnewUnleased := Lease{\n\t\t\tMAC:    mac,\n\t\t\tExpiry: time.Now(),\n\t\t}\n\t\t// False by default\n\t\tvar macFound bool\n\n\t\t// Look through array\n\t\tfor i := range h.UnLeased {\n\t\t\tif mac == h.UnLeased[i].MAC {\n\t\t\t\th.UnLeased[i].Expiry = time.Now()\n\t\t\t\t// Found this entry\n\t\t\t\tmacFound = true\n\t\t\t}\n\t\t}\n\n\t\t// New entry\n\t\tif macFound == false {\n\t\t\t// Update the unleased map with this mac address being seen\n\t\t\th.UnLeased = append(h.UnLeased, newUnleased)\n\t\t}\n\t}\n\n\t// If this mac address has no deployment type for whatever reason, ensure a warning message is presented\n\tif deploymentType == \"\" {\n\t\tlog.Warnf(\"Mac address[%s] is unknown, not returning an address\", mac)\n\t}\n}\n\nfunc (h *DHCPSettings) freeLease() int {\n\tnow := time.Now()\n\tb := rand.Intn(h.LeaseRange) // Try random first\n\tfor _, v := range [][]int{{b, h.LeaseRange}, {0, b}} {\n\t\tfor i := v[0]; i < v[1]; i++ {\n\t\t\tif l, ok := h.Leases[i]; !ok || l.Expiry.Before(now) {\n\t\t\t\treturn i\n\t\t\t}\n\t\t}\n\t}\n\treturn -1\n}\n\n// GetLeases - This will retrieve all of the allocated leases from the boot controller\nfunc (c *BootController) GetLeases() *[]Lease {\n\tvar l []Lease\n\tfor i := range c.handler.Leases {\n\t\tl = append(l, c.handler.Leases[i])\n\t}\n\treturn &l\n}\n\n// GetUnLeased - This will retrieve all of the un-allocated leases from the boot controller\nfunc (c *BootController) GetUnLeased() *[]Lease {\n\tif c.handler == nil {\n\t\tvar emptyLease []Lease\n\t\treturn &emptyLease\n\t}\n\treturn &c.handler.UnLeased\n}\n\n// DelUnLeased - This will retrieve all of the un-allocated leases from the boot controller\nfunc (c *BootController) DelUnLeased(mac string) {\n\tif c == nil || c.handler == nil {\n\t\treturn\n\t}\n\tif len(c.handler.UnLeased) == 0 {\n\t\treturn\n\t}\n\tfor i := range c.handler.UnLeased {\n\t\tif mac == c.handler.UnLeased[i].MAC {\n\t\t\tc.handler.UnLeased = append(c.handler.UnLeased[:i], c.handler.UnLeased[i+1:]...)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/services/serverHTTP.go",
    "content": "package services\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"path/filepath\"\n\n\t\"github.com/plunder-app/plunder/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// These strings container the generated iPXE details that are passed to the bootloader when the correct url is requested\nvar autoBoot, preseed, kickstart, defaultBoot, vsphere, reboot string\n\n// controller Pointer for the config API endpoint handler\nvar controller *BootController\n\nvar serveMux *http.ServeMux\n\n// TODO - this should be removed\nfunc (c *BootController) generateBootTypeHanders() {\n\n\t// Find the default configuration\n\tdefaultConfig := findBootConfigForType(\"default\")\n\tif defaultConfig != nil {\n\t\tdefaultBoot = utils.IPXEPreeseed(*c.HTTPAddress, defaultConfig.Kernel, defaultConfig.Initrd, defaultConfig.Cmdline)\n\t} //else {\n\t//\tlog.Warnf(\"Found [%d] configurations and no \\\"default\\\" configuration\", len(c.BootConfigs))\n\t//}\n\n\t// If a preeseed configuration has been configured then add it, and create a HTTP endpoint\n\tpreeseedConfig := findBootConfigForType(\"preseed\")\n\tif preeseedConfig != nil {\n\t\tpreseed = utils.IPXEPreeseed(*c.HTTPAddress, preeseedConfig.Kernel, preeseedConfig.Initrd, preeseedConfig.Cmdline)\n\n\t}\n\n\t// If a kickstart configuration has been configured then add it, and create a HTTP endpoint\n\tkickstartConfig := findBootConfigForType(\"kickstart\")\n\tif kickstartConfig != nil {\n\t\tkickstart = utils.IPXEPreeseed(*c.HTTPAddress, kickstartConfig.Kernel, kickstartConfig.Initrd, kickstartConfig.Cmdline)\n\t}\n\n\t// If a vsphereConfig configuration has been configured then add it, and create a HTTP endpoint\n\tvsphereConfig := findBootConfigForType(\"vsphere\")\n\tif vsphereConfig != nil {\n\t\tvsphere = utils.IPXEVSphere(*c.HTTPAddress, vsphereConfig.Kernel, vsphereConfig.Cmdline)\n\t}\n}\n\nfunc (c *BootController) serveHTTP() error {\n\n\t// This function will pre-generate the boot handlers for the various boot types\n\tc.generateBootTypeHanders()\n\n\tautoBoot = utils.IPXEAutoBoot()\n\treboot = utils.IPXEReboot()\n\n\tdocroot, err := filepath.Abs(\"./\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Created only once\n\n\t// TOTO - alloew this to be customisable\n\tserveMux.Handle(\"/\", http.FileServer(http.Dir(docroot)))\n\n\t// Boot handlers\n\tserveMux.HandleFunc(\"/health\", HealthCheckHandler)\n\tserveMux.HandleFunc(\"/reboot.ipxe\", rebootHandler)\n\tserveMux.HandleFunc(\"/autoBoot.ipxe\", autoBootHandler)\n\tserveMux.HandleFunc(\"/default.ipxe\", rootHandler)\n\tserveMux.HandleFunc(\"/kickstart.ipxe\", kickstartHandler)\n\tserveMux.HandleFunc(\"/preseed.ipxe\", preseedHandler)\n\tserveMux.HandleFunc(\"/vsphere.ipxe\", vsphereHandler)\n\n\t// Set the pointer to the boot config\n\tcontroller = c\n\n\treturn http.ListenAndServe(\":80\", serveMux)\n}\n\nfunc rootHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Debugf(\"Requested URL [%s]\", r.RequestURI)\n\n\tw.WriteHeader(http.StatusOK)\n\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t// Return the preseed content\n\tlog.Debugf(\"Requested URL [%s]\", r.URL.Host)\n\tio.WriteString(w, httpPaths[r.URL.Path])\n}\n\nfunc preseedHandler(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusOK)\n\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t// Return the preseed content\n\tio.WriteString(w, preseed)\n}\n\nfunc kickstartHandler(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusOK)\n\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t// Return the kickstart content\n\tio.WriteString(w, kickstart)\n}\n\nfunc vsphereHandler(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusOK)\n\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t// Return the vsphere content\n\tio.WriteString(w, vsphere)\n}\n\nfunc defaultBootHandler(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusOK)\n\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t// Return the default boot content\n\tio.WriteString(w, defaultBoot)\n}\n\nfunc rebootHandler(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusOK)\n\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t// Return the reboot content\n\tio.WriteString(w, reboot)\n}\n\nfunc autoBootHandler(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusOK)\n\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t// Return the reboot content\n\tio.WriteString(w, autoBoot)\n}\n\n// HealthCheckHandler -\nfunc HealthCheckHandler(w http.ResponseWriter, r *http.Request) {\n\t// A very simple health check.\n\tw.WriteHeader(http.StatusOK)\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t// In the future we could report back on the status of our DB, or our cache\n\t// (e.g. Redis) by performing a simple PING, and include them in the response.\n\tio.WriteString(w, `{\"alive\": true}`)\n}\n"
  },
  {
    "path": "pkg/services/serverHTTPISO.go",
    "content": "package services\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/hooklift/iso9660\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// TODO - This currently is inefficient and results in an open/parse of an iso for every file operation.\n// github.com/qeedquan/iso9660 may need looking at later on. ( thebsdbox / [1/9/19] )\n// Comments are left incase we/I revert\n\n// isoMapper at this point just maps the prefix to the path this may change\nvar isoMapper map[string]string\n\n// iso9660PathSanitiser will take a \"standard\" file path and convert it into something that make sense within iso9660 TOC\n// The iso9660 constraints:\n// - A-Z (uppercase)\n// - '_' is the only other character\n// - Filename can only be 32 characters (inclucing the terminating semicolon ';')\n\nfunc iso9660PathSanitiser(unsanitisedPath string) string {\n\t// Get the filename from the string\n\tfullFilename := filepath.Base(unsanitisedPath)\n\t// Get the extension\n\textension := filepath.Ext(fullFilename)\n\n\t// Remove the extension and leave just the filename\n\tfilename := strings.TrimSuffix(fullFilename, extension)\n\t// Store the filename and shorten if over 31 characters\n\n\ttrimmedFilename := filename\n\tpathLength := len(filename) + len(extension)\n\tif pathLength > 31 {\n\t\t// If the path is too long then we shrink the extension to a seperator and three characters\n\t\tif len(extension) > 3 {\n\t\t\textension = extension[0:4]\n\t\t}\n\t\t// work out how much of the remaining filename can survive\n\t\ttrimCount := 31 - len(extension)\n\t\ttrimmedFilename = filename[0:trimCount]\n\t}\n\n\trebuiltFileName := strings.ToUpper(fmt.Sprintf(\"%s%s\", trimmedFilename, extension))\n\t// Find if there is a full stop in the file name\n\tstopCount := strings.Count(rebuiltFileName, \".\")\n\tvar isoFilename string\n\n\tswitch stopCount {\n\tcase 0:\n\t\t// Append one as there is no filepath\n\t\tisoFilename = fmt.Sprintf(\"%s.\", rebuiltFileName)\n\tcase 1:\n\t\t// Not needed, just the semicolon\n\t\tisoFilename = fmt.Sprintf(\"%s\", rebuiltFileName)\n\tdefault:\n\t\t// Ensure all other stops are changed to underscores\n\t\tisoFilename = fmt.Sprintf(\"%s\", strings.Replace(rebuiltFileName, \".\", \"_\", stopCount-1))\n\t}\n\n\t//rebuild the path uppercase\n\trebuildPath := strings.ToLower(fmt.Sprintf(\"%s/%s\", filepath.Dir(unsanitisedPath), isoFilename))\n\n\t// strD replacer\n\treplacer := strings.NewReplacer(\"+\", \"_\", \"-\", \"_\", \" \", \"_\", \"~\", \"_\")\n\t// Format the final output\n\tisoFormatted := replacer.Replace(rebuildPath)\n\n\treturn isoFormatted\n}\n\n// This takes care of parsing a URL to identify if it should map to an ISO hosted file.\n\n// ISOReader -\nfunc isoReader(w http.ResponseWriter, r *http.Request) {\n\n\t// Sanitise the URL, there are a number of steps involved with turning the url into something we can use\n\t// Remove the beginning slash\n\trawURL := strings.TrimLeft(r.URL.String(), \"/\")\n\n\t// Unescape the Http query\n\tisoURL, err := url.QueryUnescape(rawURL)\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\tio.WriteString(w, fmt.Sprintf(\"%s\", err.Error()))\n\t\tlog.Error(err)\n\n\t\treturn\n\t}\n\n\t// Split the URL to find the prefix (first part of the URL)\n\turlElements := strings.Split(isoURL, \"/\")\n\t// Ensure the URL can be parsed\n\tif len(urlElements) > 1 {\n\t\tisoPrefix := urlElements[0]\n\n\t\tisoPath := iso9660PathSanitiser(strings.Replace(isoURL, isoPrefix, \"\", 1))\n\n\t\t// We now have the ISO prefix to look up files, and the path to look up in the ISO\n\t\t// Check for ISO\n\t\tlog.Debugf(\"Original URL: %s ISO Path: %s\", isoURL, isoPath)\n\t\tif _, ok := isoMapper[isoPrefix]; ok {\n\t\t\tfile, err := os.Open(isoMapper[isoPrefix])\n\t\t\tif err != nil {\n\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\tio.WriteString(w, fmt.Sprintf(\"%s\", err.Error()))\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer file.Close()\n\t\t\tr, err := iso9660.NewReader(file)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor {\n\n\t\t\t\tf, err := r.Next()\n\t\t\t\tif err == io.EOF {\n\t\t\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\t\t\tio.WriteString(w, fmt.Sprintf(\"Unable to read/find file %s\", isoPath))\n\t\t\t\t\tlog.Error(fmt.Sprintf(\"Unable to read/find file %s\", isoPath))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif f.Name() == isoPath {\n\t\t\t\t\tfreader := f.Sys().(io.Reader)\n\t\t\t\t\tbuf := new(bytes.Buffer)\n\t\t\t\t\tbuf.ReadFrom(freader)\n\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\tw.Header().Set(\"Content-Type\", \"application/x-binary\")\n\t\t\t\t\tio.WriteString(w, buf.String())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t// isoFile, err := isoMapper[isoPrefix].Open(isoPath)\n\t\t\t// if err != nil {\n\t\t\t// \tw.WriteHeader(http.StatusNotFound)\n\t\t\t// \tio.WriteString(w, fmt.Sprintf(\"%s\", err.Error()))\n\t\t\t// \treturn\n\t\t\t// }\n\n\t\t\t// fileStat, err := isoFile.Stat()\n\t\t\t// if err != nil {\n\t\t\t// \tw.WriteHeader(http.StatusNotFound)\n\t\t\t// \tio.WriteString(w, fmt.Sprintf(\"Unable to stat file on ISO %s\", isoPath))\n\t\t\t// \treturn\n\t\t\t// }\n\t\t\t// fileBytes = make([]byte, fileStat.Size())\n\t\t\t// _, err = isoFile.Read(fileBytes)\n\t\t\t// if err != nil {\n\t\t\t// \tw.WriteHeader(http.StatusNotFound)\n\t\t\t// \tio.WriteString(w, fmt.Sprintf(\"Unable to read file on ISO %s\", isoPath))\n\t\t\t// \treturn\n\t\t\t// }\n\t\t}\n\n\t} else {\n\t\tw.WriteHeader(http.StatusNotFound)\n\t\tio.WriteString(w, fmt.Sprintf(\"Unable to find content ISO Prefix %s\", isoURL))\n\t\treturn\n\t}\n\tw.WriteHeader(http.StatusNotFound)\n\tio.WriteString(w, fmt.Sprintf(\"Unable to find content ISO Prefix %s\", isoURL))\n\treturn\n}\n\n// OpenISO will open an iso and add it to out ISO Map for reading at a later point\nfunc OpenISO(isoPath, isoPrefix string) error {\n\t// Check that the file exists\n\n\t_, err := os.Stat(isoPath)\n\t// We could use os.IsNotExist() but we may as well capture all errors\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Error reading file [%s]\", isoPath)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/services/serverImageHTTP.go",
    "content": "package services\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/dustin/go-humanize\"\n)\n\n// WriteCounter counts the number of bytes written to it. It implements to the io.Writer interface\n// and we can pass this into io.TeeReader() which will report progress on each write cycle.\ntype WriteCounter struct {\n\tTotal uint64\n}\n\nvar data []byte\n\nfunc (wc *WriteCounter) Write(p []byte) (int, error) {\n\tn := len(p)\n\twc.Total += uint64(n)\n\treturn n, nil\n}\n\nfunc tickerProgress(byteCounter uint64) {\n\t// Clear the line by using a character return to go back to the start and remove\n\t// the remaining characters by filling it with spaces\n\tfmt.Printf(\"\\r%s\", strings.Repeat(\" \", 35))\n\n\t// Return again and print current status of download\n\t// We use the humanize package to print the bytes in a meaningful way (e.g. 10 MB)\n\tfmt.Printf(\"\\rDownloading... %s complete\", humanize.Bytes(byteCounter))\n\tfmt.Println(\"\")\n}\n\nfunc imageHandler(w http.ResponseWriter, r *http.Request) {\n\n\tlog.Infof(\"Incoming image from [%s]\", r.RemoteAddr)\n\n\tr.ParseMultipartForm(32 << 20)\n\tfile, handler, err := r.FormFile(\"BootyImage\")\n\tif handler != nil {\n\t\tlog.Infof(\"Beginning to recieve image [%s]\", handler.Filename)\n\t}\n\n\tif err != nil {\n\t\tlog.Errorf(\"%v\", err)\n\t\treturn\n\t}\n\tdefer file.Close()\n\n\tout, err := os.OpenFile(handler.Filename, os.O_CREATE|os.O_WRONLY, 0644)\n\tif err != nil {\n\t\tlog.Fatalf(\"%v\", err)\n\t}\n\tdefer out.Close()\n\n\t// Create our progress reporter and pass it to be used alongside our writer\n\tticker := time.NewTicker(500 * time.Millisecond)\n\tcounter := &WriteCounter{}\n\n\tgo func() {\n\t\tfor ; true; <-ticker.C {\n\t\t\ttickerProgress(counter.Total)\n\t\t}\n\t}()\n\tif _, err = io.Copy(out, io.TeeReader(file, counter)); err != nil {\n\t\tlog.Errorf(\"%v\", err)\n\t}\n\n\tlog.Infof(\"Written of image [%s] to disk\", handler.Filename)\n\tticker.Stop()\n\n\tw.WriteHeader(http.StatusOK)\n}\n\nfunc configHandler(w http.ResponseWriter, r *http.Request) {\n\tw.Write(data)\n}\n\n// Serve will start the webserver for BOOTy images\nfunc (c *BootController) serveImageHTTP() error {\n\n\tfs := http.FileServer(http.Dir(\"./images\"))\n\thttp.HandleFunc(\"/image\", imageHandler)\n\thttp.Handle(\"/images/\", http.StripPrefix(\"/images/\", fs))\n\tlog.Println(\"Plunder OS Image Services --> Starting HTTP :3000\")\n\terr := http.ListenAndServe(\":3000\", nil)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/services/serverTFTP.go",
    "content": "package services\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"os\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\ttftp \"github.com/thebsdbox/go-tftp/server\"\n)\n\nvar iPXEData []byte\n\n// HandleWrite : writing is disabled in this service\nfunc HandleWrite(filename string) (w io.Writer, err error) {\n\terr = errors.New(\"Server is read only\")\n\treturn\n}\n\n// HandleRead : read a ROfs file and send over tftp\nfunc HandleRead(filename string) (r io.Reader, err error) {\n\tr = bytes.NewBuffer(iPXEData)\n\treturn\n}\n\n// tftp server\nfunc (c *BootController) serveTFTP() error {\n\n\tlog.Printf(\"Opening and caching undionly.kpxe\")\n\tf, err := os.Open(*c.PXEFileName)\n\tif err != nil {\n\t\tlog.Warnf(\"No local undionly.kpxe found, falling back to embedded version which may be out of date\")\n\t\tiPXEData, err = hex.DecodeString(pxeFile)\n\t} else {\n\t\t// Use bufio.NewReader to get a Reader.\n\t\t// ... Then use ioutil.ReadAll to read the entire content.\n\t\tr := bufio.NewReader(f)\n\n\t\tiPXEData, err = ioutil.ReadAll(r)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\ts := tftp.NewServer(\"\", HandleRead, HandleWrite)\n\terr = s.Serve(*c.TFTPAddress + \":69\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/services/services.go",
    "content": "package services\n\nimport (\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/plunder-app/plunder/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\tdhcp \"github.com/krolaw/dhcp4\"\n\t\"github.com/krolaw/dhcp4/conn\"\n)\n\nvar dhcpServer = make(chan bool)\nvar dhcpError = make(chan error, 1)\n\nvar runningDHCP, runningTFTP, runningHTTP bool\n\n// find BootConfig will look through a Boot controller for a booting configuration identified through a configuration name\nfunc findBootConfigForDeployment(deployment DeploymentConfig) *BootConfig {\n\n\t// // First check is to look inside the deployment configuration for a custom configuration\n\t// if deployment.ConfigBoot.Kernel != \"\" && deployment.ConfigBoot.Initrd != \"\" {\n\t// \t// A Custom Kernel and initrd are specified\n\t// \tlog.Debugf(\"The server [%s] has a custom bootConfig defined\", deployment.MAC)\n\t// \treturn &deployment.ConfigBoot\n\t// }\n\n\t// Second check is to find a matching controller configuration to adopt\n\tfor i := range Controller.BootConfigs {\n\t\tif Controller.BootConfigs[i].ConfigName == deployment.ConfigName {\n\t\t\t// Set the specific deployment configuration to the controller config\n\t\t\treturn &Controller.BootConfigs[i]\n\t\t}\n\t}\n\n\t// Either there is no custom kernel/initrd/cmdline or a bootconfig doesn't exist as part of the server configuration\n\treturn nil\n}\n\n// find BootConfig will look through a Boot controller for a booting configuration identified through a configuration name\nfunc findBootConfigForType(ConfigType string) *BootConfig {\n\n\t// Find a matching controller configuration to return\n\tfor i := range Controller.BootConfigs {\n\t\tif Controller.BootConfigs[i].ConfigType == ConfigType {\n\t\t\treturn &Controller.BootConfigs[i]\n\t\t}\n\t}\n\n\t// No configuration could be found\n\treturn nil\n}\n\n// find BootConfig will look through a Boot controller for a booting configuration identified through a configuration name\nfunc (c *BootController) setBootConfig(configName, configType, kernel, initrd, cmdline string) {\n\tnewConfig := &BootConfig{\n\t\tConfigName: configName,\n\t\tConfigType: configType,\n\t\tKernel:     kernel,\n\t\tInitrd:     initrd,\n\t\tCmdline:    cmdline,\n\t}\n\tc.BootConfigs = append(c.BootConfigs, *newConfig)\n}\n\n// StartServices - This will start all of the enabled services\nfunc (c *BootController) StartServices(deployment []byte) error {\n\tlog.Infof(\"Starting Remote Boot Services, press CTRL + c to stop\")\n\n\tif *c.EnableDHCP == true {\n\t\tc.handler = &DHCPSettings{}\n\t\t// DHCP Server address\n\t\tip, err := utils.ConvertIP(c.DHCPConfig.DHCPAddress)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"DHCP Server -> %v\", err)\n\t\t}\n\t\tc.handler.IP = ip\n\n\t\t// Start address of DHCP Range\n\t\tip, err = utils.ConvertIP(c.DHCPConfig.DHCPStartAddress)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"DHCP Start Address -> %v\", err)\n\t\t}\n\t\tc.handler.Start = ip\n\n\t\t// Additional DHCP options\n\t\tc.handler.LeaseDuration = 2 * time.Hour //TODO, make time modifiable\n\t\tc.handler.LeaseRange = c.DHCPConfig.DHCPLeasePool\n\t\t// Initialise the two maps\n\t\tc.handler.Leases = make(map[int]Lease, c.DHCPConfig.DHCPLeasePool)\n\n\t\tvar options = dhcp.Options{}\n\n\t\t// Subnet\n\t\tip, err = utils.ConvertIP(c.DHCPConfig.DHCPSubnet)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"DHCP Subnet -> %v\", err)\n\t\t}\n\t\toptions[dhcp.OptionSubnetMask] = ip\n\n\t\t// Gateway / Router\n\t\tip, err = utils.ConvertIP(c.DHCPConfig.DHCPGateway)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"DHCP Gateway -> %v\", err)\n\t\t}\n\t\toptions[dhcp.OptionRouter] = ip\n\n\t\t// DNS\n\t\tip, err = utils.ConvertIP(c.DHCPConfig.DHCPDNS)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"DHCP DNS ->%v\", err)\n\t\t}\n\t\toptions[dhcp.OptionDomainNameServer] = ip\n\n\t\t// Set bootname path (used by tftp)\n\t\toptions[dhcp.OptionBootFileName] = []byte(*c.PXEFileName)\n\n\t\tc.handler.Options = options\n\n\t\tlog.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)\n\t\tlog.Println(\"Plunder Services --> Starting DHCP\")\n\n\t\tif runningDHCP == false {\n\t\t\tnewConnection, err := conn.NewUDP4FilterListener(*c.AdapterName, \":67\")\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\t\t\tgo func() {\n\t\t\t\t//Close the connection when we're tidying up\n\t\t\t\tdefer newConnection.Close()\n\t\t\t\trunningDHCP = true\n\t\t\t\tdhcpError <- dhcp.Serve(newConnection, c.handler)\n\t\t\t\trunningDHCP = false\n\n\t\t\t}()\n\n\t\t\tgo func() {\n\t\t\t\tselect {\n\t\t\t\tcase <-dhcpError:\n\t\t\t\t\tlog.Infof(\"%s\\n\", dhcpError)\n\t\t\t\tcase <-dhcpServer:\n\t\t\t\t\tnewConnection.Close()\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t} else {\n\t\tlog.Debugf(\"Stopping DHCP Server\")\n\t\tif runningDHCP {\n\t\t\tdhcpServer <- true\n\t\t\trunningDHCP = false\n\t\t}\n\n\t}\n\n\tif *c.EnableTFTP == true {\n\t\tgo func() {\n\t\t\tlog.Println(\"Plunder Services --> Starting TFTP\")\n\t\t\tlog.Debugf(\"\\nServer IP:\\t%s\\nPXEFile:\\t%s\\n\", *c.TFTPAddress, *c.PXEFileName)\n\n\t\t\terr := c.serveTFTP()\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\t\t}()\n\t}\n\n\tif *c.EnableHTTP == true {\n\t\tif len(c.BootConfigs) == 0 {\n\t\t\tlog.Warn(\"No Boot settings specified in configuration\")\n\t\t}\n\n\t\thttpAddress = *c.HTTPAddress\n\n\t\tgo func() {\n\t\t\tlog.Println(\"Plunder Services --> Starting HTTP\")\n\t\t\terr := c.serveHTTP()\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"%v\", err)\n\t\t\t}\n\t\t}()\n\n\t\t// Use of a Mux allows the redefinition of http paths\n\t\tserveMux = http.NewServeMux()\n\n\t\t// Parse the boot controller configuration\n\t\t// err := c.ParseBootController()\n\t\tfor x := range c.BootConfigs {\n\t\t\t// // Parse the boot configuration (preload ISOs etc.)\n\t\t\terr := c.BootConfigs[x].Parse()\n\t\t\tif err != nil {\n\t\t\t\t// Don't quit on error as updated configuration can be uploaded through the API\n\t\t\t\tlog.Errorf(\"%v\", err)\n\t\t\t}\n\t\t}\n\t\tc.generateBootTypeHanders()\n\n\t\t// If a Deployment file is set then update the configuration\n\t\tif len(deployment) != 0 {\n\t\t\terr := UpdateDeploymentConfig(deployment)\n\t\t\tif err != nil {\n\t\t\t\t// Don't quit on error as updated configuration can be uploaded through the API\n\t\t\t\tlog.Errorf(\"%v\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tgo c.serveImageHTTP()\n\t// // Image OS\n\t// go func() {\n\n\t// \tfs := http.FileServer(http.Dir(\"./images\"))\n\t// \thttp.Handle(\"/images/\", http.StripPrefix(\"/images/\", fs))\n\t// \tlog.Println(\"Plunder OS Image Services --> Starting HTTP :3000\")\n\t// \terr := http.ListenAndServe(\":3000\", nil)\n\t// \tif err != nil {\n\t// \t\tlog.Fatal(err)\n\t// \t}\n\t// }()\n\n\t// everything has been started correctly\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/services/static_pxe.go",
    "content": "package services\n\nconst pxeFile = `ea0500c007669c66600fa80fa0061e66684c5245542e8c1621052e6689261d058cc88ed8b840008ee08cc80306d4038ed066bc00080000fcbecb0331ffe87005660fb52e1d0565c45e34e89100746d658b5e20658e4608e89900741db85056cd1a720a3d4e567505e88800740ce8ac00744ae8ac000f85e500891e6d058c066f0526817f060102721526807f082c720926c45f28e847007423e88000741ec41e6d052666ff770a26ff772426ff772626ff772026ff7722bed603eb7f891e71058c0673052666ff771026ff773026ff773626ff772826ff772ebee203eb5d2666813f21505845750a260fb64f0483f958731dc32666813f5058454e75f526817f04562b75ed260fb64f0883f92872e35089de31c026ac00c4e2fa58c3bade00eb03baf30064a11300c1e00648403dff9f77088ec031dbffd275f2c3668f066705668f066305668f067505e86304e8b001b02ce8340466a175056685c07509beec03e84c04e99601a165058b1e63058b0e69058b16670539c177039187dac1e806a3790583c20fc1ea0401d183c13fc1e906890e7b05be0204e81504c41e7505e85e01be1304e80804c41e6305e85101be3004e8fb03c41e6705e84401be4004e8ee03a17905e84a01b02de8bc03a17b05e83f01be4304e8d703bb1200e882017305e89b01eb22a025053c027402eb1966a1260566a38305a12e05a37d05be4804e8ad03e8df03eb06be6504e8a203bb1300e84d017305e86601eb2ebe9704e88f038d362505e8880366813e250545746865751666813e290572626f6f750bbe9f04e86c03830e6b0501b00ae83b036631f68cd689362b05c70629050008c7062705ec04c70625050200bb7100e8f2007305e80b01eb1066c1e6046681c600080000668936b5046631f68cd689362905c7062705ec0cc70625050010bbe800e8c0007217a1250585c0741066c1e6046681c6ec0c0000668936b904bb0500e8a1007303e8ba00f7066b0501007518bb7000e88e007305e8a700eb0ba17905648b1e1300e8640083268705f9bebd04e8c00264a11300e81b00bec704e8b302e99c00508cc0e8c202b03ae87e0289d8e8b80258c350535152bb0a0031c931d2f7f3524185c075f658e8af02e2fa5a595b58c35051570689d8c1e0068ec0b9000431ff31c0f3aa075f5958c35339d87406e8dfff43ebf65b643b1e1300750064a31300c357061e07bf2305065753ff1e750583c406a12305f885c07401f9fc075fc356beed04e82a0293e83e02be0505e8200293e83402be1b05e816025ec3e85c038ed3bc20308ec3bf0800be6d05b91c00f3a48b3e2105668b2e1d05668b36b904668b0eb5048edb5068f000cb5058452d3e45423a007f10205058454e562b206174200020215058452061742000204e6f2050584520737461636b20666f756e64210a0020656e74727920706f696e7420617420000a202020202020202020554e444920636f6465207365676d656e7420002c2064617461207365676d656e7420002028006b42290a00202020202020202020554e444920646576696365206973205043492000202020202020202020556e61626c6520746f2064657465726d696e6520554e444920706879736963616c20646576696365002c207479706520002028776f726b61726f756e6420656e61626c656429000000000000000000202020202020202020006b4220667265652062617365206d656d6f72792061667465722050584520756e6c6f61640a00202020202020202020554e4449204150492063616c6c2000206661696c65643a2073746174757320636f646520000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffff00000000070050535585ff7405880547eb0fbb0700b40e3c0a7504cd10b00dcd105d5b58c350b020e8dbff58c350ac84c07405e8d0ffebf658c366c1c810e8040066c1c81086c4e8020086c4c0c804e80300c0c80450240f3c0a1c692fe8a6ff58c35086c4e8e4ffb03ae899ff88e0c0e803e8d7ffb02ee88cff88e02407e8d4ff58c3665167f3a46659c366515031c067f3aa586659c36650665566680093cf006668ffff0000666830098f00166aff6668b0098f000e6aff666a00166a1f660fb7ec66c166020466016e0266c1660a0466c166120483ec080f0146f80fa80fa0061e160e689b06fa660f0156000f20c00c010f22c0ea7e060800b810008ed0b818008ed88ec08ee08ee8ffd30f20c09c24fe9d0f22c0cb171f070fa10fa9660f0156f88d6620665d6658c3665153bb180ce862ff721c66f7d96601d1bb0e06e854ff6683c60f6683e6f06683c70f6683e7f05b6659c30fa06a400fa164a11300c1e0062d1803502d8c0050c1e80664a31300585b0fa1c3665666576655e8d6ff6631f66631ff6683cdffe80700665d665f665ec366601e06fc161f6689e18ed366bc703100001e66510e1f665766566631f68cce66c1e60466566681c6b00c0000660fb7f866c1e70466b9ee00000066baee000000e853ff0f82420166596629f166f7d9665e0e6873075068e500cb0f8234010e6880075068e900cb0f8230016685f675068cce66c1e6046601ce66b9a207000066baa2070000e80eff0f82fd00660fb7fb66c1e70466b94801000066ba48010000e8f3fe0f82e2008edb665f66506685ff752ab488cd1589c76681c7000400006681ef7902000083e7fc66c1e70a6681ff00001000730666bf000010006658665766b920bf010066baa8e10900e8a7fe0f829600665f67660fb224246657068ec366bf4801000066b9703100006629f9e8e3fd07665fa3f400ff1ef2000e50682806cb665753bb0606e8d6fd5b665fff1ef200a3f800ff1ef600071f6661c30e1f31ffe85ffde847fd6689f0e856fde83efd6689c8e84dfdbe7a08e83afdfaf4ebfc0a496e7374616c6c6174696f6e206661696c6564202d2063616e6e6f7420636f6e74696e75650a0066b8101b101be9acff66b8200a200ae9a3ff6631c0e99dff67f6450bff75126766c16508086766c165040867ac6788450467668b4508c366506652e8daff66c1e80b67660fb7541d0066f7e2678b541d00676639450473176766894508f7da81c20008c1ea056701541d0031c0eb1467662945086766294504c1ea056729541d0042f9665a6658c36657516689df66b801000000678d1c47e89cffd1d0e2f56689fb59665f0fb3c8c351e8dbffd0d8d0d4e2fac1e80859c3665751526689df66b801000000d0c189ca20f289d300df30db01c3678d1c5fe85dffd1d088c6f6d630f220d584e474dd6689fb5a59665f30e4c366535166526631d2e81bff66d1e8676689450867668b5d046629c378056766895d0466d1d366d1d280f201e2db6689d0665a59665bc36631db6766397d00740526678a7fffc0ef06d0e7678d9c5bfa07000031c980fa06760f67668b450e66f7d026678a0c07b501e85bff67aa80ea03790230d280fa07720380ea03f8c36650665166576689dfb9030067c7450c020067668d1fe8aefe67668d5f04742067668d5f02e89ffe67668d5f1467c6450c0a740c67668d5f24b10867c6450c12e8d5fe6701450c6689fb665f66596658c366b903000000eb0c660fb6c967668b448d0ee31367668b5c8d0a6766895c8d0ee2f2676689450e665667660fb74d0c67668b750066f7de67668d7437ff6639c6720c66f7d067668d34072667f3a4665ec3665780fa06b2077602b20a66bbb2030000e84aff678b5d0c83eb0283fb047203bb0300c1e30781c3ae00b90600e846fe3c0472406689c783e70183cf0289c1d0e9fec96689fbd3e329c3678d9c1bac02000080f906721480e90466d3e7e880fe6609c7b10466bb9203000066d3e7e826fe6609f8665fe936ffb9000867668d1c554e000000e89dfd751867668d1c5596000000e88ffd752b67c7450c0100b509eb2afec167668d1c5566000000e875fd7411fec167668d1c557e000000e865fd80d10066bbd6050000e892fe80fa0688ea7602b20be9dffe67668d1c551e000000e841fd0f842efe67668d1c5536000000e831fd0f8408ffe97eff6689f266f7da67668d4c17fb67668d1c166639cb77282667ac24fe3ce875ed266766ad6639c87306266766295efc66f7d06639d873d6266766014efcebcec36601f266bbffffffff67ac30c3b9080066d1db73076681f32083b8ede2f26639f275e66685db7401f9c3665066536651665266556766ad6689c26656e8c1ff665e726e6681ecfc1f00006689e566570616076689ef6631c066b9ff0700006766f3ab67668d7d1eb8000466b9ef0f000067f3ab07665f6766897d0067ac6766ad660fc867668945046766ff4d0866ba00000000e808ff73fbe831fc665667668b7500e81cff665e6766ad6681c4fc1f0000665d665a6659665b6658c3ffffffffffffe100000000749f70230b2b98f0793213c56f349302d136fe64cda1a6dbf809bfe3ab96044c49b99ef76525d24e9f1f332caf09c5d6a0ce2b6eed8ab0730dcf6eb44d48392330ee87ca71a679f15717d766c367aabb183520ea82fd6d5d3b4b1f6b5e7fdefb227d1cbb9146c58b6306fa62de4755e70d83ae5fba29085bed85837dfb5d1762d1d9e5ccf8b988894cf33fe555a028f1fb385958eb7fcf59ad3584a9705b2957df7e7f1c800ae9272ad37fa28f6157448bb17345601f1564c4f373084917b888acb7bd5b961398ab8472493093aa0a1769559a04e1bcc3ffd77f100034439fa6ffffffffffffffffffffffcd0500000046039865d1e6c44899130938f923c1872ad016328574821eaf78524b92a7fce195df4a226a41695874f9cf5e1cc08910529f4593612a0ebf37fc0eb8bfdb5084f71ba0271ee024b0617ae00046cb092301b3e9d6f69e6a3929931a271d0c645bcc8a6ac1e0d14dae0e19ad499179e2a4d1ba390ed056a2f13bd25c9159d41fbac9aa036e6ae2b44dfa3052f100114b173958f2f891f6e544b89ed8fc16e59e9767f7ff39cacf4253a6120e792434ddc0a3c49259c0147705d5bcb551d549391507c74b4c3278a74a2f42674a3deeb655c4c4f608a317f812cc3953215b000c892e759a1b32d34f8bd4dda9ed991d578ed885d178d2e1b74a2749024d7baa15ee75fec2b57fbe8b63b85320025e8a6e7287223c3a987fdfd31537905ccc17bdef4f93e9996377b280aa868a13fe0795357e6ad44ee92f92ea810d2dfdfdcc4b0426590ba84474aff8a45d4269f0702971a7f90788075a0385381cf966be8b12eb1ead1a53b4b189d944b622d63d8e02f7e4875aada9ddc4d93e7646c170e4fa16a73f66f162b5eeaf57e4c0fdc180d357f8c40183f5dd733fbe62286bfffb015193479a5db52c8bfff3c16893cfe8fa2dac0e8f8e0ebf42975a0f958e8d6dfc30c1e89cfa6fd983340ea94d88c76c14c43fd3302392d1cd34e5e298b1338b2d685f2c9a4edd0cc94caf79f546754052fba2b3c7d4145e19e6e0eb3b71e1def8576bbaf90637026b80a7b6f8a834d68d588b271144452b892260fbafa601da968a0edcd74135be1e203785001d9d8d27decfcd6ec176efcb4df50f4b3fb572baafb935c7e8012e3c41cf2a315d549325b257d17dd5ebf7c5d372985d001fdc82b1f9de949756fd61e9921a789a0617be365cf02cfbaab02a9252ebe415ff449130da7e190ae27c0de32ed22b8884e0c12d4dfef0fe5a4afde446332b2fd010b15659ab5e3dc3f6d79e6cb2acdfac5fe3cdf99b048789568f1ba8e057959b47054b1c26f451638d9033b3f3f5621de54cfca37629455a34cbf0aa73ad44ad3127fc9def3f8984ef88b954348dd5b55f4b668d17b589487bf8ff5b8d7b76d4a502cc850787a2caa924e1b33c203bc563d0f2f97127124011d156411bdcae996e900908882e90fd5d0bdf637443c66bca491ea077d58f9c2df79523c5f3cc480b930281c96730e93a20612fd1f00f89db6b52373f9491c2d614d531db7e80877cd4529f2cd65bd541d56e461a64c0a30a36852a2011d5dbe4b472aaace3351206e32f5e8fe75f5e3d8253da75554675c51b7df80b562c903d582b53739e082940228f69551a0b3fa5d3b5b13111a3ab2075b73af5876fbc3c898a899a3f6f7c007e73b3ee7bdd9ba835f6e27f69a11f5ce474d0c03c55e964d7eaabe7eeacf6faee8e77c7dddd9cd5ad3e7b47d5f2ad1db0e6db0637345b1a62bc5641a48615afa2f1c60ee5301c97eb59b330d3fbc506026daa20785aca3d93de7d919f97dd63a7033946a7abc48523c2a2d3062a0119c72f35b767fc1391211f291b4d47d62ddf4df5785a3be298dbf4ab94b334cef68f26aa6a6a6a4a0f3cb80e7d12f14bccac150c83ea45ad93093a3d4ef48f2591cce1bd869772fa8432ffca289ffab74e18d437af6545b62ef8e4ab8d51435fb3d909436e04f24a3f13de30d8253f08c69664457c8be68542fe317a24cb90f03dff12e8b2e7bc1377a8d0d2336fb730b7936b093907f3c04955ef0112bbbbf7097d59c0c24dcd21862c80f7cd0d8efacb65337c99625a429769953cba9f0ab6fb5fedfac9bcaed97aa9b85a72442f0251eb2890fdc95aa9713b5739822ca66afbff8a053906d94d6b282289490c66205ea05b68aa183f7ae9fab7e568fa64e437674216290946106ffb0b66efa3e7d051132297a288020dc753379a65f9deb8296a81b3155ee04f882bc556b3ca714b5d30f11a8c0db0d3335f86e159b695d3ef4be767c71808cb45bec3407dfb43fdeaba9899c77ee33d311ce784d92445cb2c9e35155838522c7fb7a74b93138d456e06b623ff40b1606be60458de8ff40875e0156991e9f562cc32f5f4857ea832acee3a0066afffeb50e27160a05f2dffffffffffffffffffffffffffffff440000000000057cc0af7c191a0640ca0b40d5bf34657c04cc3664a7cd929ca01d4cff351d96eaa094c3d4193469a79f90ecfae3ffdda562f657bc52a64f9fff699e00001dee6a27ffffffffffffffff10f400000000695c0da3041ed452661d7bda46ddff2fa8261e0591c1d698aaf2a6eb5cc57ec38b0ca565ddf952114dccaecf1be89398af4a48bb9ab0e19770c3846d4d114ea6bdafd800940f6f0ea3bc0a822b0edb76c0052a9ed305ea1c420572ea39eb4afb391f8ee91e0309480e32f0ee85494199b0afb8df6d634a5eef37f97e9a5324d37c6238b61489e2f5c3af2a7417540c7a88fd6dcff14d68f6cf307e047dd0affd7fb7a2cbe303d2eadde9cd4e8f3fb73b1edbd3f5fe2b1435ff2d6a4ffbc426da081b2518d89edf1528a1b3d7d6680a9644f7082eda1323b4f228807bf9723c28d5e5b6053912d83204bb4c3c9d5a81e7df35cfaeca62dbc113b31da44ad6171d5fc1b9b064edf5749620b2a7ec20f1915aa0d03e41c249a5c93a0def69ef5e7031f364b52ea6069b341c2b213acbad0fe3a1f7e72eb67a104c2450ae20f5dee3852f7d0039451a8107846ad65d8be126ef26927f204a1808a71b349692069791b85c8e9faf1f75038bd685e89e8d7b29dc13d81be498d30d05c22c9b7ebca80224f5b282566442e82f491baed53ff310e2ab86f4fc5bc458512d3d6681f9b79cef00618acb236c5f8943db29679f351da9d749c89fadbc7950a6e943e3b24061badff07f0a779646bcaa114f44d95696787b23c1ffc8058eabe1ff43e4710d985dec88ca6784db90300906ce88c2513cf9c2d5ea8e8b9367bc83c2927b1800257c763a9aa70b46fe53d5ab2d0e6976b1281ca11861c6e1544bf41f31ffee7fd78a045eb9600920b1274dbb0f8e1523101d37dc703d9a47abcf0f724b63e7d43afad741e8700cd956aaa89c300cb845525d94043ae7fac98b0fd845c03e7c77e17abed6a588f3a6ba5038a6d8259d848f6aafcaf8c5073d6f6e3e5341afde4840c8ce83ffc46b8aae0be0fbf276bf6f92be287c4388a2d743e1af2416eabf63d8447844fe01d1d8b7b5ae688becb7f0404a15476b883f3555ccc46713a9a756df7751cba55a5ee61bd20633a220caecdc49ff2c77a0b985dce90c2e3e994de2bae06e9090260796c18e7fce21f22e423ff953836f74c34ea8b47c92494d76ff8f0e0bdc90975599b155389c3d98c53904fc04fa06b4bf2195ec86f07ed9799cac1266f9bc18b1fb2dd21cd414113063a3a47bf4061c4332c6d810047534655b5fe0e9e6ff489150196ee54fb128435781cb0ffd4d1ef3b9ea4f2a009cd55bbe1f5133a1626388d3434474bb1091bcf6adde559216c7462b6a2d1fa9dc42dbf83e737363b95070b27eb136521f0d0f8f5e003772072b7f917fcdeeb26018c75c5a0e85d19f2637cf85e75abefe5527582133e365395993cd1068b5095ecca90ea73883bb9a9beca07e30c34d79648e2a4a9b45fe5dd6d76dcd77be2451f189c4fde4df402597fc6efa99a1a9708374eb4defbb17119f71c62e54aabc23fb518d880974c403a04ed92f47e9cf4bdb99f0207d1b425d0c816c70050d32ebe6fe7be51c6fdaa98e45b02130d3aa6186a137803980b461873bdf18ecf7811f26c3797791544a075f66ec542399c57c91c19d71009a52bb341e670b32e6bf5d5f63e669fa7c7ff4cf2cdf0aa2739315ea5e57d7684bd2ba9cf93188241b29799dcf701a39528ccf19b4416307a3b1efae4014ce1db803f1ca05f11eb0f2c2db838efd7dded20a3e1578d836e5b275de97de1f0c63b39b2be51401e376d2aad048ddd1d0bbdfa5cec0edaa6adaae830b40d89b83877ff8c571c75243a94143c403619f3d4f3c45e2560eb3a796fb03ce981422423d1564d5d572b57bb146a03e24481090b3930dfb0272966e03874adf02dbc646bb5ea1f8b182743d0878d52baafc20b4d9d5decd7db6cbb8ece612361ed078b03dcee9a7ada5b0fc889c77faf401f69aedf9fd354117a2c985cce05430467d55744d195aaa075921e8c1d713a7e670c6f847cc1bce6b2208476173c8b5edff5c09b2e13c7671db0e994f79ff15416e64b50177db17ff860c1068221cad7f4de015970343607c2331fc5a40c8207a898c3e812024d47ceae8532bd39dea9ad995880905ce09391fb4fe924342ee05138cefdac439da1c151ec72f050c67e59aab7d9b14883a3c1390498a3c7a5bb71e159904b52fa8b9c4c3157e03831c1c31ffa629e865caf3cae0b415a0a1506856d3665432637018ae4c9b3fb29b60e59dfeedddde5c95dc4ad80b256a246ad88227760b433668060cdbf908c83d9ff840a5c0a0e51c13b647c45c209e81f89df808a624cdefb9aa0db11c7e29cb68a792ac74ed4528710afc9b272593fad8e91f126bc512f28ee82b5b4600c23134a1e9dc7f3ddedb5a0cb1921b4e75c24345bf649bd35f5346c63762323adef8ff1426c10ff50e8abfab1abae16aae7944cd1d6c13ec5f591f0820fbfbda8b1b95d1190a6ccd96925e1b26acbe4f678160dec7d9555e35e8c9b0f3d5e63f86006f053840de2140209790813b1b75fb529c57bbf95b582a2eaad38ee587987ff70367540a252e9155ba65665c51d4a3f62b24b4e957a81077589c3b46a0fa17d16d3af26834f774c3d9f32d8eac79e096f9b02b38a5d7860a070da508231909a97c9ba4571cae6ae49dc92f9a05f49db3dbe275e3f1dc18d411d4f379533f1128dc58defc0fd39675ee708dad2bce61b4898b573624dd8b962de77e1cdc6acd054ddcdbbf76b203acceb1955d9eb018af29867f146f7cab585254de8192c9dbe857ccca5592dfb6842ad7591c62481e52e573b749989de0d449617a56775bd13c27f8315a4b4c7260b707628b0f24002e5f914ae8a3162c24329d28a80cad5d5f2d8d48985f8ab5cecc2e86e3a0f9d07055d10a8fa87f6cc5be0a58232c88a8e5b7456f335e3de089aa4b15f072a00538a531e19d76442aad880e935cd133ff3dee15a23e06bb9206091d19c776b0504efbc88c20fcaacd56557eff525ab63c08fc7c1e3f5930374ee6a1a7304346b6d5fbcd30ed7227eef475da60109ec836ce1ea25af91849b99ed3a13b1b62ef697ab74868eaf97e8d023abfcae6e5fdf1e9bd6373cce89a5e7ad28a5d6c3dd8400c5d08b1c77bd2fa8d277b28f00a2f97c6cad7be17641cfda291f0b7358f333060ffa8027af088e1e1740fe446502710de1d4ccd4c2ad004d0af64804544e7f3a4caf962b17324dd3b9e7743d750cb0e64314f142252152d94d40009dbd8fea8dad51efa992cb963a0b23554e2b8f89cec9a7f76e39fdecb6a55e530fc8ef8a6ed523c3d4da06bfe7ef316fc4fa8224382ffce37d74b05edc5a8935f6c017f786fbe5214b584db6ceb28c0ddfefcc930fd72889ab7d7ae8dc2fd0173ca1c6fb057e10e9b34e90975c5a7becbc925c13ecc353b3dd69572d5c6bcdfb83a19c64080bc16e89d6d3362135f4c1393d2d4097473538c763d5e3b5f7cf25c6dae115689db2034c4583fc06c8c9751aff8b73e575ce00011cc19e3476fa34a5955ea8dcbad8cfd77de55ae28ae58aa4a0740c3e66613d8f03ba090912c61e1332f5e82514cf64c9463d1c507e7a657b14124d708b2d51b37b1c566b5a7ebdf23317fefe06be9b7441a28f478f940222cb100a86dc5d69480e2616471aefb69bfe1d91e4cf5ced120fa656d4f5dd8781853f9ae9070c05effa054c755648080936445125d5f47bcaaa88f87954c4265d2499b5e8554df1629b99178ff97c925ef08981591d8360d4d1e0bfe6d6eb56bff40fc5e94f6e6e61f5fc422384302fd4f9c0bd9eac0ae43954f28cb75f8ab237b56009ff8599dbc1735d2cde2a4f98171743b662b9276325d128b3b3b31d54a92396a00f4a6999c97205fde6fabb49199c132d9d9c767a455b6b2fa0bf74c12f827423aff8dea613c55d315ba17d43528213ac51c2fa82978be681699226f4f5aa4c651abc68fbae0a06c659729ae24a46c915f65e4757884c39651a2403fecc0b99de29fe03d815bdd7836cef5b923c0eb6f249e9af8f0e0a1fdebf64110f322ab6d47c6ce1e0a1d4cefdb7ba72bd05b148dbc41a84db1069b3fb2c4ddfab8ef0255ad648c59944b86c34b9d5685af27cea9aefde3b7c6f6c4e21c6bad88de274c8a4536be9bd24fc053031c9f37d03f203e3498abe78a2b3b2e9b7d96ee6ceed269a113e02129cbcab5dada09c63e15b0fcc0bd7f045908d6c6238f6d4bb90acd70dcdc6e81db9e2c756a828cf15b7c83eb58fcffdbba29aa9299f14d637d52b875713896d325b75a5e7d6fd814b729a2b0b5d43b15af6031213f3185ef0c1207e8a394e1b336e5a911cfcd7cb9455bc64e8fecb67a23cdaee1fd8eb652f77a6ae5eb2d6f51561af7894038dbd88c096681c8f97aa3affc443b0cee9790755126af4dde0898702d7b5e3bb72f555bf98db16ce29e4becbbc09fca212d7634870e71c19bc27bd1438fc5c36ee922f85cbb3e939ad29cbee54c9809e2c0d20702a8f8be23c640d9f8516ece1bc8c57ef6720e85d0fa959c86e8fb3aa9e44de16acc87ba58cb39993666755e8d6aaf94d1905994e2df237a63006fbbce225c929075ea749405a438ea36004e4e944841c92de256e47da39e0cd7dacd3f93ac979b05a3997a4b1275e3807f53ac5e69bfd0081ec88952f3950f891669e71d2528ec9f0d5bc56a9311d45e110d1251c08c175a8a17993ff1082b1a046cee12e8c87ca4334556ecd6e74cb3a09b49f41e67a81dc805c02115fb75fe65492c243c950e7b0c7ced8f3fb98a27cb4bd1237ab3562b1a3e92a375f86f2a64eb2d507af4b4aa064f18769fea11e5c6a662a0b57b4bafb150622f934127256ba573d495f6281031afb927da6a3b1529bfe7fa3aa316a08a31f075ecec5ff43e47f40226d65d3e62224f9d270f99570c6815c4824e1dd99ad84d51804adc14a4689e3297bd465c18ab3daff0a77fe49e9d6bc0fbd3ec5c35db162b9b2fab0a816b1b00c7499550e71343aa126ba4b581c915dfedf6712f5ae9a54e38e200bfb3888954aa7fbdb00ef2f28beac162daa7175b95b8c2b6bfcef4a01d505c7386a575c4db7cafe265c251d7150e99a437e9f2ffbafc6589b5b10db772e3ab62cb17156810c65b17db5ff84da31f75e02a39268a24331254ee948ccdcf75ac5bec82dcad0307bdbde010001875726e9d9b0f28ce3e976656a805652f9e658774f4f17c80821bdb0038be2208fe81c7ce0a309a496a6941696fbbad9575bb14fcf13bb732464b940a306e4dc416fd9f0701f76644d1ade9bd206cd7aa3f293f77efa426025b8f7ac88a681943cd2865cefd482d6a66a3e551ed6f270bd594e9ea7728c5d577491248d3e0774f214340bac4c5731b77b1e3fcebab16919b4d7eb170a725cb93257ba155ee987d3b18d2e21b200d27557c93bf3ee0f3969c50acf487141da5f2e2fd6c84dd0427eff02de5ff330837ad63ea493eae40092f0398f963bd58ccbbef50ff40bffdff4d28ad98b93f20cb857f62911a3d38d6ecd39496b9a009e32e687a8aae2c634a4c66846f09f1e81cd661780587d7076d069bd8de96ba9cdb72d4986d0fdd2781d6aab6f7c8e4d7c1386c17cfb8892934049fc36211b3b8742bfbc6dedc344e569a56ed07a4b5c94fdf069922b1c08c0c5a0689bcdeaf493495c692f0aeb3a65a37ace324ade32c508446dbf42ec7849b3327b2744f1e06a7211d9754568115be082564e3d5e481ba6fad89be0f5987e0544c642c1b44db3ebe5079309c822f27342274977a19414d136b77d270ea3943992d82a59f94824621ba0778078a57716b085ebb782062cec89477c0a55467559342b5e1c3eebb4bb5d24eb628b9cf0fe418c6a0df917e708c9736879e3c9df9419df7412287dae41bc19e16038b62b3f7aa4c2d6f98cc3c27347c612034fa09611c8068694802b1995ecee2ee8ceab79f72a2c337cb896cd680dbb48b2b46fcaae2f05874306c1c07ca47b36266ac4e1144c9fc0f6453ce5ed905ef7fd4e2ebe72f9664675d36761c6f001b9206eaa02adf0967b64a46217798edd605483ba159892e3ea672c79bdf7666db42124e2d9084c927629c3e312683bf5643df4fcc7fb92959a3a6afdd751e0c3dc7152d059ce1cf5c884f03cfad0c1b5885d4af46fa86d8a1a62130e8406f3591389c2a5a8c094e36c0ab3c05d87c2f308c2caea6634260f0d16fd961f0963ca890d2f69cd0e1ddc50d37dd6ea4d9341fed4024a8190b7aa988468e99966f1e1f059f9028bbd473e3736b0efe26bb567abf53db5d206e806bfcfdfb3e3d2359d26ccf44bf74df30ef6c450b13569506fd27f1ba48154111a0e4350a6fc0a3e77f31f0d58e780e6d87af89a40ecfc7e33546e99254670d00c3ccdc2e718eb7e81ee61fc6065986fcf48e8c82197f423752f1efe728c55d35d740abd08efdb1376ad28303a3d4c756c81100a4c2eb0a1a0bdef9d8e606eb40b368e6ecb82d70a52f49904f07db9a03c2a91e40d740f1f185cc67c73aceb5ef00d65647cb300feaccea59ecc06dc768976ca1ccc62b7545f9f1e9f64d50dd63b650578205f8f0dc4ec08d89795be0cca030b555ed109a932e4ee2f74a757e0bfd046589c72ec7e40a5daf6f1a436cd3ac7818145d2529e21cb79d33b00b7ae104524708e110ebd80d97299afc68defec91f2eea20f415ae8c9033622f39e8c9d12d9e537c474b8e0de91cd53507554738ee0746e778b4438fa4174e0c76f594e53ff7636889fb62da47f1ead52844ee2e6bdf2f9e71dda246e413d0d170b9773b3340ed9033021d3c2c01d061d29084581b54609c2332297d1c0509b1204c598646ef8d0a3ed92778b4f8a24159b203499d4d91d665348999b2e07ce89a99ad58319d819bc37d40a2fad2f373f6255455f03f1229e28fadd5ef5913877f0e06e3e352c234080eac07d612a680da21446657efe4dad2eaf59a4b748e8b908927fb0fb0e5b3808b6039cfeeb4fdd1f0da94f987221be5a4f26180cf6a31d4296b52d477eac0f3e09da90048bbb179cfe3c9f2a006712992737ebba11b2a67a53095ef2b8611d72209c7ef313e4ead25aa3e9992722637a92b72bad54664ebc696ab32838109cdf2b7d360746a11c543eaabd73b8a0444092e61a5370506cc0d6c62cb4967c0e72e509318ba3a6fc94a7c2dd9a47a5f48e30be1adbf8ce54380fa7b0219ce08bb8ec09c89f2134be793686e452dcf0202f531815096c0075a1de410f443ffcf55a3dc675fcf8e3e4fc53ea50b0391528f6e200c98ec76b3c1b4f1f87ef5688effe85c91663b56540ad47a8feb2e1baac0d1a217ee217b0372758be4826f21d0ea6ac0b9e39dc3e6c0d7d9442a7a2ce33372a53aa050dfa69c9ce494142c86934d51b8bfefa89637c9ef7834a607c6a22e85c9ee7a79b473ee8a9ad518cb2fb4c968ba632e7804a015c41fca302ee5c47819ead5598d8d1061a3c8f31f83bd63b9e8f3a4a7d52d17c2a0cd77b4ddcb084c45a58ad6b19b717e603ed5eee72fa89ff53a17d4509171b137b565415c493944916aba85b9e25b922715f14a0699de9bb53709576e88e03734a130efa75cf87bf2548806f99455249b3d131026166d0c7bd5d7383afe837cfc8d8f737e8fb5ce175e495b12c3be8c13b40616d984557f13da7eb78ed9f1dd0bd09ac44425a9523d80e312a44a6d5e64c42bc613092d9b189359a60d86d8c6a6f367ae54794e29af6f42ce5b92bd469b66fc4d8f6945334db1e9e1de378b534acf45e36429b2db33d222d2393b83c2f713cce1fec449ec35a02f1c34d1e09fe80232af9a8b1fa44eb117761ee9b53cd19e7d48e1b6870b39e2cdee3eda0655ed884cc33b3d854efa86f17be0ba020bb44bbc5a5620e6530fd210404d23c8fbdc46654c687ccf512557af46cb8c659dd65a64654b88b6cdd72d7a0ec6b968805be30b50b743d50ae066d285ac6bfc46c5c1dd3f3c371ac05c2eba6026245ace3863f0cf7c14f533de695092ed1ce6299e55ca547cd22700ba3f3bdd1dd0420e79eca2122f81d21c1d819b78782280dcd0246d61b4935483bf91d39fa56ff48046c94bf904e736a15ddfd5e633f5edd563d954838a9a51b604eb3d12016592940ddcc6972f8e52183bdbad71279de915f612f4f45ed38fb8256ed5c0060b671b51e7957893aa9f8148589242e2e94012afb4d8161394258b0ab68eb1464e1dfe11a5b88ee84f0b4a9b5dc397aa8a1902ad8a14851fd99954df42802f6c631fc752c88c0b92f67ab5ac216905951fda37566b8ca706140e9e5bb2992f1025f89098eb46b06c63cf1609945052d9977b6fad03b225febff9ed06463c3b2b640cc459d6422bb369573edf948ca396b5b2d34b033f65951fe426a70324d686bb7c17fadcbd02eff6ea663ef3e089df02e9fea6295ed0e3b3bcdcd8b1697ea4cf5f38197554389743d6126293b4b57547cfebf7abbb815116bf49f21d62da9fd9d5dfdbe0e7de2fdaacacdfec572627a61cb0acfb6a9c5a5c8ea26f3c2d0f0fdbf4f2a8f52c888e08752a4c7328fa0ca81af975de4962ebb5d7f01b2f4a871f8d2f5d84216b4cb51d40f0e196188f86cb96b1f87299563388073eb1553a8954ca904bdbaa8057d3e1f8d150664430a32fd2dbc012a100fbb77efcf52187b64353649d4c0e440825cb92898ff7cecce9557cc0395a54aab457da4c4912cfd62e3d42d91b2f314f4732932f473beb3a97f1bc65ba44aebf4ec6989f67edf17b835ac02bd8c3717cddec3c991bb7294f194cfbbca1e03bcb87352644ee46b4c660ccfb086c9223e348109140597e49be47a283e0b28c1eafc6d1e5bf65c967c463fdc75e8e10b21c7d7ec85cbbea34f97063a0d4140591ea937b5f31812612025bc160ff48676c02844686c306848b228b8242453ae5d56908d029f879a92c99e0e17a03a6933c2e083e66bf0b7968e64fbdb32ff7231c764ee2e89b3b9fe2357a2461c3f5954b547441ee640721026c7fe2936b55438952130dca956d2360298c4be39ba6aa2d797df69f4e0242c684a9b1aba75133df5c9df3bc643fc8f0877d1fa9a3ea68d4aa67791441c41e69a19a9bee40fde6a39d4bf279de3e2c463c2efdb21377ad4da98829d2ceda3085b71265c68f5541e75448b21828305ce23d412be4f795fcdf698e86a52db4abc7dca882d7422314ba408321f2baa47b958f0954ebece9cad46994b72830be0fadebfb2eda936a1ef45dead0fe8bcaef6c8bba9878568c7656238e2851e281b55ff9e54c4664d63f13154cc2aaa28117380a8f8c2cf315e157ac953dd917cac7bdd136697911eebb7be543575370ef285770e77f0efcc53c165bbcacc16fd77e7231e1e53372cf3a2e890f109742c8ad881eaa2a465c7d79c3557426b74de2be3e79622b08504c95a7b74a7a98083850522b3643ebb77182ef834a43af68b0f54fc38d2378ef0f775b83b76264f4a6796544a4dad1c6081c876829d2bdb73808802a69da9a847fd46eae944970734c695e61a85b283b943638c17677b4f35d2425b1fb0bf16fbff1c1340f80842f3b05d1a17216f8db4a3f3da0415e95299a3a82c1e94e8dd26cb703cfb95302d8cf3dd3e55f9f5db39c32edec92c4b833a9ec2015bb5c38df5c7e4fc7e92b86c68e57c8592d24ad4a02b728a6c89d0c8b47704b41c9c18db5126356964f622138cb466e6ecd0519e42b2050d47f8f8044db43b1ee9fa1e6dfb2c6ecd41bda8b48262d30990d844d4afbe757899c7506fb738437af9aed3ab0b12ef947bb201e2191099891d65be08b1f43c80a7cff0d7f9c8d18a7adb04514b040d3dc7938cfc41f2325930c50a53fdb12ed8798e7465c1a7bd23db5d01ee04524015a1107348d082af7ac6f4e1397b0abdeb9425c100a287e1a957109cbccd396359894a6d656670eb4036ab0adc45fab0d84d7656690f831e21e00e1ad87c0f0edc67872b0eb531d7ba428ce5b75c68b7c00bd5d46b3ba71f82b752aa968ea1516fef2f4c82e998c95ba88ad10c0c6549044d25c865d0b290f1c7be9051bc8423c28b72a01f62f9e182f6ed74aa419042ff856ca917cd754777159d5b656a9504fcc3100bee5b7d53694f86d21f401012a13e02c5e8a4991ce5363a7a81dd4cb8c906986bd5a02e538d4e01b9acf9b35a8163f3ffaecc70ead38b0d58956b8212cf029e179b36d10d100d2cfe06a34669319a716a6f91bb7f4a49743dc12e874c241eca239f57975679adca66f8f27ff00237a37350ba96a4f432cd3410b0bdaa239224604236fcf16ad22137bb9cd7c849a983bba5f6975915263ec88cfbe3f06f8b05c538fe0c6663c773c8d003e42c061b96cea915c0c4bb12f90d7eb365aff32b2024a0b176a2142be49bf89584fc99de0a1af84137708ff2cf5be682f1f1e4d0fabcae09bde0f1248baa6106adb2b39d0c211dab0214f2d26d2b6a3808d68dcadb783bb772373a2d0e0e53fd520c85dd1e6bbdf02c76a4805507097000647914c990f5a9f14bdceac6f70bb48cf8c82d26156597ee835bad01fb2b16707da1eac999872c1d39fdb1dae3783721543390f227b2de3f997a7f5b8deb13bd3077a9fe2a2e42f0f33d9a51671a00a5cad49c7ee7902cfa64f28c010b8a7453127f28422c906ed7040c04d2155c2584f5a0adcb9bc5247dfd5abab2dac3a9023a11896d0f4c3bf8b21bce6c616b4b941fa518555b340048cf3c0830510ebb511348859b49b8899e9419b3a3c70daa16e3e2880b59bbaf7184098a4dde9b2c0bd241ce44d3eab92c0a4cd01c0c1b3291dfd2ca0cb875b1285588b28e6e614376e8becf7a5bfb9ca4f6eae568a25dabf2c5493faef3bbaa90af55c879d63eb94e44d7e31d4f5b16b8ca2bf5463e9ac2a1abe7004b24cc5b2767f3a309faa9db541a94f76afbdfce06c16321a4762d83414825b2a7e51516c48b5e382b60ded825e50e76f48473451f3d5aefcbf539c775027c5f6761be7b69105a7a15a85fa34da7f6e6bf3851f3530597e6bf63a0612e114c7276d67698016ea8015431b094e72c19181a8f703cc70ca5c72fac2db7aacf55318b1b9e11cb67d2d09e381bb03503cad94f119632bffd85bc4ddddef69207cb05a5915db9896429c1a4cf2a530dd2ef5416ca09950b0b0a7adbf305504397b9811e91fe62ffc4ddfbf2bb09f7fde453306c6a3ad6dc28a193bbe742508c561b37cf0de35ac50b2025c662521dc265c28ad47311bfed1d80a70a8220b50f9f0f1c2ebb2d7f6d8e9b9ef96f673f46ef8143a7f1e36a844152270e582c94d1f8814cd1f800a164aa0c3af15ef7284122d1208d8c2e33fee9d9b2a2a30b8dc27b5460e0de3369ddfce6cda4184a7c3c219249f6a1bde120d7977d4339abd8092e0fe09dd02c559ae73bb6fb61f2150575336ae98da59ad68fdacf89ef5337ccb5bf6a4e59dbcac78693f2afcc2177c72451d64fd4419867d023a01ae555593d425f2fe4f1716abd051df26f7300c6b9b29aa946b3d77781eb6000c064042546582ae5efae2f633f4ffb6e02a7a8d039fafe1ee72d9446fe52fc94e78849ba727b0ab43faab2241840dfdd00582bff5105d0a4e18b18c502e17395add231954be358eebf19e774c81a44f650910be98e1ddb71b1ec5b78545e83a03e8ad26ffcfe09cc5d9eec8bfbe5b1547b1325e35195f1b134f3ce23bc652a506551a499e4c6cb26ec971932ce6f851d66f872542d99ff0c4ae3c1faba39de1b657ca28e0070ec45471bbe6dd7db7b4c06eec5b2361fac12a3470f4288f6e7aae5b2a5c2893bb59cd609d7410554ff6afc192b63a45aa1c72ac87f0d2962d3dcc81f83667bc99e0cb60ca9ad510498b7dec8dfa4066dd1189d4159bc5b910eb707fbb599c48f60cb1e2d7a4084b4b6894daa6499efa205830149551b9772f1a3c26ce811c4ee2da96fca0a0d85122a0c3f5bd44216acc4ffdde95880969747689e723815c22b49cae0bd20a9d72fa3281fab7046e50cf4b6a4366e23264a540ee71cd4b0f5e6fa8f9a673ee0ff3e603c25a69d0ce3ed0bd650c1f629caf2891226f2418eaa82cd49869447ffd6c271b311304df0d53b598f93c9308a0faa78d90d46974816fd7d72705a27bcaf5dd15c18d101924b6e57be89551894718a16485405565e98334cfbf81dd5890040f32c7579d7316ddd1ee51fd008bc2d2e3974ece4cd77f690edc34243f51a3fef4050de693179419d409aed8ce972568d4c58289a7b39c701f27df4de6aadccd7532cf7d8b639206abba4749e56d6be0ff1c73b5fefb8e4c1d719ebb7058b5bc08374665a5e8a31b4f858e7d2f2a7b334fde3a033df083bcbfdca443212dbea6e19e42891e49ba35775646bd1ea445d4d819b67598e880385ee89617115272345cf003bb10fc5af9f225479e102bdae6fcb254a45296e884c12221b35a921a53441a573e030ce5fdbb1cbe83c68aa24fffe5d6ea8424ee8526f19be878ba1838745edb202bc7ebfb0062c73aa8fded786d8b4a2159fd871a2fb6d6ae95ed45fb1b17a52756508cb2c2da6106e66b574bc3700870436ee473a96d045b64b87972664fb4639638c2014594f1dbad9ecdee3992cceb032dc4948484d5f34a5717372e2f7a90b697ff561ecdd9c5ae88d7393a8bd6f42c7ccfc79122c830ae1f2277663d3bc33b371fd5d51fef569dcba951440040c8d84dd72b400ef618a29f75b2ced3529c3f7ba02ca828cfa6fd58ae593b4a8c5758f501c9dbdd5c47ddd0d40fb1c1fd5d123d10b1d89e087763d51c5895f06a039f0b001b2a06a4732a9606f9ea8ad8106673278eed232fd97818811df656aa501153cca0077eab7a2efbbcd641b8a5659fe22663145c674de7d008b75c6e9174626bfb0babc3fe7d0d0347e2ecaf1495979a9287059a29211e78c75f1a499dd38962155b6d9d3bcdbc19b45f1a45423eee23926ebacc87e4441023bf62f30d7f98d7b2ed6dd029d804be8259c0e9a0ddffa165615c6a4ad5a2b35739e264affcc3726928891790da69a1b9ad4c1f93e01d50673b004d2256c33b7995ade928be3a3c345a81e33baa02cc5de05a9d7e67492dbd8162239ad03260797df1082693b384bf9d99a2ed29ccaa8a53b89fc44e74b090d9762a248d07e952660c57ff8c9e88c9d4e1338be09639e0c8b756c18eeab307759d26e54404279cc7e74707b7b0db5152516dc428474e9ad046d062f030ebce9a9779b1a21921ee8dcc87f19a9e74e79fa424b5d2c813e43ef662d6bcf57dc52c4048a0ba7139f0e859ddf53c8b6f8d2b0952c6eac8968171d067b083d0dafe57afd07e8399aa39171863d619e00a60db73d101245edefe321bec22fec76668d9a55a8906a2b840753f197f14ebc3bfe9de9f62af3766ceccd982993a0cd2d870f2da7ea75043a44a7aa031f7892bb17e2b2427146af406d5167b4859cfb70c99edfa55bb186edf5eee2db3f792c0c69793cdda5d223edba349fe244045cf9543a16981bd0e08cee2ac81000b824c2e0f0347ddd73cb861487e09c14a47112b9f2467fc0aca329b6037877786deb942dadcad5fe41db865c9b13ad8482f962b7dc5c2243bb20fe3bcbf7f353722cd96ea3682eac6791a61d596b1631d015ca53b54af2998f3e19f44abe6676acd9c10c37a1e147547f9edfa11fddfb4d69502e4927272667a8f4337752877870a6dcb707515bd6ea33475012e9ffd02679f30f64995b6a9ddf1289ba01bba1887f5aec7f3b131a0bc0b5ee25aa8b2ff39c8c286a5fa544ec82085d4bdc79931e255ac8991215985da6c2ad6689ba660595004881fb92eaa8e672373ced1f12b9bfb303f096b0af8ff09b900ae9b275cc20bf6172e90bd34b312c43d5a1f9bd63b62598e016112ac8240b16722a1b3fbb97c6ec42ffc1538c2c27bd54af0af66649bde4cd7588320fa0c230413157d8be40744107eeaf798bd3f61457b1a7a6aba5e8c1ee998ec72e23981c18c509fe86c1e4774e42956e96aeb12fdec0e1f95238c67dc19d9a191219ed35504cf4793116f39a29018c7b9eb54b1570f184e4df98396581df4b5e53058324e7a37df817f97bd77dd341e39ec9f7d4db8736827f474cbb1857b4585a41ef50c9124feadc638aa50d0a57477e8e6309d89f43d3b2b8a207ad0224727efe2b773f86e85f8bfe9b9d8f94a2948d31663377b753832f69e45f6c26fd4365e88a248577a172f6b86db2da4719c7defa2eddfe635c15a6fff87e0c5b6ec3cf188564dfdd501d30131293fe77c3a26dd053577abccf1e922b52d4bd168c72bd849fe6874c7f9db6d0c1249f90b29a8d319835e719972e2613a4d83232d88ddcebefa98aeba925d1d6d41cfb68bc247c4ff6063d35862a8103a1fe6dc70b73024b0160592241649443b9a4485d73b3be2e541930c2ae5d0c53a7ffcc2680a13ea8cbf2e12cac212e7d6b70775425993261482319af6ded4b35c6637f6116b23ee6b8c523cab07a53e925430474b7e61e961cae4fe9cbd94cec4b9a902089afcd120587a6784205753101acae5cc22606daaac228c11155289eb6ed2ff3bfb0b92ceed73c1e06b1e11d5a0117de5b0c556b47743a398cce89915f345ec90994629445f1b7b08b066573191bca832f77f75b079399b5997f7b19d065b7f6ccc4192b6854a28adf66d585a87b47d49a2a432697caaabc329d360a26c7538a94e894a33c83bfc99c12c28d6f42a22d46ded1b9768c5f9ff35a290386309a4a3c40dbf7a8a7227a1526b322351f3ca852caacc0cc40431d386bb86101fb905805331564c15d67ee25122c1e10e06fc48b19a54d22b0b2e90cd748d6855876ed96d3846961b0e891dfe7a6169b95194351fd1fd9278185c61ffd993b4a610081133609dfeeed3e9ee69f63f2463204cb345ca32249f6e8c244c8b38175d06af9621881c840c3130f0717320697c620ea905a1c42a02932848239296c707f74c3303f079677020f03a240247293b5506be91cacfe29452eb366af500e86bc948896c001c1ef4eb62649d6c7bebba887234f2c734e7cb5e768f80221c7e50f7106aae8b44958ad63aeebf68db72dc9a6e7506ba19320631b73831b720c53f8f931b0505d1504a45989de54a399d6d91269748840f95bb7da6cf76ea3f778576d6bdfaa831c0176f19a587bdbfa6c7353de86cb49b9ccc1b37f7a7b33d7764f51730d88a625f73621dbebd8fce55fcde947f372146076a17eac3b7586c1136a25cc8aa9a3684b6e6ccbcc724e457f26b531d9604c8ad192ab14661cd0a27fad32c0fb76d717e9efb24c89294c8d4533d67f066d774b13a9f576f3f4b4641ca6bc7c9291a97ddf0be6caf6ab3bdd89996d98eeb3b81448fac1e001abbbe0e8ea206589d9dd222e12c34b8baf92be9260003c1a0ba958b95fdafb7e13147226e51604adeb27ac7d8c6b78ff4bad30cc94cc09b3f6fc8384e817199e9f6c0df42de56988c7ff1441c866428edf4203c74ce32a8a51d100298ff071a0f5ea63fd95093ef8fea16dbdcf1f34f2101e5c4c6c565084f8823b6033876d6d34a8a00a13c3bc54fd4775e64b114a6d909abf3d685a736fd37052b39435c7ea200efa8c80a76a71c9c61a3bf18f6a07552fb523b37709be6b6018ee38e04435cba245d9bff4b3d3019f07ab771e274276371271740fc9868d22040a87910b9b31487551852a2711cab3ff14d283103634d90a300b570fe408b9b352d2859ba9154f8bee8829707c96146b59b350e3876f205d4732d29303beb0ab8c9ada8f3e6e85c72ab8f7973def85baf02edd25fbe7304d196d02efcafee0c8c61f72a70ecca8213150b1258bda5e3f2c307f91b1dcbba11bb8a1cedb5bd981e3486ff55f617e74ebf2f898483b079b2cdbdefbd9c733340dec1e06615d3672fa2caaed46de022c4f2b24ef99263b99ddb8edea6d75af9a73032a552332c2776727b6393c715460f8b15cc92889df1194f7f984b909761ba0e73dd67705017abb1ae8f311207f5dd9b4ca14e1ea5a982143b6fd7a25412fafe843323410c1b4ad93a27fb16d17c90fb6d6369f35cae02318fba6a02b54504b91489495ad369938df0daf84a8be67647555e6a8ae83a17fb6881761933e190a1b13a4e8dd9710377a82d37358470bee6a3445702773493238a5bb693d7b7268ea557af058a97cf4d1c64865af9f13e8cb67138ec08c774e202479456e722476041566ca6554a8995707d8d0626c1c5feefc30625d45dccca4a5530f9f61356dda9dffd57942629bf67fd2919e6865023cd39565be1caf90c40f699599c263fb64e7548fd56ad1c5af8f39ed03309d4116a838f776a25b43ccfe18387e9b11c488994a25f80279c76e998db40bb319eaf5ab566276404709b1d0f33cbfdf9a2da4299f0571a122698ff54666933a38f9538bc955fd7d7c5ec0fbb48b5cbd219deef1740e53d61c29666cfc842150e5e44764fc60a339e3b371ed2f70f91687f6370c6a559a6127285b341a6f24d4629a3b3dc2d42fe5c23a94840d1165fde8227d516c2c18f51d2083ac0ae7c13eb3d4442c105731234b34964529879e37c6dddac60bf71dfcdfbff1db0e8014825c8dc5135d5e6696d91effc8a7a31ef0876a432bd0155995c6eb16bab890e1adbc0fd723250df5a547c0df41c8aed7d91a6c6c88c7f13b45153c994d35f4c0ab0003d025cccd12f107b6d7f4ec2d46a443389cc144d39fe528a57ae1b5aac11041cbffc6811f1e9095b73e01db70705839ab698611e5a150bf0adca9390114711c1224409247cbef0d17ed01b7be94f07daba9e9e52f37658e381b70dcabad51708b8fa5a90ad7bcd88ab1131e14793228116fca63c18330b6ee66571fdb6918a8d08ad3436389fc18233e39ac2c21e6a0d593fc2ea344420bc0e60dcb1c97679b3d1e29a7cb41f836356c7c4aadc0b4848e0c12ba19d4e61b11736ed089ea854a502b01039332022081c12784fb8cacc970101ca22b7700fa675c222fc060b6d53e35a2215132131b48893113124dc565e2fc8f591d3958678e8d7b52067e89548ed4f9d4e0e9d2c02ab43b4eedcc28cbd1d407a5b78c957fb5b37a9d0730972810d65fc9af3c82396a88f7e9edf74ef31696806d68781fbdbdb67b2edd6f4a22bdeea2b897af4cf0781d88c9d3e86186c383f2ef9eb4f3374635cc10bd0a3c448fdff823ba4f3621d3d4a4af7798292158e7b4caa4ab4c2dc150301f3dc58df9950ce56380e41af794dd271fdc680e7a224a39d973688bc0fa39af2dbea0e1f2fb848e3af1bf9e460e25531e4ddb2eaca997a45947cf7862c26be374e889eb592f28b50fa7a634174feebe5f7aadabb3b6db80a71c8909ac822f265df4bab7919c0d39a670a657978c4feab84caaee22fefcf491c78a6b322a8d8050512707b86c86df2e15e31d80454953011c4ebaa62b520b4f92aa13a79f5315644d4025662f0ff1eb5db51f5680f1c66b3548a2339e69f72f20aded8ef4ec43b41c7152194b3c5919a26668325a6ebc3c1b365eea7df2279747272923ebd5e9f46d3d17f44908c0653bab3b773e22b67a271d696c1f06b2c3223613faf5d7488ab3b8407e709e941fe21b2379b5cbb0bca1e13336259b232e802d51a95f96a58e631e1232937dad7d2a8bc2a24f5cf39413064f7146c9bc5458847390e898b761a56bf282a670d6c274e84bfeecb3411c689a25b1187e5d1e320d0c78e556ad9290c85227a90a793c32f8375c9167fd7295493bdefcebfd61377feca8fe6a463249ba9fdbb431c2354df373007de9606e87fa4cc0b07afa5955b1100957f28b28d900af369c68ee25ebb3b72ea48c567494c5efe755d293309d04321ffdd61f2ff9e6d7e4519b4a77d0e3764d9dc17869e99af4364d17bb242fc8baab993f9cce77da42429c68b92be6fd2b4704e7c5c96b8d7a3cb764d5f18c21bd6d6dcd84fd5a5105860592d065ffcb67d348334089b66407da086e3ba1d82ee0d07061dcc4692525766cd03877232bc8519334b68c67a3a07d181d4b8df3f4eb50852e0a478b72584829fa01079f94bf33ca32625c164d7d58fbcd61db7c1ec6440c10e5d59aed448db5ba53a085d3b4bbc572e29261dcac7f453a89e35e0b3626d33609b843af65d9396306166717dc6277f5897a0e54ddbd0ae802b8fde95052d33f19c36a202133539c63683db482a8de1d21865f18732d1f7f378e27efdea2acb1028b0932af326e500be881777ecc6f0386eef65a3e16f6217005db584bd9602d894985f16f24556d3ed997681b22bce6540ea191b47071a97698a5ff4be5b14536df8c36ca425d047ad6bfaa25b4231e8b33153c1c08d220c0a44f2971ccb69e04647986ecb6420cda59022ab2e3f7b3d9d3df2bd1478359d6e879fa9cb65ac57adf5dfdfc90678e76066d004a428853ea45fb26d441e261ab8bde5d3fa287152fc4852ee6eb7cf619a8c3628b26613fee692940e694d65750be0831d2f15835200fdaba3925ca37eb9a75eb0c14bf645991fe3a24365ce986119157d3be7a14d11c4d5b01b27842d0228041c01d0cd99aa55d8cfdafdfb12cbe0a31cd5ec4464b5579ecd71674534953f997d1db2a14899d753a6d56f3d5f159bf2c53438c2b971e57f1f686ff919aff53ff9961218aea4f19b56e29e113f6bcf7df00495f8ffa43e391e4b0d4d2d2c36d7c3ba787abec5d58e7eed72a7595f4e787a0907df34e0c07df0ec3f435a59ff72f1aef33bfe7d6916ec7aab0ac58c78b33d65e299107ef56c3ccf08afb046e2be48b00f2a92207ae0f9ab0b47bc09f6c6bcee94c25cf94807415a7737438e31faf4e24569f17b7644abd48c920d065b50d3edba23efb09fec72094a249924da15c47cb96c4e535892d009410a3a905107232be41544366210e7ccbb378b0e4fff41abbfee924629f29b60aab36244d60068cb4db09b952ff3956656c1c29ae5f5ad0ca2f14d9694e13479ab5e7974bee72431d7c3f824005e97a7d62c3bb7349025eac82e3f69f20b5556aa2b18bf59618445d668e2cbe08ed136ded6122605117c58f277f2c8638ef4abad2a5f0945ebf7a0f5fd6bcf24b4c94d9ea7227f2ccbf4f80b08e663c7bec0325b2db0998177399ac816ce6341bb7b8054b261527200e3b5ccee48adf039fd59ff9c5b11cd09049ca866094f898280b43ea645a074f700fb7b9bf0768d647157744f2a82d2d298509b28eb5299186139960e6b7bb8f1d3835b51eee130dbded7babc9aff3af50461d2c8671cd0ad0f6cf71cbc7e084a145d66827681c4b7dd901eb0e50be1ee3588c43a4f60e4922e1f976574fbecc30efbd251134a703ed4c0ff9f20b40822017f1d5ecf0c9fd64946042ded63b38ee8b82e3a5c43a05ca9df6613deff2eb433fceeb6eb223426a4a7f2b0337ff614d10b8970cc35faca2007300faac95f054bb6daafbfe255387fbaf79ce75ffe7f1db2afd06202338b6f2e869492f941d750ba202164e8fafa8fa481e2e84724f28bd232edc904ef2fd6a2b3d71639c9f20df7f5218c27041a83c4990060e60f70b636fb09295f909b43ab0d3fc226bb1390673cff5918e5e8ec5d1fe8ee34fa7186aa13a56aa346c868bdd1c235b67e8264198890a04efba36a0b751ca699095e63f02ecb07ac4a5c4a2da2d11fad8c4b318a3273bd82a6e405c11e8d6bac6dbc91c1f8e3070c58ac3d7f16d534f37d962374d226ec3fa37d37e52955a9c40c62d1a6baa7a8c3dc2decb56047aa2ca09702dc8dd00fe046a6eff392b91bb4012f6ad62bb2b87aaf4ed8e18003e8a632b73dfcfa62b1185bbdbae8221499fc9e4c6f7e137e1a141e4fcb383ff11d6bf5517b6c3ffdcadf694ef8bf2f74bcd1099f4f46e95ab951602f5ae8cbc30e14dd9db04e7576c6cf553d67b09a72a4fe87bfe24fa0a0c91222c67d8ef5147299fc47c193a84994f8944d5b25b29f6ee04ff3389c2460c085ed88e6c602cfee783c86c115093265fe88512159fdcff0360b374089b7ba59ee57d8020dccb0dce2d74dfebf7f83dacd5ddddea113f1d8f43db2eb4612ff72064d3855ac8d69bb2cb89719dcd150dffaac7a661e57972203358fe69595752d9ad3f934945dc350f25f390a8a9f438cc56b98826376c1ef7f65d277f500dddba82bb827aa655ce4ae6ef5d7afe884d86955185254e49439b571cfbfe3307ee8479280c73f177a0bf0035207ba92bcdb4e18fbf56c5f6e02e4481fad96f3ec36aab1d1ad2e90792d77a857f8bd6f1e71d2037326a0dd26587eab0158af48fdaa13617b90d35dbf871613f4ff83642d06452a71920daa5f3a55d36106ab8ef9750d5065e0ed5b73b182fe943d1cd8e2e4126d083c713ca70d20925f92c67a082f3eeaebaca203e78d8af13a9a5cb85debc5e01044d83624302f5c716c7a7701c2129d4ae7e37a4d7ad90071876cc3eaeb90e3bd9419a4cc03d8171b6146d0135dbfc7cba1c3962341858d206598ac4f481828c7e550d9c587d28451821e1d59f5d9df2e92e6c740e6aa3865f7b74a6bd35f92856615b8489394d727e3cf6acf1854fd221b1645a342e61cbc2a7c5b122574de91f08007c38c84638c3fb1bfc01676c28d32bac906ab213bfea261220567d2790a31aa8cd4188f523ef776add1a21575b2b4750a03f4837f1ccaec6ce634fb6d1d3733c7a30c2cfd0407cd63d685c3df6ac0f29e87f94ec46b373467026479d90eecaa25cc986b8cd845d09962602fa06fa93706220cfcbcc08fe7675eb7e5abe0a8dd1820d6cda76f97ab6a6a83f8467e4c334a15ad505edf47b9063bf4a733758a3076ed156ff3ae9bd6fb90f68d3612acef65e10c866c700c56edad54724a96e7c4eaacce705090bf24c3042763b98ac8f16e3bdc3d9a98f51ad8ca7bbeaa01d18019d587490f8f338463184ac462453157b7472335eec7041ed9f2bdab16c488d9bbe0c3258740f922f3dc8542c4ca0e717d63763f0fd035981a9a0e7ed7917cfc1fccafd23ff7aed968d54b52f10772fa466f6b09110db22e4d0e7bcd874791beb1e3e59d0b05db4d419db79a216ed7ff92a69d18395d9eebdb78cdb6b74b77c2d1e5ab91d006ee2bb0fb40cb86b0c8c6bd5ed0a08324600668a9f772f669d3ba4c75f099e86d3d92dbc52d6fd7ca8f22958642e75417cd7fa43e06947111eb52a227352548d706adc2b5bc5a3b86984018af34b5c3d719074695b7c91918014462da058562077ca067236db32d00b83879567295abf1019108e290992bddc67e82a4d13de1e1f21d3ffb94d1380b98c9e1a055a68780b533fb680d4f9f5ee2fc73a87d90c1eda834fa434d44e01bc0c8d624f9f8ad2788b7e57735878453fdc5bce8513d73465e1cdf8dd7bc111e785625bd44cf7af41246673e2c10e9ae9edb43f99e4c1ddf1673b145357a4bc462837f46f828ae727f34a2d84f354f21070ad172e0c74ef12e3fd61bd7bd4d285722e14c21dec0044a0c6da8a9bbf1febbfd5c8c2f42f1e4b617ecedbb75a6480cf637d574e7e83cad4a9f0c3bfc2cc6c1f37756f84e6dafed5a4e695725875f7a3c47bd76a2390c874873b80c3575f7b277afe4e2c9e81141e313a6f0435bdd936a459226b25d355bc3ec2c440a09b64a9ec202f26679076421c75d53fc4ee13b677df6ca1f5bbd310f707a171e4fbf636dc3821411b266705a776169f49ee430b61460b0856f79fc111f16cde952ab19a9dbe40dd64dc4b3bf9c0104b11a6c1418c6b286481ffcef3cc50824b1669845ce2f299482c7d67ce9f023177245680fcb03ae3f1721ec5c5318e4bb642c69d099bf5d31ae50edfb7632921d6fdd3039f02d5f7fc2ddd1ee8ed6e3bc29200b5dd10ee6cda24e53547d0886263cf4e134a0f63bb90b7ff28def70c2f96a4caafb06ab0f1e14091cea8dff45850bbbf929c20c5d8117eec29406ba105f47b25e5250a732c4b0f4b7a0d0677c3e9f38eb361b7d10454f4c59505686a3e0492ed95288f786348f5f537424b8a5250aaef7dc4c5b3ca727bee705d82dd9334d39083d3065fb9bc6a5e45301f7808d2d4d46ba3a458f7aa844a570be90263420fc9950d5c7be4189ba5de1a102607786c3dd73abcfb8adcdc0ed096b0cc1841187304b943c00e9061ff0f346b5b6416ffb054dff0de0dd928d98256bf010255659f3f010d400beb1c29db7711d829e16032aa46ef9e13f3a66cc04ffdffc120ec8f61e2bc1d634c536b9383292d1ea495b8697aeeecb62c71058a73d346131a7a164aa9b655b50d386f3dfc9a75f4d3a522618989c28b88143f67641120bc84fbcdd40d9451151cbca84d705a2b61025077fb1a9316301849e9742160242723f2f2ee53dbf6a5b6b27892ef9800c2fc1a0243c9c955036bbbde9e44c9450d9ff5a459d40b4a291353f8d3cec86a0ae4761a302c031e0c47b9f3591a89d62bf71601db5cfd3bbb87470c80cde1871e0819544748a91176278908cec39176f0c2cfe16114008e1cc746632efba71188965561fb45cb733cd4800d0a2a1c982326ef2f16a129113d80dd088274abbfa799343edb5556261de9e8a2e7a8e9fd325ea98b2299df7a7f6035fd56112f3e7607fc5f61870c2f7130f901eebb8a1747a9efea7851f4ad127c724453987769758d2e806e72a2a3a8ca268f3c9f5424cdb1f1c7876868b424dfa744d824aa2edbd2e26186a31281d84072f3acc391f876f477fdc13dbf15498567cb6a763475f9460532bd11410bfca4920f938a7d631b8c56c24776d4e331638cf25f8a1e9fea6871c985bf50e0eba5a08e22dcc1418a5688cca1e478ed153801380411cb9863fd55d8ca25046f2018faa18e6011a4378f19d96ffb4d5de047151583ab26cfa9bddb0ae8443e2c15b8ef73de5c983e40c94349eb294c8d9632a4e462c65043a1914fd474d45e2657b45f0df3c58443debd1f72859f7ae70d1f3276e67f6fa59459bd7117a92e76f8eb6733752cd190bc01c64de435c5f56017fb22b51c5bdc31ac943ebd0567855da2c2300d34db393f5ef6be8a9584163b8cf591c0495c6b50d26f608923754549937becfb841e5a83b0afbf62088ebaa1f3477c49d06b6f507af940b4bee468d97ed50fa21d81231e0b72e82dc68b10f81632bac6d8be85d6681d9e3b8636a91d91f4dc654d727f5da4fe19ed73200ea769056e789fe16c9d077e264e342873a1ebad33218a27bdff2a3d6daedc1306408a35930b83c7be88584a5c2ce0ace46065bbf862853c991403686dba6897122d014ae1d6483e58bdb02efb80590cf00c1cec668f67095ecb9de4489a9427b57b9976730099468ab3cdd14ec9ec70f01a3913a569ef7078880bdc2db5f5f0b67db1f3e4b09edef82da3228274bf22222b5bde7ec0a70ab70814d4fcf30b7a8a6167e0d0c24ffff3a345324b49dd2b04aa6258ff667600d395de22ef34c63080c8fbb119544d4830cf0e7eab594c84e44b55b8e612cbda62a237beb32029e4a058466f90d499d080eac0e948685d37f192c2a50130a7c96d5926981ae49793f1d259210f98883916c837840c859fa00999a5c6894f341554702b3e17f7d027bae6e4c11448cc5a60d0d717e7e5ce67251f6370df8f47d964713d6b4d46f0a0cbc42760f2d5a9582d52c3f37bd77b2388d956f8ce6802f1da74f4dd50d345ff14d980b4e5190d70b2f164241413b826b9ebb62dcc22ae2ad6292c317e3bd7b2553725f9089ff69d4ed8c0cc1c7dee7334bed87a182c72710a7b2240e57d16f7937e9772abe3fe2f535f68ed450c717f912574df11b583e1a558658fab99a78249d40b3636ed5108a86863e667079f019b3aff2b2509ab3d3557a4400e8b97a742e87c4108af1f65472dd7d7aa9b1b700485f95eca5a88f34b47121514546aaf90134d8df9a9ad7c1538a1da976bea7da5f4aa8aec129b4395e549f1b7dfd5e96e9837706b26170d1553c93fa4df654b32e75d91fe77dabec0655a5196eeec683a5ba034c40595074e4c61f67139e4334d05a5b2796435361dead562ccec717ee6a7626222c581c35e42231251af3eb2b072509d2fb0ace121e429a908f9731b9a7d74090fd0f255781af87560bfdd7b679f32333441f0eb4ab31c34433691e22865758a59e31ee49e4deac0ac9f5eb605659a9abe89f34a703e9289ac6be31defd2cabe300a03fa048991f9fc14f464b73d817418fccb4958b510e9179bc38fba60a610880ae80163af67843cb23333ca0687c04d97da5923705344f9f0e6940ad37f6abccc99faa839a44fa30e09bfc0a5155ee288f77b61a450e76bcf6f3cee345125ef36ace27a40ffc369c92463fb6d3cec7ca9ffe5cd34d8b386be1bfe06d0ab4528699136de5f68bd99a2810127c15d3930b753930b0cc4dd1663c07ee11dc8146019a3ab65d044fa9fe9a3ab2c51149f5227f9b521c21524ca5e6bd133bacfd34f28e832a4876a0d3babeee7c3458afbc1af81c716f8cee2ce9e7ffba4b020c990b67b0350ddbba39aa6c6224c670949840db9801d9f0c2125ba6b536336762787f00e9188ec35c35b51a76ca552dd96a4c83c3ab363557e9e8a1cd78216c8a41420649edf933c6b41f9ce8d7beb5ec71e2f11b877f6b5c6266bea7e2b0d1298a9b24c57026e67e6f3b7e4139b2f67b128fdc79bbf69361437f115d7a36b3b2517bc9fc262807f0b448e6c15707b7dade1d87e30dd152e30b3ce51526f91dc504b22e30b52f1242ee36e6baf36cb4bc53074b2341467d9691c208d51d42f66af80fd86334265ca515c022d2c47e5e82120d84933ad971b66b69cc067ced801c1623354431e739085c3cb273acb5f3de123f71ef911af6564f59a93da1ec1bd6f120f00b5c160506186c41f0b721922d9e91e92e31a65adee66638bcf0389cb5985c83518f0e6e2e6482799744bee811b1dc6a05516be66c0ba0857dd10cf137582bd88d5f92b6550552c96fc65429ca799a5f1e69cd17dbb000d130289fb35a19d7ecfb6a81e1aed4b43930384d164fbb8f61a6e371f9eea019c1187324a4100278de80fc6d942a55d32a4647dbe5868dcb2ab6dc62ac2e3f7d6efd428c624f92bd7042dce5cfe678d8b6989608628118775e4004c72071eb3b5bb9e16adf31f9f8a6b25d1163d22fca0de08aa98799060fb1dcd183bf9b2d2b3c60a8db5da3c66737695fb744543161139456edd04ed36f496899e3d10d0a3a331ae6fb08dc941cd4c1da5907f8f7432d0a32f2476f737af23a10c55c16ac923259c705d7db7984762b1128dde398cbb7586fd3a594f456682d14b34d6ad08fd7789b05c287ac9155a90e70770dbfafa3127c1540812c32282574335bcc14eb56225864848a9d474896159e156c7cd6f384afd37026748e66945eee6450796c01fd6f9e6d62f790fcb474e63a0adcdfb3ba746b40e4543443af10cfb59877689aae97416d39bfba42e93d003de775d7b6865d61bc6daefee9469fb4e5414165c2a084588793a7bb5c17be9b603a350a9552d1d675fdec3b03bf5b2bbbb2c2a075a56e623d3f2e4c2f4e87dc1250e3c01974bba3dcc87b340d4de12b9d7584f83e6f08236abe6c2d21c8678c170a237881bec7e35e50ca2413c9fcfec96e38d5a971e47b7a8346fea2fbd9fc36b96fd0fc4f8347546b9568c08e2db0e3eff7606345b959b7d0fc25b6879eee8498ba0c894f4c5a9104fa13568e03c288e12661f97ec078a8abe1870755942e460d4e5b0341682217479c61d239f9962269fc28c3f89b90c688c68106f961b137da39145f73f8dbe5f17a6c257e6594e58ae1b316bec890848b14b292bfcc1ea3b8aeb132c39036943ca4269d6577989a89b6b62aa2bd192b6f264087c250e47fb17cdc315422a9dafec72a1b2995ad7080da43d2f93793a4ae1d4ecb3500742a40312985a1986de28f433f940f60e94be7cba82961c4efb5464338393b3af67bee8909aab18d62d3988398b4500532d3a0de911f207fc7ed91bf358fb91e986fa80ea8b8cc12a61450e790c829aefe800e4d79322c4649ad60dc5b897c9b615590cdd52b2d8d5cb97188d2462c60030383181337bcf194f76b75cf71bace645ae2207ca5e78665c2baacd7066658074d30bb63722882f4371be3467a4abffb4e62de4a62265934aed4d9df142f6d70a4500abd6ea43d2da6fb5a61533260d36b8f27865aa86afa175c380bf92f987203ccabe8a92b558975e02639896c725cdde5f586623076a9a5cbeb27d45d63f1af3823e369597531ac49b215514254d4bdb1c5638db03fc6c2c610a507a3e1fd0b7793b4c8836f4c4d7db83cb7333919824c24f883323433090d96dee3c52dcdca3deb648487d53f1a894232bfccfd8dcb441f17b1308b847dd08959b91e085ba5d24599dac773f4642c379ff510498a6be429cacd713e3ade627d8a57a4bd34a3642e6337105254011bcf395a5e42f05a336ec9013f61aae56cb23baec8df5fcf7d0df684b5cac311fce19eaaa1fedc17bd80ff0994422705a6f45a104a491e320a298147c87aa364a9f6b4a463cea08deaa2587e6dbe8e0de46c1509097b62b4fab6e5877ec1be8d70d881a9dd68eca14f16189ca618a57b52786035cb277da77c7502df7619994f58669ea07dbf2255c42409b10d8285b9f2982ab0d936abe63ab4252d7881883eb98c699ceaef9d0e271701d21d543014b301c63aec8baa9c9945fb645dc575e966520953a252964ef29184ea442b3f188c6e4e1ff7a03eddeb68114da504705bec1783c0686f53ee5199df0977d1f4c77cecfc26e0fe84a9c29718ae5d3d1a4aedc7e38eefa20e7493e5a11a5384d7d8242e2b30f7b71595048bf1ca239308c1c1c50eeb428497ed8945e41c14662e4a6c89678060317364256f914c4b31f2facdf9cff1c0eef791b914e47c0cfd80fa8d55ae5b662105143c279c7a0e5dfdf90aaf39e159758c1a47c4f9aa8ed6fab74972e0f0bed169e43871052196bfc2db960e8f1a88d77a303b5948766ea1747f132722dcc197d9b903724e3c373bcc150a2c742948f1872dafb573e4399105acb795d5d3abb59afa39cd23bbc1b95aba2fe6fee929bee23ba3fcb28a68c2de29ed620c7630c870cc2d7f6fc3dd36f2204a81a30d25b67b2bfc16114b37f849a3f3d6d7a22e9e61d4df538445be24dee32391b43071882ac11297af1e9367dbad03a622890e38e2f9b8cf1f19a4c5a9c8e96e6d3766fc28a58a47e006ec96b16145be00a3eb059e8a86d17468cff8bfb55e84ab11798c1433cc1dd9c7691b302c8c9d6f3e2a918379d1426d21a001f30739cd670c952e34b433cb078d46c34a95948ba60dcb480af24a877b5e559ad121f285ec1bc89f3e3eed1bd13d3f1d56497d7907bfbbe30ddd1cb7f57979bdf74abe10ad6a537d093c34a5c23cb1b89dc32a8961caa752091ca0f465651f51380ca389edb55a681ef46ba3f7e22ab3552aeffdac9fd18b4b7182a1a9a501783c737ba1b66b37a584453c13af04b05c70d5ed057deb09b286787ff3f1b738d216a2281672433e617b2089a3c6eddbebffcb350b9e814868d3a14d1a13719fd906b9e74a69d0cd718898b40efacef447db0cc134a3d9a92da350b5fb1957171d443b620bed52004219eb0655badb4a986649eca801385bb2581b73240361efc6f880623fb25fef2f16bd15414d263bd377d2e86bb8128f7c27864f85f4482a244fef50841c8d991c641f32a4f4eb1562df63bad6600ae9de4c1fc79b45e61a8d2a28c6d3a9697746e116f90e0b9e6acb98168df498aa6da6a274a9ca079fbd434fc21e933d7e5828385125d8ced1653b42b50f912c9f2a117bd3ca716d034bd873738aa92d636b9e626e636e18af87209cea20feaa0eb7347ac9200a6c2a066a8420ebcff94e20fea02fcded419cf4646b7a0d05c2113954b79c8e9a7e4ec36b525e3ac67629fe7b1c5aba29e6713efa12fa4c6fe4c55d083eee42c49519ebc5dce0c3d5ed7f17429aacfcae5715d6cfc4966b9f1df72193cfc459b9abbdc5b9126449b001c96145aa14bb34b1113db34f8267427a584a6e117908cedf59f47c6c8b74b85f292b4822ba2ff54417f1547d2552590e766b1d6428b5815abf75cea6cb6f2ad2a9fc0e32e149149b7173cd89a87500ff45e52b515e53b1e24a0bd10a23c3e17fb225d137f8f6c2ecace49ca6f6ed8406b382b86e0d5b8bca3bbe2bf7d826d989f9c3eb8caca51868c37c99f5c763204f9b2623c5f69c63542627107aa6c330b9dceb79649155f80fe1beba5984079ba923c9b2c02c604afb8f3f0e3eeffa1f4a82c5ba16d1a20e93d2e7d4da8fd0eb47b0fc1d8257852d45c8a571ebfc9f4b9332a36fcba9b86ffde95cb85f1af7ec8780a5ad7758a2afb4dd508ef44277721404f65b5fe44e2d15538ec97f8aef11f78baaed249d76b0439b232855f779d246fdc31ab1cc9b323dec8f33c312a27cfb21a3d14d4059876bd444b64c2e1ab244559b1c6db06e72f1b916a4a422b99f96803193ddd3a2aef06e77333aa190ca0f4557e147f0594b87aef957db4c68c247f0827966763128419e238e23071d67444b44a758893ff0e5d7de45e9242fe6ebb57908236a4a91bc0a5dac30e0e4c7e870d96dbbe4a1259da6a94808b5bf6c5cdde6d1d9c2ba06a02bf4dda12e9aaceffa00adc8d549d28efe23c0dfa4feee8d4d2f0f6a4950991ab61c78dd9de1accb85746222d8aec70cb6a20ef5ee3255b4c87aa32260dbcd3f8027ab7190e3562f1b2840fb9b48c8e3cac258fdb666e0586bb9785182bf22ebb2a0b3f76eb6dac0ba4f384f65fc8711bd73190a36c8aa381b5d2f79d395328f2a816888968eddfd65d75fcc99647adff79bf381ea01c23f6e0f7382cd29eac8c465e718069250abafca7b22478b2fe7c444433760498a1691f1dec9402174d35f4e3f4f7c2af698b7feda9a4e9d6d90e1b27dd3a3cd1ea3d01bab2127d91d4ae6af8b3f0ae189d192c07ea3a8eba923c389e9373fb5d9f4ed9375fb9c5ba88de0832263ab7ebf872055f27779b1e4bcc23b766e270fd1ff583de32cf23154473ccf5177b22ac97c46d23998898cc962841d891dce8b7407cd29b78937b5c374a23a83361702feb8f0fdcf05c0e4fbf73d26e9109e3c7f981f495a3308f9b7c4f53303b795ca811023c46026c79381996985d4a159f59115795bbd58174372d76327a928cc38c29689a12001868bae4a6a4048f557c2ca869893f10375ce54a7ddd62c147abad926cdb7aedd1df6ea41edf1b6f6ec829353653a7c3a9c1f00bfeb02b21090f711236e5c13cdd8a52c4dc9868951cbd634cdd41456d9c8b7d10bd2b86e52e9c7bdf5bbb332ecd33231c63f40ea5b291c1a5d77816e1969a0ffef9a400d6d9c30fb8f99232b6880916a82697941a34278034ac76a229c852faf6611f46b07e9ad3b447fccb3ade54174314ce20f6c331c8b9fb626fe32c45b567013ee382ceba746c4f30856a61c5644d7d0d54245b950a60b737fdde6240f9df7360d746d1ea89ae80ec6cf420b2b5f117c1cf710d4ca50ac736f13b86f033fd9a0821a541fe2eb3e5e9506836060e33e63b5f9d1e4988ac5a66f5955d562919a6f3c6d9003fc48f52532ac679f684d3b9064036729cc7d1d5c97661ad793dd4541571961a992f60d75aedf111372e4be610a133c1ef1206fbf01848705cddca2b94099efd95bdda539ea915b4f1342581070c6c24d5d53a422a8b280df4c3e3cdc784bece9574449b7c4fa50e8888a371438fe33d8fcc2dfac9e2f4dce238e690de8833c53740f1422c49d091527eb11e8af66dd01de854482c2dbb7b5617e2ba4cbd2571be7367e1c3d17ea3b8efb4c0950b2f2fbdb647fbbe4cf25e9d49fdf12d51690b8e0299ee2658c78588145c3a13b42ddc3dbfedf1a9454ca88cc59ae923c6d8e99265111d84a6439426ada5d6104f7f5de41b63f38e87cd7f4656267ae175439463678991d1f4c40cf0babeb62ddf9f9405fd55d33248ea9ec285c1d1e4ae5c446956ae277270000e0f0360230a74e75cf94212c5b1921ff233b5af33b6e9a7c678b1fa6bd089823efada18b719c1b1446600fc4d579785d3017025ebc8bdb36668d6db4ae5b7ba115bdaf20845ef83a4ff7f0be3caa544dd75cfd36ac6049f6e8328b11a8010f7541f41c0b1f4ba7ada7edbdb0ea10ef96d6c2f18cca0e3bec4171f56acd335306616b7e8e16c0736cdeef70471af41293895a3f8606dc9c6f81ac8649c7bb80a87d0359d1567616b1cebe92f1388f28021392155ba88758d770d8c840c2fa48dd8fca051714b81efcb6380039634e5b09e980988e48d362cd37028bc36358ef407e5bc633b92dd4cee5d61b8fa030df16c9f0e14fc8da156a5d6fe9f0130b558913258e3753cb1be575253e4f7b90641ac6dd139a3718d52cb63e841b83e74cdf787ec9325d51526f3a78f479205fd96dedce791915d711a0339002266bba99a974b4aeb6d455619408fb7e0af6934852a5eb2407cd3d726b60bdaf01b0a23ac70fa8fed285ab00a44b3b724e4dad68be5b224cb1de655d302df04385688f40c3979f8ed33e9b0d744445b5caaeb10473ff68796d78dd0d9ed7bc4727c6008309cefda7ea011a190345921d2c367b32e9fddaae5d3aa7f9f003a9a1bd1fcea84983e888c4566fe6157c7c0871a336d2f6ebac90f2db7003e32e8dc47bfe4417d36ffcd2e20a30b23115c8f98d12dfffcec805bd4ab402763de683e38732702a922195338b200996532d8a0196bbdde5a5bb3b99b6a886dc4bbf353785f507897516aa3d0df1cd4958e74093e244f41221129ab68b8660970d497ccb8722c3f268b2ef39be6b3a1bf2d5185f0923951119c86e0636db712e86bbf1c7047b761fcebda0701bceedd1982d06427593baee7f536110a876ba472cecdbb5c4b1886512aec1f0e4b19a0cd890895aacd938357ac346237173b2a17d145bae827c0e269b321886358e80a2108491aa22efc447a3e186c9d54808521c38f85024816ad1f150e03b5d4bd500ebe13108ea51e04866b2375ef9da453873924b7bd2699666c07bf23fd1eeeba98d17ccda13990414052edacc4ead1ae347c9600bf0ce4fe0179aacfeb681be2049f61739c27983092a2e14d9bec49194af40cabbaaf7cd93834f4eb97c0f2b2b43f3d4c36411dbf0504d1ce7ce02e530c5023dac03637253ae7616b223e6c06f39aeb8c1f5fa2f43e8156c31f4b3ed0b75ed0fc46a7bead497e94c0cc1b8ef56bb91ca425538321b8193cf1571c1f0782d6d1f2000cafadef58112a7ce38cd388caa0bcca2eaaa5573df0cb38c8b0061fdb39a25f9977d84c8a51e815d7f83ba2a54325546defcd13a4a123ea54103e087011a1dee3e6164cc8a4bbd5f213020043261ea01b0c4aab2f5dca21d61ab8649208d04d6eeeaa69c01804d73ba802d667df6a79dd687dd8e2bae5e5126be4eaec1c2b4ae61d5cee00bef173fbabaa2bd054cbfd723c484cf916bd98828c2687d35659aa86a46c6ecaff0686ac58b75844ea737bec955648be4ef5ebad34a872bc3acac2160e898ac33745a03103e9891e2edfdf4cc014a0a387ed49415c8bb95bf3c2c0d56cdfb7203d0d5e9d30e96bfc18d604fc3ed93a9da442b3d1a7017a68caa815a2d5a1680f615075fda154938403fb5504be0599dff633df875646e464b4200c19d9382605d9ec6dcf8667cf464e28275270fe374fe3b128d95c4d6d2d3094fec8348d1d88c9d0f98df32e3ca6b99063ee54d4a461fed1d776128fde919088b089cb4e9693583cd67dcf8b4c66fec5c1a78a1184846f1bec66f74ed1c5e0034d34c4b1dc96d64f7aea7a58855f556196efa9ea4ebc8e50edb7fae8361b883167b89afd003270bb2e3ae65b7c79c9f9f9065393a6183f5738d1b58d4ab2cb87ff1ef5ae9d54bd0a844903c77dfb3d5675a357768709fbc61905476735fb0e474383651617fd2a39ab11a0fb4b85766e6182e2a283e0942e414ddad3d1868534c098a09d73e19fa1f304204465f4b2fa2bf20e80dc3f8a49a43a627f0bb0d4788ecb0d414b715d39663be60817ad2d1f7746bd028887a6cfa4a12f4ed5f4160cff628bc58a02d7f7a8a880b0a4af18e39f66c69731a59d75fa4bc401a968342bc84d5a16f5f1f32a77fa5a981815e03b5a3123dfc56d728e1646fa102a19b49a1da4f1f4a067ba3ddd0b3fc8c6a920de0929b8b7c8d14ba99e0e4809d7f1ad004f3d4f3d7f705cdb588b138172ed040e24fb0d7d954384d8cce63ca7904894067b36d27cd1daa5c7d9f811d6bb6216eeb23a7f2f9d050252946571fe9cc62f081a86de205022b9c1d159e13c9c6e9f2260bb57965ce3dce2e2cfb494241cd7fae53b8031c6827a5c2f45f20ebe26d797d4612368f87cc50a313d9e8b627afcdc1fa0844924d9099af85be0150ec1e9df803d1990894eccd6ddfd1f95113261f957af05ac1dbd3f448592097c7f9ea8bd0df8709b928c2d045f18e390d373c82bbc99e2502da2a72b71b66f9ed764b0cdcbc1a4dabbdc2dedbbbc9e9827feafab7e2666dbef3bd5ce4847343d7039768fdc3e2cbfd1f23eca3bd20d4834949de043e4dab64c181af58ee3ed659f6b140686698dc9be6c9387c507a933462f6aa41935abcab51e392959cedf68f2cb47f81320a18772844c71202b8fcea0fd9c24c0b50aec27bd81c43908310b654873d93779b42b34a0472b9bbe15a04ca3547b0b5157314962c8ed5e211065bea53dab97b07d972b9c9269f60fbc33f5d6ff7638001709768520feacdc540096431f2c0a6400e5a372f347b4388e905276c6628e81084abb88d362dca6f8e6f6bcd9a09730b3a3c84374e950eee7d99edb5bdddd3aa27719242c9a821f5e0da373de766453c8227d4db3433a3eedad07e46867d4e6eb3547fd79046a3050d86896802ce8cffae5b2259ba0db6208dac1b3f2abdf7a5e00e8f743d02cf4a6814ee8a9c7265d74fcbc05ab4911e903b3bd479bfd3496c7ef9a9fd786b702b1925dcdac8cf8dc40ef100912be631e83c2e2acdb8608519ac87785d0bef0aa44a300c42cb08cbe8c82562f4d2917fbe78b46f9ca666cf3ca01acea03d8cabfe60f2f8d6b9af71db5c871ce33e07d15cb9b01b43208d9068047882cfd7af11d72193c58628078197a343688d1862a9a4f2b01bd1abb5d1602a6a4a1d020f7898b99c8e1548a4ea1e326b6607b528b2eda4417c72655b12747d87be3be89e01f4f20c61b62a5df45fdae0b09dd8c887bed04c2a406dc8483aef815499e515d71edb176c01337e1024991d7c4d9aa5d47c5519974d58ad89a2393de45dd6693e62d6d3570c4e842ac56fde57969278e02f09917c17c93eb32466c83ddcdae65a3595f4104823496b5c5ee79743e1f74b8074c880e74ad7ac8ed68ea71e73ccb085f961df854f04072b29064be6e19fece70118cdfdd635648d9ce7a147f59308ffe419460392e7788ec4fdc1c13ae311ff981072b55923f55f35d4ef8309c40ff3b791627257d8a27b9a0f8a5fe02b503f6b0939c13819e8086e8120247e7648f7f03493061155b43876cd1fc63b2e29ecb09be271d5252d9dbf00bca365e8e3489d5d5ab576b673dda8a4f015623d4261ef9aadee2ad07ea65f5b4e5312a52954eecd8fa39f89410037808a3638a07c5e8203471c517263f4105716c64218c68c6e17cb65a9628ac555d4389176fb742e46dd547db053331d5e10fd2711f3b6fc774f895ce39ff461edcbc596999e87c904d47a8c1b6615a880acfc0bf9e541edc5906942c4a242d37f80af6660a6bc44f133919ec75c5805a2562516a302d420a6dba9182ae8b0b4708fcece1b0960539142e1524f7acca992b785c5ce058f11208d45885ccd7f3badb95b2065c6f05e9b077c2934d0d73ee99f48fc83122d87441824cc7510d13d4e10c37973953961f3ed7db9f4cb5764790935ead613af4b3fa45f2e2d5d7988929b12f1f05cc4e0b9ea4531d39fbde7c180cdf1ca9bc380f59b07227c5836e27ff952088a99287ea786e3297404a0ed5a023074a84f95f02b9dce17231ffeb7a9642372fc67945a618a9fc3fe0fd841ffc68bb0e374660b15a01f8326dd48fb545303aa2e062f5658d8d4d2dc072a8e61b35e02f7a7862cc74292c1b78490aca4c994c9953ebb66a8cf9064398593805684a17b9543c1b3d63f05a5004eb271e000305788b82dff70cc4fa858293a36b17e0f7d6400f1a8a1866de632a48b34aebd93f90ae3d4d37b122e65f54afef859e20361b4c00e8f545a19b42bf3cdc003edd4141e1af9c153d568acd84a95bfa06d2bf5acf3731a69f02b39a20e130f6d036ce5ee21b9e266f6f6aef8b7ab5206c033a51ca418d26e5d3ddf69a423959fa9650cf4e5742a62e29eb7b6763caf5403a28a433b4206aabb5a8dd6bf13835445c920e58fbf64a5f011f1566b8adc25ba8a07038a4fbcfd64e07f2a707c9bc92c9d28c4de61838d4ff47cb935286214368ab4e00af71f7fc20d7ab029e4de93d5aa610b4d127d794c3bba08e2300f99dd4e5e3edfe59bc809a50dcfc8eca51ae2eeec8e10b929a1c4951b5481d07734da26399abb29dc8564d6ace82c285b4bf7256c9254b660471de14eef493567d4910005151a9ceb91c0a064c9d051b51b1d6ba85cd4329ba0d49d4c06dddcf4960a3a7995372489f1d2decdbff1e1dae1ea82a055424ba866d9b51a517110e115a7777077b9a172049a24378cc2f03c1e2cdaa47c7aeefe6c60beefc81241b62ea64cc79fe90f70927ae6620b61c0e7bdaaee09d1077a591dc325e261478cf6db0e9c22a49a561f90ad935c0b631942a325c0b7525b049bdd784bc5eb0d510fa1b545586389fa70c0426a9fa4b37c233754606a69d04b90b79fb66d05f580f6519e478c8cafe532f72c4884c1f228faec58584e323a2697ee3a995875e59261e74b9ec9ea137b5311233722647eca7465209714b0e5955b55de2512705dc3b2617ffc4eb64f72a204d7cbd25c2b47511a40e89f97998204993c4c9aaafa427560598b7ab4cfa23432b534f847a511d6d278fa602eff8dd12e30e26cf9c4ab4d7241709d53cf0427e061028e518771718bb00cab56e9b35c7acdc9eba0cdd53fdcd41b3695def6a2a8f5e0289adc62a93bd0637b28650e6dfc65919c856919d7d6602c16f7c5620e0d8ff221bdad3658de25e6d474b7590f413b875f28f453714fba040470956862487b7b2ee61bbcc05691027b041ef3329578428c180c30cd09c6d4291b046bb259378970b85f2d0c84f5f2c1f69cafd3b9c0d41bda9202aa3c30904effbb08cb01115d5adf0509f894d6e7ebabefbe90f95fd712dbb5f94588e1ed68b468ee8446c398de79c7e59efd50d67e998118a438ecb022f90deb5f8e604a6f0083832506ddb50acbede8f0b1de081437eb5cd309e2193d33f867dd113c19efed3816311e62ab43451e8a26a992fe1c74c0699dbdea13795d3b6973f5a82002f1ee161dd50cd44b7b2d761d6a8e655e6c6b97bb26d5e8cadef23b4a194df4917ec978c27122e08ec7e8134149c9178284072003d226bd7fa8e769b3847a3faaf1e6b25e81b36a6e2725e2ecb087062371ccce70d207ef06f9cca419ded5abb736e2542e914e7a6e5afe69a6c19761ba70e5f082b8f0fb7e002c60787bd19089ce134ae8aca03fd797801146c4f33a2ac167b8c701a54e4b04ed929399522b537ef7ffa3a74f6376fdddb0e76e63416fd5b5a0ff935da4b9055b5ddb9f4f7d646519ca2a3de9edbfb1beffd39f97985f955dd2a09beb25ab540d76e38629c741e08a6eee4878eab57e9245e44234a89f3c790f855a2559f388fe80cc1d174a880e4547fda5108d00f602624c04aa2e4da805b6dc027cefb4fd69f6d1fb653f725a8bb52689de52e6b806182e6d2c90c71d84bd86be6f85f92dc2301ef6a478634049875153512b4d7b3c9b9ed4341d377dad54a79a191c3a6117a3eba9814307efb6e308e1178e54e86875a930a9c368a5ef5f2e9a57e2f2779dc987c3a0265599de429ff02eaac05c9ac8d2ab30a181fafaec3059f112a4ef695892dc503a01d64836f8616208da1c67d48f259a6003b97201cffd6bed736be1550e6f528be186db754542ef6d2581dee67df113c8e928ae62efa40d0dea69e5df40f63452dac360b84994d19ec7eb0995278a56b08ec550e1d567281c58339277d259d967535d999560f29c0133cef5b2a6a21235f172a9c69a9934a0cafc3674208d58ec07657d87d3192e009399c060528a3de3860051174dc7db9b09879f2862c090fe55466967ea871bb1a889a08c2d981b9aaca9bbc1f1b70f48a7d42ca5b66d00e18577aebdda4f222ad70dd23b095e462513e29daa0e633ad8615f269ed056d3f803ae4180e5dab509483f8164a97bf0c31a94b22f2633a1073bd3636d265cd0b5faeb733b64aa8129933618f89c625d79a44457af1cc60ba2eb08de4bcd5e1e57f88f2db8eb07b3d7fb9d867f88de9462c292d29609b5b9c8f76647faabb892ddf87c0a5639755d19e1d5a0038c60bfff3b823d344aa23c9e0e2ef878d05a8e78c7be868630448b784af9178e3fb7b6dc65b7bd3f577145d53089829b38ce2ca935db321fcdf1cd7d042c624c4acbefbf25537b39ae76c93d1711dc4f0e3c3c39e8c529dbe9498c5ac8379bc4d7a3bc12fc256df353f1b715b13d7c57f597cfdeec0fd88ac370fab2e453df0469dcf27f35d852e583a9fdb140cf4953bdff73b5a35fa43878f7178402fdc7ef18912d011cc49e6bdb955ff7ca0b69c152f0d18e28511dbed145496b1cafac50ca955fef2d0b1dc2700d14c967de34da2b19796bb418aff16c8cbb4c145906b11e32b6ea2ba15414d954f67c2507ff262f837a40edbbadf512a0e921dbb85c760f9a25ff4ccf6956276409782609ae31616699ec348412a436f2af274e72392bf3f20fd6f0a45f355b7ccc3b9e516ef5c5cb7cc0c6431d0ee512ac6f1e030455f8316337297c8b9bbe45775ae6ab85ab7f3ce60fdde06e77b15982e6d7a596f0a584107f6bdb7eb5f2e9b173bbbd960baa18a402213dfcf1159077223ecb16c67de792d6d85d33b19df6646cd13d154a7d9e1261b8d65127a8a9a7703264db182ec0a937392ee34d3321b29c418c4d177a5ce626f993dd321091edfe60408dd10d96ee8da6e881658a55bd18fe3434d9c1adfbc2132ba0485662a9a10f42250077ca725341bd6666e0b44e1b3d1b253c6e34c3b862543603ac2c5586b1e212cbf63d33154434c3c77d2ff12dc8c278f38d6e4a9dc64893c9af7807668b4369b2da71120c1d3241669844143a47e1cedc5cfd00475efe5db010392aa4002bfaef275a9522004f4095f19a3dac7b3259f6bc6a21cb062a36cccc64d0831f70d1b1d6949c8195a84b3a89c863fc0c3585af719c6eda7f14458fecae7a53e0ed17fa6be58962c09555f4308a8dcc0167334b40e635899532c691fcecc547c3096057742a494d03cb7db0774dafc83c7526512efc0d359a48c42314cfb2f2a621cda09edd86f601f3b2653a4d83f1912e331b8740a007e157078186c9d8efea8a51be963284804cff0e346d7dfa92006c3fd5ae04e874c86b37a4c838f19b412aa8344879aa898a27e48f4a63d8fc92d7fb9ba35cee4f39b296e1a636a636c7603025a16ff7a1522ef0d1aee4989da44a13ce9e01084c0a9b6fe0b04f2060652985735b2e581e0db3122436ee7d0b38faaed02e00815579be06ddc13849efcaf7982beaf4b87f77c6244d8a12787168b7e07cbe28a20bb65caa0beeb2b49356cbc66105b64e54b7f7626803f29b6a208e0cfd513253e22ae013165b724bb5defc9b1b7d8e1ef203dffdf6f9407a5d278cea08a98e5d59e293c8cab3e8a1a13480ded7a5c33ac7a5d656f6408e40f322c39be58d067cc7f73fd7f8b3234e07c2f819b9147076593014fc41cfe30131c3132d6eb0d685eb017fe34a3aa32be2c0c1ac8851cb8f5f4855a762788c49929db20f3312ac36f46176f16adc417ac27a218b3722c7e6150339d677ac7acc7238f33085d8c3c3f64dd24bd6ac9df6f63410779bfc0c988c4b642add72e65738558fd00ed86c4227666f5ed09ce412ce6a519b695260b6bc9868fcaa23a9a2cc79c0a1c8c2ab19934b61efb3faf654f3d57ad18879b5377380fbc4363f94dca7cb7a13e736ae1f10a7b7c4626e447dbe59a6b28165fa4113e105e337bbc1fcc870115e7459a058c1846e918a6debda75c5858cdf4296e1c7e3a9d4a0168f58c135df31e83b5f548ee1ca1ea95f1b82e3a0ac023863304c3bf45ba0181d4f440ddb05d56f2d24f3da6ee9504118f3b612162f194ad26d9fd47bf32bff59235b32a06a0fd49da1864ec14880ed168c6daa0c571eeecb74eebb24bca24d68e4336e5616d2eaf785696c248a9c82a73d4ee214ced93d860da0bc8a4e0783badb94694e15214a1766a14368d06b2d75a4d435d0bb2f9313582ec1a52931ab2a8f9d4a28f15efc34dc0f4d11145a6480358daa642014e2a0afbc753df3c5f37a9e42e716604679432218dbfefbe10685a08360ac202fb9569f86411e3d76094ea08ebb323d4a330f8b239371dd95b0c155a0b74ab299a629c186919c577851907385bd9b0db4a2844698cf27fec3d437a74c3a84d0c2e7dc7f429dc5fa4f2f7c1abb476e8338204f14308cc95fc8766e97684b74244f81dfa281c1dccaccbef1046436129f946f51d7e49798dea0f62c839f5d71237d65b993d2de173793c9f52ce5371cee484b8f68af02bdb408b844564109f3286261215bc50fa34a8cb87e9648a9ebc109e4c09fba59b85a7eb6b2be567955939931344c1e511e4fe6f5592b03ad8945be4675750944c07ef85c7c39fd7cbd9590b71edddde14f4a7442eff67f3e61c439c07786a690e3389eb07a849461dd5f3aedf7e548b25c5f49ab8d8dbb2ba57f6f11dff7de075c6c22ebe26524408685c65a8733793afe4d4219e3af1506f6c105a3ea756e5380152c8bd881b1f47a46534868db4763bf594b91477a93b836f6e9edd9cb618e863c13e1f93b2833b5fdb1de4a64125ebac5ec7a83db113b942a9ba107df6246909dd637bbd9c8f04f8cfa737ea20dfecf2d82475cbad6ee94177febd55b6f5529b1c5966ab464edc0f85b48e7ab0d07f9d63d52457b45c03735a548445ac2b11cbd6cf36733da08c28d1062eebce04d968f2c8150f351717fdcb81e0dafe4e5c2760b3fa1b2d11f672d27ddd46353f7974fe05e1d99772a91b819378744b96cfdfe24264f9715b17779c41f4c4d2706dbeeb64bc49e7c481eaa50e57ea76500e8ac0c628cfff63265ec69dd2a39bac3f4fa22ed1e429fbd3aab27cd149b382999ddc78559cb95f1a38f6e20b1976adf26eb9d0a6640d678328f1b1d6d46920f90a2199d62aae68173f9b808d2dc5c35f9469a00918eeba95bce7bf357fadf4527cf39b48b6260c43ae42eeed3603519e5d226695359f98550615e5b6cfdbbd64bba926bcbd20321fcb01916d30c6402e93ab310af38a7db867511181a3a4b2ba0e4e47ed67cb9d2089c78bbe3f5b2fabca4db4ad2e251858dec064628eb7e7fbfb26674f8675a14596c176e65102dfe745b8e5ca3a6c8d4867fa405d14800871c3b30e214fa687f41053bcb00aad67e6a028f19723d2143cf8e4a651b23057f7e646c9279be4d9854ce915de57187f5d8529ae3fb6ed37d92caec4d8c8a8734825ad3650cfe81692169f3f58bda2eeb1a89f900b07b49cf0a3e77bf663fa208378a6c659cc56d817d7f5ea8e781e78cee7fa6d57df47d12ed680e709973e21b65071a5beae66468d88eacd6e72a8d2ecd8aba6e13fd8d3b44e4c7b5cd9b377587ba2ccbb6460e9b43d58b388ff67967acbdcb39dd99db199d23355a238017f424f495fd557248d2fcc6fa0e7676f6b76ec04eee12114b96da4b0da0e1f4e485b57b3a980d549fb35a84ecdce7c570b7403c083fb51f2371c87f3fbb85103bd86fa265a3567403b0c84d1f91a0e74afebad4744c6596f7efac0977deafb0395ee1b97f4e6b4910baa9b585bb408e2c0699e3230583696592f661dd9d43fba81f20c9779b0b5612e3f9d3b7f5cc8daba44401af8fa906bf26322fd2ab2fdb027b15208061f90d88a480c8a7d0f4adbb5436c0f2e0b753caa4f7c04f377891800e48b782cda62720439a5ea35eef28542f95b950d19266638749178fab54f7a30f5435f578938c5b10cf8023d2a002fcf61133431a1f70d6450c170759f880d26dbe126e4d6ef73bcaccbb642b8a124d2ee33a851f140ed745eec20183966d67de667f44558343c71e88c4d1e75bc1686b2b6bb8333db0c3daf6a7d6f4fe906548a09413b207028c727defe88babebbd6131b12db4d9c0e6eadb7c4ebff96136e2cba6a5efa871628f5261273fb8c308c3b0058e94d8e877b77df0d77d90a7c6aed93e8b4d613ee4dd57aafcd8c03dd408dd4c945364ba1b20721c4d79a51678da6e7ae7ba01e2aa4158e0ef995770a1bfb87926c2a6edb74f191fd99c59a6e32c583ab5d30a324bd1b04ea4340bfed72c3590ca3fd3e4e83fb84c54fe9cc84925f050b0c3e63a42b17dca837ecac03f0b796209e63f8565af309b37686030e2d68b60a1c8d91ef67a73d636063aa4a9afc4ecc922026394a3ae752103dc2b47c3eea5003ef8c4538435e961efa77e220d92f2afe26348adc6a27004de6e37a835e5e9871a2ea2bd93ef7599ce661150d01c4534e849a92e69afb11fdb09dac6d1bf7194404ba3942024ad2b128e5d8596f3478b0c510fd49a5e3fa8412b89d190c3f537beb2f49af3a4fc271816bbbf99f3139666296a0e3270eabaf7271a66b58d2660ebb7fa195da14d09a543d6b59d8f7eaeec351dc90236130377f0134c7cbc9ef6a251f2e27d5edd4402361748c64c05862a102e1e0fb95cc219213b77583416c6ba417921ab1b2a4d02aab8b236153972342b771a885b22b4bc7cd7f993334d21f688bb639133887a2ca9cc393cb2503092c7d7355b668b9bf6bb4da063f1dd295bdddfa1c82c43e56535077eda516536584ad958c647557675258fec8ec1d337c7f0e9bdc8f53c84f12d67e4f0e309607b36001aaddf2daac833f7ddaae3e0c91179a2741abcf1c6e00853cdeed3af13a41ee1aa91a864c1ac9733c0e5367ded071b1fca44d178200726b563456076618f4c4b199ab2bf0325c823b33f88631fbc8f49e2a47f663a1a4285defaf28c44cd156511367c614416581410423f05e958f48895c541050bfe6b365f5e1a3db5b91aade34eb1ae2427343f1e3a5c6893932a730890f3aaa0ac7509cef6e95342a43c9bea7601234043af6fe2b155427e9f39285f5257f2bb6c2f55473556317cbaec029f0c8a95cad299b24def02f19d378c0e2d19e63a278ed792666e9ae9bf99e6b8f81f1d12e0bbfb212859090ba287b4f8aa286860be21de6c72e1fa376f00bb060958deee13f5051f1644f81724f69153b7078f1cd8f1cdfbe2f4fa59be6884176289312e6ff957117afb8c60784dac18473c1fb8bdd11ec50403e6fb117858a028440b4f1a5862fe62237aa1c8fc67c808a2e749f1e7aa7c9f67aa7c3213c831bcde9691f98959de89f53fa3b895d2612a1ccad04aaf2927475b97af5dfad0cad511449449023e31bb960257c168d0987021dda4bc666596e696fef0033dc0b93d0dfc6beeda912ac0084aa719bb628cbc9353993479954843b840af237e8c73f80c38969b78a4e368964e51aff72c1aa8503ac48b33213ba2f3859a7016214824aa51787dd1e0b971fefec75e5cfed968befe80532b36302195d2ea32137044adf7ebf2fe8df4f7dcbd704382277944fb9a6a030b192355d6000dd963290ad4813f076c4afe0520d068edf71b7b1b0e4de821d6ea9060f5b18b714c0fd7fc62318ed2fc81f6eb110c11e68c204fb1b987a041461f67b5a06c8198ccceffe60de9b10850300aa9db9c0d32a7af11f6cc75d4ef7918a589ab9cb80266e45b236d3ad729caa11129aba1b8b0276d55cfc7976739b020da492a9a9a248c9854e12d10320d75bf4d46fa4c2a94b9e28d146b91548cf752e4370f6380bcd1bac4e4ecc2c1a668de6c3710a77f5698e054cff4a1c054b53823b1b3977d7eaf9cd3d23693e3784a28a62d6d5c10db637089579934253cd92db5077ed473ee4a02962fd974d40526d9a37c6ee3dbc6d271d2595eccbdd2ad53103bbb5cdc702bc0c54b60e67dae20d95fc2c913fec63dc8c1988c6d6807b34f240e3aca3462ef90f1629ea923c24650752f1a5b392b095b1c623c1703dcb422ae3c80cf1bfc374802d20d772c219b31729eaf2faad749b25256976babea1676b263f4a766130b8da3e2fd384f3caca250b5f56bafea2ce9a33e1c8083a031245122701af738e01ab31efb51d8e500076777bb784c0905974e3d6aa4cab7488a5cb608af3fee78c108eeb0fdfd6d73218ca0073b32992cb204f3c5abd92d6d52de41b6d33077a47836ed83054320dc83d6e064ddd10ca915ee3ab5d3b6e0736c5c067cd2908acec21962846eadd1f72392abf7266166f9ef389dc1f40d4494bc2128c0427fc697f373e43a8552746042547856b4ee4fa7613eccffaeee5651bd0905843bba2caf86ed32c8faaf994e52ab1fa9176fdfdc515212366ece617f42b8b8ca91eddcc7516559bfdb2c39dbecdd301adf01ae5ca0cb51e5e42df14146d19233cf4daceec9114cf448814b36b41a277ad907f9f04acf778b1bd9a91bcf4d0ce8b7659c636cc8fa9fb1dfdca0cc15f686b695cab4281f2bc3b4cc7e947c64cf39e00e2e52608fe8207b6a21434bcd72eca032435e381133556f0ae72409ab3d3ad10ac0ab2ff63be368a45538e93940aaff100bb28e4a89ed41dbb774ce26f9e8369c0e26c937b04552a633e671c9a83e368e0925b674c5f683a78986bc9a06fec4d74d0f62f3f0efb1ae161ac93a19483a39167c55b24750c87d63b0d432986bc56446ca245a5003dcbaf3cba78af71ba305cb86af12386495efd97d2a4eaf394afca4f5943ffd6166d1e3d4ead93741bfba6bafc35970c7ecee6c06079bd5249a984d3922cc31414c2cf6c8c62b25feb81b0034c6e36370480a49041bf0b0ddff6cef4e2611006aff3e9972ac53679a1fb855a85637a10e70ed14ff2613fa08e31a93df082d7154a14eb2a6200d0a5fc159aecb202f3a2cda349c1a14429876cc5c507979c3917ae786f3488088c467f31aad33404621e46419a66632be312f464d3e861bf025de084f7c87cfd2bfc24c4c07595289d1e6d1ca7f9952848868ea11dbe6ab13449bc207eadb7a667eff7e44a4644e1c3f72d9da3a62009fb845d29bb312ebbdd66a37cc7318664b7491e8d2b02c9ac7d3f6dd52707d4f899e669d115de86f35f935549f4465d622d4cb8fcfa7456dc79ba6075cce4d3be19c937cf4cdd503ca36e71a72ced87166de1612fac60b6f44deb852c8058c1fff7dfbf06873ecf5c87bb0539033a9fd03a943cc8fe71dbd695828faa25f6751678352570f08b26ce891c1a1681a389d431ad28cb6090451b40e633c65ed7a97cfa2063f38bd84f8487c6cbb36ccb5a404af46150a46e45417a328a3962bb720e340a3885968182b017fdbb8ceb10b75de2791b19814c7ee57497f48a16a180a2ab33bdd784cecde73434dd20a5eb57a32ae2e3f413d8466fce540c2eae3a4ea1be7b7505e97f339d496bb4f6753268b11af2be2c60516f3d2b1e2d46da6546c7772ac28f2868cc198b4ea80e714c1047b2e025d6a189b30184822b6985aeb49d1e7023ec1c1627913c26b5f414c3d771ba0d8cb4ad7404df9c618ea83d81f951ffb431b3a0cb03a8848397de442731a310ec0b441098c898b65cace625fe67ad2ea27011239f3d018d83489136817e7733f8559f25a728483db087e3e25b0c15472fb0760297063bbe1e117b0899f7b3041574ba77ae7230205b2d0d9a56e1ac922a28b1b9b415bcabcb96d2279b8da3f6f0c292a41bfecace3ae82282c656d41bc6b593e0c4e07679c18895b3a02db54d343df8b0f5617c74a25b594d10c429c3d43f848585f03bb5dde125070c2c2bcfedc08b87ee36de1ac9031f930be87dfdb32a234ac8e87c991127eb68ca8fbfe0bb2cf963b2a65b39d5cfc2e1bebef90e68815ca6b898c5abe0a2bd207cc0460a5f551b2813bfa3e55daa4d8755ca21d04a88628aa6e34aba1b9d7a3f3533405226d2b78a664aac043a074af4591ddbbfb0d721a62154df854bbf6dd6e64a0a9a2d46636ad50c28425adfc6c70d7aa25d319e2165ba774c534fbdc0840aef5c26ad2978d09501d04e10ecdb7ed8832bb86e8062bad0d53762fbdbc3e848505fcc57c52878b121be9191171fe8d73e24dfdcd338df6e9093f117383c42d31bb3630c507565698acca7a4f31b2314f34109c0098e68381e98c489c1765449b4d80b7e0d0410c27eb9aae967826da68e49373c50927c280ad76195870b8d048a76222603505d8f25af9b419043304874f59159b6192799fb4e184c15073f2c913d9629447ca8eebfee028ef65bf9b340a00f30178fec7b1a07522cfe99a95453dd59d021fe77e78d92fc678c00c8d346448558e4098d3e3b78838b7ac16737a14dd8b7ebe71b1a07d5cd7cdee5beca13107fdb13799b101f39ed2456f96c249ed66931d85174e3767a9e41e3596094fb48f50270a1d47c42bfc077b4a9821a4d6066f7b200492b9ea57bd62a9472a93c0c9dea7f4629319f12e1062291dbc0ac35eeb92a69d18ed293a62d782ee4cf5e7be008e0b4abe6c9d83e2148eb642d46dcd8b337f8d38532adf7b0ea24aab99f168de01aecbc6d24199d1a4454ec0d1ef4fd43c2093de50d810c26bf2c261d066268a267124b8334ab2097c612cc5ff9eb2955c0ef943a17373ab27b3f0f07af64bfd131662fc40d138375e97d611eea395356aadc0efbce0fc80234c3b948ea646c809c9f0e946173d8b54f35e0440c22039aef26680e8cffc484ac3391f9de64b373eca657bf291b8de023d88fbf7170739b1ab35a0dcd4f8617ac7ae013f53427eb39c54afa3687989293cf1746a830be69f42d08cfcf17de5b93a9d79985e85d360da282ef291f23695f65f5f9fad7db4ff8cb5b06aca3809ea96eb9609115aca01440fce24bba1d21a11ce7c58a966ed621be9b37750ec3b0d212aa6395798535e56ed46c2fd325e39155a2ee773e08f341697534a63392a6b179bcbe656b6053a7af61cb85f62d206a215833575fc31ed1f60b6ed440262f371bc22b0ca23da5b12e19aa14a2a2e12351f364e4652933f2c7f26fdc8ab2652d5101f43fd37f1d43cf0266d019e6655b76fe8c01777003e0a9b611db00cefa668d0d4dfc69fa9fdde2a5c6ecb8ed91a64229c1574394ede0f32d3d0bb661309617a8e3c67f906932d123dd9fd8c7ca981d2d3d030fd587283ad386620f2601139e9c13cc8bb213b14dd0bbe63f32e47a9bcbf059eaad4cafdb1aeee28eabd1db1bb5ab1875fe046245d2afa3af536b3b8e0ff40fc00b4463a5a75eb9fb4f3df3fce44c5f5e72aae2363c17c7f7922d0948b2cf9ded15d3d2988156e90392944da1d13d06dc842ea54cf5cb9e0b77758909c329efa83e93e2d4af025e918740d4aef2f75ed6121f0e21b9fcbeb17fc407854c188e4767c73b701236feee3b25699e2c2cded44f256c326bc174f45d27056c34fca261f31731723cfdac79a619d8d23b64eb2f6aaa1d1c7d7987386e13e0c313cdaa42db2e1d4261b23ff80d733642bbb5f3e0db7cc23caf313720568d38d917e282c91f0fe5e6d3013f87130c4aa9b8f19cefa3612296f862848e3c55934a7d99b86f7f16d302c92abb98c3624b2eabd36dca06376a0c227637cf5b5bb1e3e36cbab7fa81864f2ecdda1f1f5b6a813f6b44ad6e1a6a071bb14f140204d75018db1d1ffcbc0c484a5ee001fed0aedb5a345dfd83a79976a6f6800ce32301aafd564fc892d8b6a7e7a51faba81b5284b9a651f2fd7b007b2d9769143b4e94124abb5f9100ce312c47ccbcf4b2d5864549b9646dfff220a3fda75eebc0bac0bf6d79f749aa449f1578809dad8deb0e95efa6836c530e9f554c5499d8c868fd7509633adf4669a7e1bf2f4d629888ff9ee932158a8524f2db837a79dd4e73859694cdaa6249fe8de5dc94ce1357dca5c1e69a27212294d5302297b955a3d6c6eee8b2a7a91bbbc50f34f4ce9406a2310424a0b8768f6ce3d97ce100324d65585cbcc93a5a34967e0361a7d5a2f8918c22d7fe43801bea626783cd1e759a462d33b857164ffb49b805dca13422029574f7c0d04d8898e24df1376a0328c69acb795be5d60721638a4585d89e73d33bd70e5fb381dc438f180323bf2583b6590b8ff7ab4bbaa147b3e01e52e50211c6df903c18c8730a5b846c63d4b3ba5637d615698cdadf9596f78992a1060e2d5fdd383900e6552efc09f05e1145ef492f1b812d5519d0c12db89c5dd8c9d42c5cd4b373a91b16ece3d677063a9b3fc056afeba239ed2990fb4e023b2987c2b9a6b5690c4d1faf8788c8af801859c4a94b25f0914ec6fa33d96fc31c884ac8af5feca1109f5a0090826b01ac9756b620df498b817e2eabf06a15116ef1ac894d951ffaf8e8a7b1ac03af641431a899c063edd003e834e1233005ee13c3c81a9e31941677168d304cb45077be54ec0d7ee77281aac070905e089d18d46619178bc4ae9a381cf77bd3c75c0b75bc044737fa70865239ba07f2f0dfeb1c72dc2d5f12cd333a22401558fe332c7b6e9c73a172ed4f82e11271dc10e777e127fb6f7baab5a5e96e5d76a390a66dd2f2537500fcb0dbd2e5a569ca5518c7c34511808e79e250c16c2b64924fe890df3036663184489319ff39ba6e0d471089a9a7976c0549c7ec2f21abb07dab79bf9b2d564bf3200998f0346b4b1d35a8ea0c315e79bc086a1f0fdbeff0b82e573cf991f6dbfab63d8d067c50d3d4f42a2c0d3dcc83fdb9afe2288c403db3a792bcf557906e22ed9d27b3f383f571559984c28d02c8ca73cb0d14a709acfa22a00f0cddda802128c60cfb992178bb45476801446b7d8454f3c77387c767437eee80cb2076a1a8a12f83641972178683aaedf8f6e57616985f9f51705ed32d341274aaff4f1d12364019443defde6bcfb387c3bb68de0c3950b517ea5b5ec52b06805b82bf3416a74cd2dc715cb9b9b36666e188690b3097a365c709d2aebe1c423d89b3799b0b44eccc68c46a00d88acd0dcc28c9ac8e5a8d2235b0ecb4c12daa416e4ac5490b57a9f3375e6e15f598c2792fb967c04c3051ec4974b4d42ad50e43d772ac0b45bd359a0e781264e2b67b7242c6ef4e0ca12664956aebdff2cf10b96babe1161c43dc977719f425d4ce129a62b2936cfa8e744da45609f04c6e1fa9d0ab801f168681ad4ed83c6ad3dd6c6c5856a03c3d727fea68324794eb3719a4ef68f735c35087cf010f081b79233d601fa67c52d94be8aa0bb4dc5d582edd8d6c931cc98cbdc88095f054aa1327e50e2cc0eb1756bcbf8fa96ed23e91aedba91496ca1b70ce204aa4e89d2f8485bb436642c003320c75fb71df983f7330280b5dd2ef1b4d4db3b05e44e62864eeeb4fab0c0df2916fa17fb5134db90927f6b30f8632250bbe7fc1fd25b423d87d8dd3cb885669d20f9846eeafbc212e0afa7b3fc7d5dcff4e7ca165914338108a6d494f216840e6829a678dfd5d43c8fe43415392cb71bce34612fcabb24ba278e702976f4059936eb46a9c879e454ec06dc1036eeecb8f322fcd30b74dc2098c4e6d94a55c205815bb69eef1c01332b92e872621f4ad3cfefbc45a430ba3563594c95944963462142566a8de4a81b5e5d1cdc6138d1cac819533db1782f5e0f451d9315a2c7eb7d03df6271b6e8b79c531d6efffa6fd2f20ce7b25d104e4ab7c539b19a8faed00617d4ab5c3c5d30367d0e680d8eadce4240075078ac2b2044065b7d14e2fb6921149943b492ec02e0f001d9e700010f4c3152495c0e3a484abdefe1b94d68ca9067de6da6547d3ad99e98bc3430490730d97b4f358f07bc82a06101daf06759aa9e45cc588dc9e1176b8f97446562de2148e4113c28af1d7872a50edbcc3c88cac4f5915185d1d9aa1c6c3309154ee8e3206f6633b0b1a231246307d078182cd9c794c2778f0b2fe2800e9dab3f2fc0eb3c7c39f950a49b4573aba189e906e9bebd97969ce96ad5ca7417a24e972667c14d00b92a55cf8e6709d692a78b9b22079c80ba8c036caeb2e2521781ccc9bc2f5d03c480ff726993f02d4d71a45a2a3c6ce64c8fbfb09b0c8a9aaf6ebf932ea50277a0df66a4401bf7605d133baa6a5ac271a5796a1a0d9fa458e7d5f5a0c6b533c6e21eec87c6ee9fbd9baf4488aa1ea8de09a4e3e11d04fe000fb2b1fdcb15663936d92a3af3653b937168039ba2426f4b31d28eec84387028b9abe11e76e4080a69478f1e8d725f2232999fae3f55c4ba1e439041753df3a1e553613971a39de9f5545f475b8292e61cb83a1a28baca0647d22ed878dd457b0a06e44769819de6f0cac35c66b50590d2ebd3938cc5b937c69ab4b3f0863d8e048693bb1bd9ba2e9f883b3da81777001cabcc41900cf0173c39ad208dc5fac0547deb5cced305a8f8772d789e73169268e6d64e83edbb5f0d9d5a456fd78ff0f94d2323ba2a2200164ef48b5dee2c4554783fa7653c8d7fc28b136718272a07f20a13232b5d7f5d426c1a52c02d63b66b956891fa14ae27d86b76fbeea8050f8d803a42a04c685d817dfbb29badb715d9bb953a223867b53a0656ed38cc2d3a4bf782859bcd4389c828b040edfc71f4b5688157f289369145975993418f6d7b9e3d7f580cd6b2ee63a162c9e775743fdeabc97fe02dfb20d3fea588766a357feafb0a96afd1a32027ff19326c181a7e22f730e68a4a392600754b2c10118ab8e171631be052495968cc2aad0c69ea3b0355c875a0e913dfb6a5f52b8e1df5738499ab56f81fa4b532c1cc86df83f31962af5503755f37c6fbba6b4802e6dc1ffd10d2c590b6cb6587b30e14ddb08a4e846ca497b9a3e899dd0a444d65ef23b1a723953e6a4a6f1e1f0a6e19b418c5a87e184657bbb46e0b5551505b4958fccf8c081eed6397394c3f06b82926d6c699bdde54cf8817eb5308256ed720242fe4225a6503762ab5f6ea966b2ee37b85476f331a62e566f2d226f21466d516587c38013631d193b4a8255c5192be489d979f3ab6cc0e70b6282be33215aa518447c63ff0c3d444d1d67af2f4b9fbec2830e20199528872b4a748f36333597f6d4b1d6967ffb86fa5da46624b1c0b3ca6a19b97f715b02a44bafc8e938df066555090a74c5929b0aac456bfcfac8c0b30e7df5c7b47fed9493c0596e8ec662dbdd7eb6467fb100ab1b369e47683544b9ab3c72868ecfaaee90877ea7c96a164a6c4512500ae09f60d6239492f248c024fe794aee927351ada5e78b77d7531e974059d55dafbb621aecff47c105a0a7fcdf4a5f7e992c5a4d443928bfe214d6a51560618cc4d9843d209d3c8c6fd3c57035ba76070c0acee971a811d1a7ab8da99878bebe628e9698607e00d1305d3e353b93370883af5364330ec42016215d1365b8c0673facd1e61f7275b533730dd0a0ab3444ce63b9ce04205b54c9d4f503c635a26cdeecce1f802f7787299e55da44b7e48d7e5a20b1ddd33990408b63fed3135ac679bf15ef63f3183832a4d6d40401e29de12aeb0f114344e6a9534cdb7e7336e2a08332e72a0acdea60a2ff149d8b4922c8f10c8317d7ce1d86c2d29e6f9fe8a02d9878b83a99aa2ea4d909a5910dba61411728db540b17244a43e57971bbe9ce81df0903c7fc9ebd52ef4b1251c214729e0d5b9fee80f03ea498721c194fd6105d4578e9dca023d710af6ab979802fa3abaa58445c6f132129c419634a0d3a2e6b8a26b9e7e261e1e2ed27bd43e28986de59b6636a37530d126a5ca58d4925f684dd2a6b4024df403f60de3a882dcd55b5b087859a6016a330e99e9c4b193a3f116bf247b357c0fe3dff49dc9f5ece2969bee8c09824bb13559ae9e4cb18ac1f40ba53f1adeaf6c6f6ef4eebc9dce6b03cf8a54d97511acfd8c8184a2ca789309f440d62be4338441b25bd07ed4be21377108dfab9a3ad006d99bf95135b163fafa16b95ad01828cad6791b4e82d7ac7cdd4c31fa69181c070c24305fe4d6ef148b6255c52ec5efbf89519e265cf54d08a05dfcd823bc5fb702063a0675552c471383a2069bfa5b60a4e283f627fe15eed5db5f2b23d2594696665069464a47c5524c0dbcb9d1eb1dcad3f01e1c01269d3b3cd1879c6b400ed59d5504993b5da7f28396816e6fa33bd50be6f3df70fa1912f9eee2984882062a0d6c8635fd3400ab78bb1fb19b8584d8b472e1e32994d7cee4ed49afd443670f6952a11a327d790986be992cc816d6aba85131d0eccd2d7c86f4f8dd9669b1278213a5c51335e208eb5a038d0de75dbd1a6715748ad7d358e6a39ae476db12fbb7caeb3686a4ddc2c031de7fbf06302f2ee1753d6051b3a69ade427829c9707a0d202a6a909e2036aa1ea25ce58bec7d2ce44badbda186f6158a675d083274020aae3fd1689ac391d9e070d5cfceff1c4c9476d1c5d6da65960ad08b3d796b658284cae7fb9498aa877e8427995bb64358efb5e0fe687daff498839ddcf55f2d5f3eaec222ee406b81aaecd7d8c888a8d7f8bfb5d889ff6f717f67ee01fe1ce326cdf12efd57593461644eb3439e147208216202e5d4dc55d677468caccd47aa4cc980fc60c4b5d38dd995e89d5f9a5e428b4a21dd454135912b1be976bdf2931ca8ead9dac979ebe2757ec915fed451b47d265a88242155908a49ab67cdb7babbcc84e64b150a7f262560c6e1dccb5f1bef18fe9322bc436d586f5838d8af1017809ffb86175fc79b0e7be53a4339a600270396e30122bde5257f4dbcc96780e905dafe08d21684470f6f0e4a00df08d7ea3025d75ff0571c79d18c7528e991dc7af20b452fe16ba359d0d3c8f028126e5e3382a7034e0f53abf382b263aa7e6cedae9cbca1419a46ce9db6fb794cb9c62419b83fd790bc21700ab3fcf065487ca664ef6dccd7d8c7de64b3736fc389f36ca286d06e803d96346a62338130511afa9131dc52c340f11a0581fe4d4334502e328ddef0fdbe2d94f2c6ed7985882ed5a5c3ba6f0b3da2ddaf50d3864346851e698db55591fd085333fa915f94e1f958367427f0c7a0234deecf797f916069f47356444ebfeaa4b56cfc9719bb929c989d5ca3e9cce8383938ced8783e6bca45ed6b818856244fe6e4bea7085dcdf786d80003d7f45f1f861f8118b42a71efeb921dc45c911a073f92ffadec532a9278184e1433b21f63d8e876df95203cc8716eb72fa61b0b4e5adf718b4e4f96bd5a07772e0de49db06350a22168d0e4851e24c98fae1fbc492cb80cfa607a35f9d2e1cf373e3afc2aaf28cf98993d691c427a5680520652decf8b8a8d00676a49a157430b2ced643d31e52cc2ae9a01bdfd22fd8d54eea1330b0c4169fe6bdbfb432d58c71284d5a34708c2b316d1f6e5ff9f0b7001fc82aa71c7860d5c3e0aec2bbdc22088b260af952fe56a19342430ff46978278f11d2bb6e2b92aff7f1ab0587729d3277eaf5ff050541d030a4c79081ab7e0010c37439bc13e07114a3e3219c10fe0543e8ac19693383a69b9bfe3202e13d0f0c4e526cdc820b2dce7f13d08d378cfcfd0b0f5ddef02884af874113623abab7e5ed58018f49e23d1ff9260e1c9b8d2554affb108bae848ed700d71e741edbe9a10c6f7922bc2b380f6cefd09a4146f02f7b3e2cd75c0fd043de1a53002ea897615de5f5174853706dddc7378ddaa6b1cbb0202f2b37ea2506c48bc70bf386160bf8f1a522392e7d7deaf994f355eeb4b40a9b4ad734107d6966649f363f1c22e98d7a38d42ba4a27625f60023af96989db0a012528c2478241f20ea296d6c8c116cfd02664791fb2936abbbbffbce7e592dbf9982024b02b8c53bdc29a03a7865841c64d6e14fa5be38602281fadd5562194cc8f4317e9b9a5eb13e56b8c36cb054e9fef007c4647f05c5226e67a3e1c7648f96038cca687a24959470988f172d7dbf0db2990e93d5da9849260a7f586ab779e1ae1040dcbe377119b78fa0a4cc0d1bf2dd296003ff5e84564adde0cd0e691ec80e26821f0652e41eed4b34fc0d1abc78a13b1867fb7d8a90d5f56c3daf3420180e3fed196e58513692a0fc2254a1b975af2c7cf6fedf696929c050e46eac1658e213082e1c93ae494ab8238f9356a7ab9aa682602f32c93a515a59eb9af644531bf84cebd5996cc9007d786a9293811684e2de448af760369661aac20c1a10babec5128403f44a0840df854401651b9dc66bbbef40b6aa8faffef2c82fab63667fa3fa60076d347a11d8b1b7b87b00515fe5e1877ba459a5e768b3e53e47ef01f5f82c659a7ec94c456e8d19e949bd66997878990fb0c55dcfcac122d57cbb4367e646c22ec8f60823e19beebf4e8ae72efc40440a5a4e3e925517f9fd61645557538fb1fb382afe98e33cca57ebb896a393d249ac86e968eadbf19e20cb5f1ddb06a41fb57eb2d786242acf8472ed1ff848941183befdccf0c3d528599a4def142f524b68c1ba1a9b91ada6651d436581ad51a4d929e0b6c7af858653d190d12e791e9838ef30d3e003a9d4f407e93bf0a4d15d85d003f98b53350021b3794e474fa4af9f9be416600f1bcec235915f2ccc3bfa043a9532dfeee5f130de466e94220aec33480b34f01df97027cc9c8622f3824341d915209f69dba056ad27f41cbc7fd0788ad0014b45298fbf9cb542ef4b6cf052278c08112b7707e6b8a927a7824cc2031ffa4a781b7d31b5de57ed69afab7829591c980f0ab4392395de2a2c6a487994ca3de07a37593cd0e66dcd7eee9669801a4387d987935a6c8f4ca8fb25647f9a58d6abd0a3d9b8ba0804538ac51c65d21aa503dfb0b496e27b551ae1507f698293783b796575fb5e9522437b3fc77cc1c64b5c8c8436bc3bf67354ddaf40adec308da34307a61276c756ec5ec5d2e2aa33a433bd95910dc485cd68a8eb119dc7f7f7a8789ad3157f4d9586f3eeebd3be61bc5f6eb1f2d0bc35b530818493a9445b7330f9fbd2af808e07748a40a60769ae67d26366de62b3665ca936f0dd0cfb95c57119aa1e26d86f872e15d545e680fa0c910fc2b27ee645c99e6344c99ac3a15a03ae94824fee7a70b64f25158d6ee7bc35b95f019bcb2a2010a974053d1a12cfbcee3e0ec59ce6ed4c7db172f9590a0d17aedbde1dc7417a4e62678628d5ead4bc80a3cdba1925c352cd4c1746641face0ffe7951932e85fe9edee3a2af8a1bc760e01ec1c988f91b3f3fb2f5fd79851bc9cea8e48af98d03ececabeb28979ba5af78d8d37296d1bb0ccd4ec7e35569f684bfeadf0e249619b4b6996de1480580a1db9b78970c8c75c0ccf37ea2cf432675ae89a7cc53242129ab2b3454b151831607505b5e1a15c74f8843b88ab9dda50c60394ea2b820935a5b9ee9cbe5cb493f065e27c2057b014e8a4c363f7dd44393b91d7193a85adbc900a2ba559bf7509f85e884520e96c61495d52ec2279ef5fddf940abb5f2877f935bea0c94b1c24e44a583f3e8331c6527a45a8ed895fa4bbc9ff623c3b609e040bbedea5bfe394ee7a00b1e94b20b3e0183d68de7fd99e229b4533c2c123ca0016e8df6b13c708e874e06ca27e90c60df8228eb0bf8e077c165630f06f696db577f2dcccd953d6902093fc504c350105971de6b60335f69ade663cfa825df7024a4479be45fc93b3e08d5070809b5b3b74c90a83a1a363f1262a096168210a84b989ba5e16c284e02f0e9df9bcf6f802f3ba6d81ae40f09ab2924d1cd8ad22d7eaffc0660f69ffd5bc2e5c7971e525f1f9532585e479dac12e5cfee50725cb4ac5ec4dcb1f9d61642f8061e91140c9de09a4356a095c6c56aa11a226c5155f0dbf9390d4f101f0925ae559e6d466dcc6cb54ee052651c6bf9904e9f8eb47c2a399d0e1dd81b9338c21aa7c41a6bcdab7cd8639b292f2c76a2febb9927c9aa09e1ad3af353a769172ceadc772abaabe2a9f1b2f2d24d194e7cd9f168dacc1c10d3bf29a08dfc89e323b555359133e20032274a6e9634828202dd80999e7a2924b6662f0a401d3f61a39c9adbdaa5b1224823e277d408b85a5b55b32f56393c163d35f6589645bced756e2aa5bdad218488549208972774d05858d768a5f3fe55816005e65538cd464bd09b82501002feb79539f756cca804b8f20db2c9a328f4d424bf0b3af359521b1d10784bb81b3b57700ff9341d8b6d8270fbd106f66a9de1cbdaf5ac5a9d462a61df5b793535438099dc3c94074c89be93c250b00e8d5ba862182e15d5db02d4232bb9adf600eab64787afb542b1ecdc7a6d4781a143d2b6fc058eb4c64da0af002504d2d35e3cf0a8aa5df099b25cd803efc981810d7ddd83455d96de055a1abc0f6ce29af8b7743a9c7db71ad8960e8d7da61cc862068f3c251a137926f6d51fbc130a2013c60c7d08e000bcb40b5645adab263a66bc7c765c981f7ba3ce2bafacb912afa23c7b73aa53a38bf1b92f1571c2e64bb387aece4e209b8b38736649451ce476bafddf55cbd21ca9cc032484e258841ccb3371cc3efaf806274cda843583cf9bda54735601aa9e0e7536265f45ada38fa4141bf20c44f9d951ccffaefc2c999bf536a9ee6387492f0e07b627f1ee886977f159e91bbd5807c0782c63408a77fd2a893457f8887e70adeeaa3feef2e56b6c6061d5b819c78c9647300c4f2f4b8349f11228566eff5094b22ad471ad714625aac003c4314bb2ececef6317685ade56f536b2a7f800b2ab40ede4414fe10de39e7e2ba757e0ef0b3000bc6a1c8404f442f68a372991c2336a8ca3294af2139ae6f46297ab75ed99b817e5f1e1d9656eee4b56c410910007c3bf58427fd30807a353d139420e47c262dbf6161739535c4794d8e61c09ea800f79a37018c23211bf46d621567852b24251d6cba208465e0d77ac8158fae7d0e5aea9f18c3211d869d92ef41da512da39cc8e2fcee08df3a13a84d4cbfff44fbbe06f48a8030623434021f97ac26b5407cc7334c8af3527538ece0184ab8dfb4424b99429e0129b88db103a005454fadaaa18647ed2e844f5f3867133c8f0a4ba63e71015df31e29a689579224ed60a8632069ea82ca2c723ed2e633f861a53313b00d9435833defe31dad2091dd7a700c5396688823abe3543f3cb78f23e15ca2885e6d00ce46a8cd968687d8a0dec78b84de6defa63a937b392f9cd34ef089c53c32753e7e1e1a8df56f55263ee5ccd76d9eef22009ccfb75fe6d641e3ad58fbb170aa8d0c80c016fd65c8d2abac03d2376a91a135aa2c84ac70bfebf0d21ccac507041876a1720938f2754a8b3cca2a34a79b5f04bf6edefdab254c79b63ade88a6a418e62ba3946ecf6fd7132ae9b9d16119eabfe643fe44321f10bebdf1c320f33d1bb254c6dc56098fc2908b068b6d34bb56d0f4e08b744eeb427351ef7f9db3fc1eb8b21d40e312763ef6f4fa9b3114d7272f60001fc107829cc2377b00e94b9291dc07b7a299ddd48260462a6c6329458737e0f5bce6be79800f4a50ccd90a86daaaa52a6f2a5333f18ac58e0f21e67d3db1ccd09f51ef0b36a673b3b8d1680e8337382f78404df920342836e165778cc60d40b87fd8346a0fc899e960dc475202e553bf72062cb95ffa5dd85daa49ca36a65d02b4ade568800ea0172168ad7935e415e08e92129d895d1eb57888797c4d3324a5086b23f0ad9e3b9f9d3f17b6e0ee3016698037fbba7208d57e12a97de54ff88281ecf661404c657947922b8d1701c2e7e6e2eaf8d302812a48eea7893aad246828382fff5e06c4b917b09b8a56f65197bbffd420eb229c6ffc0b102b33da674fef19c78e5f5fb462b88f7ea35b94294b8c6621674769922868a4c93e3cc17331d72a0b31e7837bcb92a0b2a5134a43c76255bccc5c24fa31154c14e3ce2fbcdc3ec3d76f49d8f23d6f7f77769560aa42bfa1d2444c06c29e1ab658f350d92379e5e8249f69675a4f7fc9c168d1d2f4c2cc6a899cc0aa8dccfe4c69f8021448dd3296ba33f8596340c77d471d3ab48b0899aefe1e152b2ff84a37e45dd55017df1e293efaa5468b48ca9853f5698a863bfa814cd3c8f01f5a771427119b2afc7a009aa476e924609f9fdc5c5179f67e9cb652e2a6868e07fe4d84889af51483e319d81d04b979a0551d64c3170e11025ac59cd9d757e6bec05df25091b5fb0815a1e2ec299cab5d95014ffe15854fb69566623f0f2dbe097bf8424523dd5137b35135aa068336b5c73350d8b97f36a58ebee4b8bb4782ace7d73c8b71358e34424d5ce1f8a130c8ed15c17cbed19903caaf91961461df94883cd05db7d8a95d8e09778acd28dd7ad46d366e9baa360aef0cf5f3bb90a98f4305dc7a9e8f93ef1fb7986426581a372dea93faf40587b9aafb38b5d9e51377da995da320641b72d83f6bddfbe273c31dc68519e1b445aa89b3ac2219e6a21f968b0c0ea79e12c3c92fe4c7307a7e6a1e31c76f0e92fcf8675c4e9d229c56d13594797f7573158a179b155392ce3a6fffbea06778b0b3bb2ba1b0b47d02d0ecc32cda9ca5557d01d145e812f8df406d59d058c54bee9dfd0685eecaadf924076557a00916b6fac27a3486f34ad08574029b18b5d925f69a6e9fafd7c66f5f1c4e05e52dc2bda385302e376fc1060614b6a4cb4f998d33bc1bac879fb660ab4815bdf7643b4116de54b82a37fc8794241e2c1f640ffad56ad1f31c36a5f903addb4616835df25e563a033b72836f65d8874237abba48137f8a149860520700d5fb687d826497bbe437ff000c04fa65a23716803c38a654a7e3d69de6d0df1acda2793b7f42deac5644cee7a926c81b18ce175b6c459b2bc553da1830bb58cf42e6afc72ff35c8da7a53c5554aaf4bb8489ee6c4011509a9c218938c1f2ee4c85b2431916349f2e58d7090fd1a81f1b72548bb77f19584cd4e75f0227e0b96b8630841b2982033c5a67742787cc184de15f8f6bfa35b0c8663f0039aa5c89912857eb6dfb120f022123af64b1092ff8725f55f6c6c38f63b1932ff8713fb0e767bcf5870ff589ed215b11eedfccd13a6c1c0df9ae9392e63670f9bc1fbad4cd8728eabbc642e6b53a698e1bd67ba7235a15ab2d880fbee0a10225fe7c20062b94c1417f253bdf0a4b146158f1b51b094cfc84ced327ddf7c5bce935eae4f9d2f4fcd56c68c97d51fd7a78f6a1ec6d7ceac3465e73998d6972bc728288db79f7d41446a61ece4d3810549f0efc219aa53a821a9a33e7d7037760bd990e068984c9f1caefe2a35bcbd08153222020b9c7155e0f978ccbd0ec7715702461f5ddc418207b561d29df8c7b1330d5c1fbb398e09adf58f98145104c8b5104625b13f21f1596ddf76acd2dd528ad9e7956ae48a0d0b442aef0c9e18f612d4b60a10c891147a6b37912a58339034113e806f3aecf419914f8dc788b32721a576dc946682c7aff6dc9abd27b69398c3b0d4338a4d59721ec3ca23c059bc846635cc5fddf9d9757efde60e74a37b9719ee676196da825c74650abfe3272fb523b0c85ab388016011d7101c1dd154abf14b29bccdc33d7c2c53c2cf16870e5c0b8ea8338dfaad56d2ac330e5e446f57bc8709182111c3e94e5a859860319e767890197379b07d7b05babfa6cb611160f0d1ce943ef40051e162f0364e33593351f07185a065385cebee173df34889fec752236dc3e8fb0cd6f64d75d7ec6465502d255b4a46f8de1f7e95c944d3bf91d8d7729e8cf56b887a3aaa1de24fe62d86aa47bcda8712dcf79434e087e1bd5609aafccad0d4815e3d6ff43f1a9ceb31c7f0386ff92a64aa423b7d07114fb954cd95ed46b93a4779e284608f30ed51aaf381ccf3bf477f913757dc973bb08a691553f4fdb10ba9cc9d19ab12337a1154da3c8e565242000f2342bd71e509643d3f1598b17f3ae80900ab2598f02ed7fe295291a29674804dfa207663868a8fa738799f5758ef1e214c7f9c2bf1005e2503ac7c3299e7bd1b969a1ee0fd8727aaca418ac241600b619e320a1af303b24ac9c3aa069cc8d3140be224cc7a3fbd970236093e6c1ff9dec3fcc7386dad3345f7770bfe974073d653291ab86b525fe6d71f616a24382e2d302bc0dff201bb262562c76a16f0a6e8f884874593eac4adf17722f3325a98f0a4638806916b5ae4d1c7b59c056f3462cd540ef94e072bf71cc177a4c87d24791c22d7a391d90081a04d6771870c710a97fb6bcafc11f27ebb310da40db3de5fb58b44db723433fbb5c833551a1def972e54ceb5c163ed049a4c092745c1b9ba2ba5be24533b237695c25bb920e3bd9789ab180aa6e7f50b449f532f790eab5c5e79ad2402164f7b536fc1952b5da43898aa2866b6239082fd26fd12dbd8a1d94c1421276812f61dafa85f7f60299b4d64887521ea9eeae4da71b8412cb5bcd5fb314b47590522d931b4c99c4afb5433f584b39d9d2f6490c6b66b05458b5687d3dedd3c25d59c6133f73acff963825fea7972c35922c3bd82d4a32fd2e0804640c3b238dbf4dcd9b2368b76fe7bab8f3dd0886641a91088447fe6d2a3f5dbf5268a81ee1c03b66adca888d027a601dc7a18e7939abd26df0e86934e356c8d65d11120a392d5153637c43bd37719bb5ded0ac0bb211a724b86ada7bc66ca7b5cbd4388940293f865d1d00571025a386d9ace79b7b245c100bd50f322dd93e224ce8715ebc429c197a1677ffaa4fd35e468a8698e2063f1e291f48567dcadaf90a90c5fc06868f9f133e64b57a4d2de0f70936a6b507fbc81c432edf68a11d85c830d32f5a4b3d370f6dea65b7a448ad80267901cd0026313d93bd6a18c0c1176e1e8655e710d2cb1427e8df185bce60cde9d04dfd1e9b84fadacd1bab63b1a2a0ecbc277e3699948ed2e7dd13a00ad75764d647073a63032052c97f98503fbc6477ff26360cfdb06b79452721af5b1b7c51b2f03b4fa60b172bf9c7b6816da7f7f1ebde729ec40e9025c0d80d9b1f7858cab86cf154cf0ea5b7b70cf9702583cdf812b5ce41c34991c49a878ae585baffc08be84b86ab90c6bd270110f4b4391daf60a5438c879895248ca37e342e7bd6cfd4810a02567b7471f40edef32822931cf8f38e2f06e57f8fb0c464675f8a96f334e3d50c4e62985dcf885462deb51efba7d1a06ecda876c2cd049fa5818dd35abb0713b3a71daf7a746a7d91da45f09cbbc7c32d2045e25b739cd9fb598b429ce940bca7b49b19bdbfd871bcb42b533e2d3f5a3c10328e73741ff6ecf94ee1cacf743072d08e6dc0be14d6c0e4e9ca9c8a52cc90c675057661c18e1b17537454c5ad49474a9c0fe6b11b14e9b7774f4062bdf976cbd969dbfa3a062d17cea5bcf8ab88525d5504436e4ff15bacde6be3d8ae45bc3901305e2cb01f24e5364e5f692a921394e5506c3056b450b67a2abce684af62e2ba21ed61c424cb689ef1e7fade30e42da0434c43c93f61f434d27baef6af4e9a966a082601e71245e46ca3ef6d05bfa69f9ba1325977fc38975f2bcdda4482b4d6ff1c495aa9d02a2d91197a95e3f4e908bf3c64b209e1f8438ba0e284c04b7aee7d9e7b97fe87a3a427da2204acbcaa3f916fc86d307cda19edfa21e2389f35796ae77d68d99075d5e7ede0aa46c750b2d980b0a654ca4fb8e9b6df1180ac678d96a1ebb04e916d891071ef7d1caa69f18f945277e991ba114186200edfa1828943435f5306fb2ac5046b42e0f859a36c6dde480998b43c6caade6990ea4499198d233f81e952e96002406083514b3b48312123f6a944232e9455b44134e3831923d1cb09002ea4e1473ed9dccd18d1720e6f25b6c098c71d191c7d78bf09c8fee7bf69ca08a116e275b64e933e32669da55881b9188de57c4c4681b6a84a556bd5ac364053e05e681050420bdc609f159da505beeb23a18da7d1959b246b8dbbb80e0e2c10fd4ba7f97a7d7964a9362e9580f75068595cfc8da89072d3835299bdb1670c204d29a73a6ff86f15a1ed5003f89a4be7df9b9d5a1dcd15edae56e04e9c85fc2b649e4444bdb4aba487482e2355b435df483468d2da2d978def47adc33e54e6bee29a752d48dbb37e8538d92652f53f3e5056b8ebe24a25719e0fa3f4dbc02c5b5490a54191ec25a69038b82ea8280afdfcc8ef7eeca7badc4155dcedaa4674b00445dd9ceb4010a68ef8e0ccd0ec954af0c7aa304d77170ae87c3f53fdc0caf2da8d8962c47538643f59bf21061a00040c55c6a634eda08be328e3346692ee9690f41de26a0a3c73d51e0372fbd0398be015d7630dab9a397aa87372c14e90ae88b17c4e5f787b3e5f55262cdaef0fce158e8f7e9aacf1e2040840824fde34d91ea6b83de02155fbd8150636ce96b3034b7f2259ad94be9f3cb22990b4649ed1d9c7db425f156f56cf755c35cdcaa3d83b9545ad4d131415395b9ca70392b30a3310951f29dd7b3c0d542c04f3bd8bd181efe149d54b745b4362ee4696b4c305cc7190e21c7c065725b036959b6059d30cad8185daf4ef0d3b704f86fca87bb73c4d77d0322230f72244c2d818e376678f82f4421e4c1c9de7f9ef76458257c90385b37eac306a998186eca3c167bcfd22068c3c8ef509890d4168d1883c17043f4fed1de7f2cb0b0921fbb7125b30c55e37e90baeddb15b41125c179ae12ea6cf8ffe9c2947c08cd878b4ff5ea873591a4c4938751aa95235af70a50d760ae706efa29ee3d5fd679ad7905a30aaebde857a9781f8038d57a368eedaf5b8d02dacfe15443dd04a11e90f8351f2c9fa69a22e7312cf2bfb021fc01708025c36447e52c829caa3c30d38088af92b6b7e473d22c339781da9265fbcc771a7185e69324d1a9d4c4bbe0929a99bcb9baf2f789a86e2b60c1bb3ceea3976547f620142934e77ac92b01cbea0febdd2c3218cc2758faef86cfdab89f8858d84fd0a00d84184b16214008d6224e04152f8e84564aa53ed541cb1b77db5c878228f022840c6190acbe24a496e3b8c3ac9d2945e2b9f9bad4afb9e2217dbe83a2b59420e6da41d647b77cf0c10402b0fc183c642c01c24deb6b46847fa743a98a0b0789909066bbd6bdbf9116e34cd6eafd1ba648050de7c84b9a844a055a2b0660f4f796a6f45d076fe8ab0bcf26e310f5465fae87a6638466c29cea2dc01bf9bc7ad7e02334ed0b2f787a2811ab5570b26e694a3f96456924c7da6a1390c5d07af17e98347949ac83cc356127a4721412c6444b94c91e48cb0c90968c500b7b07e479b42989ac310c8f6cc61f81b033e7e2b6445b8bf852ee3ccb72e083f335aac2bea8d7023d5b608e146c274f8479155e859ef78ef9be823f9709f6fd411e80f998df96072bf511af496915e4af74fa2911764e530768b955c5b6d13996bdbeaf4200f183247e266cf20cb6089e63154e443b87e11f0c775517cbfdcbfb85d8b348499f1e2740897175ba546cabef5ed8dd4a224426f67fb0f8e7ba1bdbb9c3a708c919de092c9c637ab5a63eb92e2a09cf3ff46bf4c8d13058c4ee7de1040eb6f455eed5075dff8bf65f2b865ea00ccd9327d09d5949b926b5bbb5509986e4b4cafcf60f4eb8008702e4d14da71e89d515d33e9d27943d61e05d6131239c005f921c45547b516b480871aafd5f9a0d52e3134203ccaddfb9397323e4c72cef631c1dbe6ac36e41bfba39c3586fdf13a24f9cbc90a97bf3b4ca8b882cc3091acb96bfbfc68b3ba7718e81bf7d6bff987011291d6a1c78610c349cd5c51f13412cb0d379eb7eeeeb1936983889cd6047df0f86a5b2985686572f297e237436908507749b5d56e8be301be47d12460df659dda4bb5fcd0ed799ed85a11c65439497999e763d10a9bdf0cf2a37399d3b767766b83c06d632a67c63a8edcc45bd768a311fc7c0c0e437a730b0cbb0b48589e412cf6f24d239f531d42d1915bf0a67bd9cb740bae49faaca3c05a1cd4eb9dfc2082b7f73b925ced27aa0b38fee0145c8e40300ad6ad3ae35c66d9b2a1c1dad3820e8055d159a9912b1b6a24ce2b2902fd6d06683c905c4a092391525f159756db1a45adb758d2c120402b575c6f494fa1bdc87a54ea9bd82db0025f8130c0684a585ff909b938551fb4028e5a8edc49b0fadfa2f673b9e736b1ed42904577bf0921549218137e4f61801235986eed49e181e5c1235bd24e60ae7207a7bd0c5b5f6c806b1dc9162ea8aa89310d1bb051282b40db54e01fdb09583901911c3dc3c2e6062a804c6fdb4c48cd4ba1bb139b46f784de77e3d7bd43ffe8b3eab63afcc51b261df2812643cdaab6b2af3ec56d76df29a16fcf46292a4c327436580c95ccc0d8e447be6a8fc910d9185489db62c97720e33a079d11d385c23c49eaab98d3e345d47144f476263b93629e52b0d06e029377c753f46634fd66628befea9c129246ba8b304ca1313d55fcd6708840162e1bf1ac179b9afb48fd67a6038857f34173c2bda27a26b8c8409b9f59430e2c3c6335f928989d0066599e0ac3839d81e46f781c834a51127ee7f03daef043154d2a1515f3065cc47dd4ade80e5a9ca836b584a2aab42a41df2b4f7eddb339a2fcb9140bfce87ea9000bc600fbad94df3a7eca0db0e2af7c02795832d37099509e276663ed08d86923db9bccf70f6d205d387dc09d73ea0af8aeabcc70d6ed4e04ac13efa74c7ef935da32b281ee58df554a1ece8a17a46dfedb2ba18a95cea847020362bc2b0564f781766a605aed2c5475c42202afeeef4cdbd386aa7464919f69d6d02bcbcdff5bb5fac0f66cc5596ec3fb9a8deff47000dc29e95c82370033078c93ba8821c24665b6fe66de683b2a99dcc752ba10eb551b193d16266a8c8b86f1edbc18a45ac4ead05b9f91e85ec902e9c9b038befc4a3e6dde76b422a757847efb6eff16fc0a2fb6d842b27ac3a4bbaa2f6a7da57aeae9a2c26d868e14426ff3382a44cc209c718651617b0e5368e020b4300f011dc5f666772af3603c799daa55eb37c0b9734b9213cd3043bf2580a98084f8043ab9e09eb84f11585dd7a3898a382e5dc9179ce3e6d6ceca6a155e9eb5db2d7a17e597fa73deca1b268656b90c4140d88af106e064eedf6fddd821c7cc806a0ad426ceef4e0cf314000a7f3e2a5deaced5435ceaf9eadc7e0d865234e450b63cfaffeb236de9e38ac7b447b545478e26b1b36d1ee0efb1e0fc18dd3d7cb269e6ff6faaf560538f34def724fa80894ea13204fe55b804e5bc42c94560e5d3f559ee6d31d485b0e4e3bbd276146c24d9469d12992bc8618daa21a29b98cd339ff973f6baf19772b46f8a8373499cf169454a6293ac4505e4c07cd35d59880cbf12d56e277f494de039a7a0a8679577c883021c6e6e6cf554ebc548f2e2443be1e3cdc3997efdd6c27101e57812d4cb064be6491e2ee076e0c128b04d9b08c0877c790e9edb6fbbbcbfad9dafb809ae3dc86bbebab375e8fa441e0034b8d189026ebb19d02a560da22c8f206cd80b7666a0e8f55a05ef941d26bb17f6af0e3f41d4db33af9531a91d160e6807d21c20dc9a89d6158c4d5a3cf30f54ab4d5918c2c110c32e2bec9d4d6b8525d5893d253128a20b357f92e0181d5b57a3abe9e3298602e0c26148179a2631d723bb3a206f2d0e08bc0ffed92b4c4ce3c97f8a5edf7f7e489f47b5d5abff00752b0bdf05c6db2724bf9a3b0132ba98880c39e61d409a358d96e7f72293fb1d07f6c863a84530e8c74d7e910062279f3e570dbc1aa79bedd1fe286d3f6c2cceff0a80bf86dba9fd9d2afc7362d18f36892c494387b68419f4944e5586ff7ce9cc3141d92080267cf50a18c70dc407422db0cda4c67852d833f75ed54e8d09adf1929257217fb6b9f2019c924abfbd21582e483f6762df4e27b4350b701ee96b1c1d73d74d3f281a11fa2c39f1693e7880a073f63b8efe5daf80194de0371ef4f5a43c6b39af4634486d4a6aabdb89690ce1e6b1805085fc2d0a00029999617bba8c45c720140facfc461563e44367b9d7e45d3bd944dc56b91f184c4731b583cacd3c279b3fdae4d93c16c6ae9220d2a8fcde3ae2902c70c317556b1efe7c195e90eccb2bd6843ba30f0dba6c916ac013e682dfc1af672f2c72fdee72c87b4869060d83bebfc2039ae04197db95a3c961aafef9f7293c9173e0e2ae39d2ef3f54e118dfa28b44e025b996bf1708b96cb2281a3e2151e83f26532406293e910b532ed8a0f8472264ad68219d5d6bb7fa2254326d79ab846c1630e3c167cf0bc99f3c53130ee7ca156e50824424e5ac171cbf44e81cefd3e16f89c2f68f37cedd0bcb59ad0b2154221cf7ec27cc8612fbb9589524a0e7b08bd322b10a9527d107bd664fe2e81f0b3035f6911de90d0095bfb369cc9338af74c4eb7dff7beb2b06a42a2a95d97f7c02a7987a69d8d9fb936206588c2a2dbcaef12f0006b9a057d5f468660af1f6625f1c4375345dd7e0a345020d4fcbf2747ed750b04326d613e12414bd2483017e6f1c70902ac2915bd86b92196ce6f2bee6c23c603461c81f60cdb681490f850d7c964a12e3d9101a92b8bc82c7fd93a166c086d0b13aaf2f1b8caccd617904a0b82aa83a5158f481ec427384ca652687eddbe558c48167b9ba356e01f16619f0c853b2296f91509ec89d6f4013b7d6f9f6d8e3c58b96812a810f0d5a28ff4b7320fbac20c6b6d7f18cd62444018c1900dfd03f042ee8671e1c1e70ba5a9586efbfd690d832ecafcbdc1929f3130a8a54c277f8fa520aea83ee2af7df351493df1ed1f084577f4abf46a1c486087a8134dd6016692a99ff5404ef1ba4cd0970a26c1ab3064b7368e67832e5e44dde26475a835de9366e1a23389102cffb6f1f7bd8348b463f838927e475f18cbc44e78d02ff6dc4ca9622d0248aa7af1040e616e8da74031587c62051425010e16e4dac17827ddfabf45f8f4bb974fc4f5ef78fe719311c40cfdaf0ea032c242adb02c5b8f3b3829047253f809749b6ca43848979cae9c3ec4eb8e674e188122050c527d59fd38f9efe63affb43b15cb0e758bafd79f3f792a02082dd1326174ad15dc7e105ae2ded85998a69b9ccc016dadf82e3905e0a5864522ad2153019204653c3f5d2bc1fa1f884c4786d0d00fff4200ce518a6a5a7e32fd04fee273058a2a07ccb4a33927e5d1fe500cfb45c9537361d4ee159f59bf5072b77a49db27cc9a30bb355f5948c9b51409fb6dd09ef283094db12f147241ec735371d35b333c55efed92d399852d50b5b762f720c43ce329a702be79f0f0e54f62a8e3df463f224f5b6774eb875498e2d19110a1f6da20dd716ee2bd219c82d5cb15868075afe33231b34039822dcd356875a1722e4e33ccd57e9b3db5f883a4168a682552c9b4d68df2e1086d7ed2bcb463b36c97ab777b8f4016e86ff7ccfeb001525f23e58ad508bc59b224e38786fc8154089f85ceea5b628620282c74d5a3e4a619ca3a087192cf93fb45fd547dba1045c4652aa38459efe2617fcd343278e457a1c90fbf9543e11780ad9e3b8ee99083bcc7e7599264527af0af6807a01e46bc9545f38a4197c6296ce7194f584250484f87ad36cc8119e4fcc5de86a54e0b509adedeaabed87b5b13b860f5b92085746683cb85bc76742abcf747eea8afced47e1e89d582add6a739aaf6fd3ccebd7915cd4b43220bc04a6aa4fa359c61950f241e0eeb24ec0b0c3f29f82b5d39b7e00cae5fde24f5dadeb784dc0643e53164e4ad10046db42e6ad37f0a842efcb130cc316be1f2aaa4c3c902970d95b13dde72940af8611099535f989fc527bae411599a6d45c3eabdae374d2be3c3d7b4c17694accba6cf8a7a6e7ad04be5cb800b2b1769d4e7916faf12ae7bf90bd07968f1a7c55995c4a81e865d329996ab52a12abfe336e848ed4b16037753fc253985fb640d66e6b5e3c714b41d0f0b742cc8d6fd111fbf16f971798161cfd3805a24b246dc4ae5208760dc3204a85e8b7eeefc5f9196f78b5aa78a07c5ddef22343260f174610e1665fd2a4ee11df5bd564e07617b230d573c7ae2fd06f283dfc0f7d5b8f016f3a82d708c6a9073e7325f18000fa099ecbe727ee2d8ae2b1caf44da0163ed24c998882ffa21601638b3c8f639682ac1d72e312e2e99a9bd8a4baa3ec10d17c74485902f89df7180c1a17472792fc2c1dbfae3c2ad5d16745e587d1c90d6c1225f4b9228584f4d1488640669ae6b60960b622611d35aeee3effe3fc967043574e10845a54b5a8915138dda726fa885b9f2d2d3bd23c969fde701ebc7f0680f9988f82d709b6dfa8331c0079062ed74bd660861ef18d2a9142c474f5e2e4ad6c9892f7f2f24f345f210507af6a90e681c1afad15963935f4ceeb58c19bb21716fa6408981b6923a0614d9803d567ce9e4a6138d8270d8ac409dde2cd933833e9ece6e37965fa770dca12813a763e763e48855da80735c55d3741f423a7925e8a28f12ca699d6cca384a8fd9e8573ecd559b712f8486b4207d7760a31a46e3df3f346636786de62699b66603515ebaf4b99de9e5e076a970ed99ddf3f0eab5bb8775fcfe4d0d1c89e7feddb1a51f97ffb5c161be6ea97eefc2e9d87cfc173af25f18d6541b865f7bfa6dc6ba6aad4ebd4bf40a96a51cf4ece7dce524ee7444e06400ba98b3c7d57ce9fc9b750f3973fc56eff6abba91b6ad680c52e553cd5d24256bee4dd1131d6adcc0ee434a72ec320b9153d433c30bd932e7729ab790104183773529f7f878773f6acbed502f26f3eb3ace5f96be7aa43507991bb1a049cc2cb72f9008b631e2756faac7b2f510aa938024077608387dc5ce32f840d74e30d2b76aa3e3f8ef53187dab359ad21e5784dd841877b643a9ba6146454b00e3f03480c059c23e83725cf7f242413aeff03bdd1e8fd6926148bf22d6ff65fc7fcf6436912c00b22fae7582cb05881744efaa71d376a43e924106626c1d0e8f1e39dfba52035047065f46ad75aac0be96b91235a3a55c0cf5664f3efae071747f414d73e4bb588fa27c84b6b8b0d3053ec5c417c95c3df3a5603df7fcaa339b585022d2bf724aa3edec20a21a49b433eabf95e7bc39985b8f0f69a9583d49b414af5f6013c0444a054fab8603562ca38348288cde57d5d29b49ceffa4e55dfd0679205e3630a54b2beac8ae2492bfec8761c141d79ebd589b3348e6ca5c5c4fc28eeab1da15f21841effd0c188cbe3157ade03fab4b3f02503d6e26463a36f6e879c51fc6b693e875e27bb98920dc282d122bcc3f6c4f764ff8c7f09ffd455d0dadf94d4a989d3944a0d4624b636cfabfa7370639f4cc3e38ed3d5cdc61105c973774ef715962a20a6ec3cd40259e4708ba6dc0453ed09b7ab41f83a2c49acde01e8d03b6a057fc6a5f36cba17ab354f5b6372003b8b89790ef8552cd473809e0ac90510729829d512ac08dc0a952ad5875c461705055b2c94a4243b231930f2cf7358952fffd8ca68f79800090f92d8da2167b01e7c8763eb01bce49b28c157c8faa78f9aaf4a3b69d0033bd5e74b499c414b8f274bfea623ede9232efb578f7e615d072c1181624226dd1e696f372d2dedefd32cddab5e0f5b200eb6f535624ef8b05cc51d7fd0741eae89dc516d5ff4f9ab7e0fc82ad69921c2d68c875e45251aa21e517126d3859e8cf9ee271513ee4e36dccd9f8925a947231a3e79c9f8b56ea586fb20724983ec22967df064c7287af41e5842b3df295ffc8d0be9654edf97e97de423ad6462be1e1c441b9ebab6b368a8429fd66875dbad54a3aff60567e5c80f0badfe0e2a13da617b4bd797da02dc0fff7c50a2344233b564dface7065923d5f2819d40ca6ccc9fcb10acd1bbdcec14d4f80f5a582fea52f1e49860f39c9776c0aab1b200d17be4d6b926477f6902f952b98dbe528aad44db31ef10ffcd88405583b37ab976ab3730d8d8a08bcf4ffb900340b87e521c5a18ceb174c00eafd3aa1c289937c30b7e570ddc5140bedd8491a1833bb839a7a843747363e6e6d05f3cac9c3fc6c837be195de6b8d3747c152aa20e290173758ede5b9937f24a472aeacf387cd2a6bd1accb3a132375f311a52f554f27a16ec717f1c538f1c154383a098e462218de78967a3ade811b8f991584b9740edec6107318f6bef5704c5a9999abfb550c0ccdcc8f53bf50971615b9f513fb2b6c00f74952cc20acb955c53cbb7fcae89d750233243e7236ffb98bf460b11dad6662705dde92b8a928e9720f2219c08d382027ad34a6d0486a01af89ef6f862075ab69d50a195073c249b15db3708f20cf8bebf145fe3a661e79876f95014cf99a3a9044c53570dd0b88909e886f6a1a6e127b0210694cc52a597a3a45ce461595e9d0c6fd460ca31067baa38e99aa98b7dd439922b77a32623601411eeab83ac605516e4669df54329ff6d49c90043e6a253fada300f4fe971a2bc9ab434aa4d2306058807a83fb3c2230b282a27e71da2faf5179e25c6c1b935faf194faa2eb3fe28e93bfa2c08d052bad3da45b2354229aaf901058ec80210e07bfc4c9565569c18af9821aed15b454af33d6d1a36157d6cd482e76cfcc8ea4a5a16d785c44a532bc86f6f1db31b404bf350cf3b0e5e02e257d787c6abbe35edc1dc39fa15376a95cbd8d9f79a897d538c878c4d298dd5f09267681efb697385b62b4bdd979aeea7ca5b250cb1a3157f815b68ce9a70546909c30b1a3aa1231825505efc2d3de5830ed6b019ad6d0e3daaec4c2600e34982cdd8b004631f44626717843457544c735abc96f334bb9ecd0cea275e732239dde95acf8dfbd7c4e47fa609c456684380fb941e77a2cc802403b706006b24dde532d9d6858db149b60c60ad2f31a72d4f8470f6cbd79063286eea9c5b61f08233f57faa27ac15a3cf7660cfd109359119fa4b14bb6e774aa07f6f7fef28d53183a683ccf7c9da7d14472fd5233424f9f940ab808d7e8e62a5a7a305d3b3a8369dd01e5274006c16b286c3076aebf7867e275f860e6c07b2e86c7fba7e94746b8ca18a91bb48af9e9ef01197602c5729ae4a1ebe7e22753bd4a4463b04dd8a361ae45d4a6e05e0435b2118db21849766d34d5504ca4d7cefce08ee4fc6ee93a1706081cc8443fe81366c56243550ee4b21b8d899222f867ca506ac16a0df9f71281b304c0c7b551981ce5d03ae8c752e46c53faf54e5592d4dca41d0fe90ed3b4af761638b8f46e9f6055a61e801f56085b00aaaa26260ae9c4716b869ca357b92b91137347cb38640366991f230144fae2dc0523f42fadb4b69a9d5fbda1faa90e17f79b39ae71f9af0e94c4631a0a56bd4f33af3627ae26e9bc26e6392f2601bf8d8c155f76283fd44ad9a63a84919e6ca7ede43c65310c0e3d521897bb467ddaff3a3ecba5fa15468d66ae1189649228a47033ed5015ce575fb4141f282cbb811736ed626fe36b966a8620a2e50b23d27b64e8bf47eca08f09a15637bf512f24526b0c8e80d10acc4644b759d2bc2bc9944a31364bdb6c35713514ecb6fe77c9e3cf7bbc59be8957b8cc6a6425544b7c486c8d58fa2bc05bdfae91b9a8ee2e8a2d4d654cafe53598cfd0cf6442e214595b3db83998f2ce2eb54e7fa004447470a55e918a08f5636fff0d7eb1936d6f28e87bce380edad9813f91b3818d57ee69a085bd5341458760d588fdcce682eabb7babbcd7ae90fd168900e937a1160180de57292f317e7f0254b7433afa71f51928c9a80e59c33cca38a4ffa1e3ce9a82b60fc0abf278322fda150a0ff68cd91b4c0c93e1a79ee2fbe3a95f0bb5950477e95664299a2bd112c8f5cc292e5461b8405fc41f9216773ff6db7216ad1d56253459a0939f6902bf35b487eea101c022f2f54ff07c4d23ed41ace2cb4e1609475838a6788aa144d39ed31ae23a82ab554963969d8ce0491c06e78be66538c27a83a29ebae0007578884677397413dfb53dd9748be7d19f9e32fda4785a13b0c2bbc6707635f512dcc5824f578429c07ff558db8fe8b0e3c556782e9bdff154e8ab64277df785e987c6a7e82d1e81b23018bf5973b9da8840147af910ba911af87ecadb38ad88c4e63457729688d197c393204905686fb61d007bf1468b5f83bca314eb9f1af00395ae82d4e5b1688888ff2e6234661e88beff5823839a41c85a01fa90883680d828d7046d6cd4ffc67aec20f45cc16a42b68e223b46b4c221ca87b238340236c4303858a32f38f4c7a88e98196502271577ea5ce2addc097ccab4be8aa074a00990669086570ee7fdddc729f0b7ee60e4acc9942093ac6798785c2563eada65fc655b2279232ff84297af102256dec1d0f68eb1c40295da6378b367aa919fc04a820fca4f10eee7796442c9f09ab819cbcb6a2d659b45c41a3e99ab0ed9b51a32d53d8bc0f1d00a92dce06b2a09c43f78221cfb2af2b0c643f53628daacdcd9b479b179ad59e92b74877dc32c9e388f2e6d9f45264c289add93dc8a9001b3a3d9c16dc76452270492dbd6fd8f8c9bc2400469e9196984eb20b5a9bf70146e23b071ac9b114118d2ab331206a8d0cb58b9c1771e6e1650c58392fee71cee18139a34e384b5c564ea2c4b7c8a3aacbd561ab195232ae2ef3e1436863963b2cd702c63af9bc4fd0560a2a4e55efcd91e1f491bac7bd7a44be80d7ba31c8c5d356374fc5498e0097ff44a3c11f448bf92de6e35eb449cfb75d54cc055b3b2adceead7a0f37d4db0cdfa48eed73c8f1ca1477d9e951bc7ed7943e891038a0766e211650e9da464f738a9670bd24cc2e68a34ecc73558bedf5ebd48a0c8826a6b4cd77eb9e50464488860a5598233a45e1ca84be2d57b9bfe8b5d4427b132f760b1994ab9c53de821ea67fbae3b9c541321f092d9133b6b6b2f9865539483dcc4ac20a9a9ae59c8dfc7c55edde63bc3c34897648302f9b3dec5b66441fff39b3fef43cdc3579f9ebbbb0aaea2e18905dbe551c204005679fecab6de0b8a371cca5c0a0c3c7db225ad09bd9dd62cce534d3e46c88968ad13c511c94c8ec60b8db7ade8b8e1b58bbd5d8bedf38a3d850ee17ea70af4cb9aa08018ff0fde01b45af37ed499fd0f3ed63547cc632b2f51284794c46042fce0bb765e16fde4b289cdb701acfcc29daadad792df88deaa16379119f51336b4ebf5b2937ff95dff1c96a015e1e5708dad2010802f5c8126f639b7b018164e90107c46da0bfe6c4eb8b87d049171a1b48a568701482fe6a56643199811acbd3f1dd18d6710d9492cae49e28945f8946f5f04de04c57523c12efdd68bb96766dc6d5928685ece1071bf227fc86cc37bb55d3925616ce859311bfdc31283bf9b7d254545f5cabfc88e14f4b21f41cbe67cefdc3638e815b38f2f734ec20858d9213a2f095e79d8fba7d795d12a3081d585e104f238cbe08fc6d01c8c222fcbb15a6e63b57cb8283b62d30e746133605e1e20d12ec16c8521a115805998d5f993f48ab75d64d710dfc84d522a9d60fc91c7d097cf8f8ef17c1c4da1adbb83a996a3fb575d16e26fa088470627deff776b7e4620e452e9a63f6bded48715979b30e08df6ded865b3d827a2e5548ae2ba34cc43c582e2d9b22aa1eb04cae2e5ef8ceb0b719aa611475742d481eef77c214f1a5dfbc6c234916f779ae173bfb500005f0ea686b3caa06105d49b3c69a72c30906146b60536681e513306d447dd814dac2dd3708d24b211bf42ac638cdd380bccd30895aa9e73154e3efb6d824e07eecda3ec6b0968b07bba0527091244c870b4852ce55c52df8567eb5afab5a0ef8e3c94cd7dadfa675babcf35065c1ddd09dce094bb616ae1dd8b8e1db488c8344b979465a1bec9598520e6958d995b3f526866739df11b4c29d45018d9fea2e6c9ff12bac593b37ce7682ffcaf49a88ab8d5076374875ad0720994803fd955eceffd33986a1b1058b78d9b0bb31ea83009f7f94e59a7c689c3b569b1042d1a60a16d01059f4a4d0eb733ed80a9b8dd02a105b072f647f695287e6a9722e3270b2b30705342d1e3319d7fa07b42543e1c8df3c500d1acc6dd75aaea92dc17ed2c971d23e0a5e282e9ea4816f482b29b61f50c5791ecb6be0a0b235f746e332fe21f7ac693c2011c326f2acf805236ea0d24d1390aa4af695dbca948fbf832ec0a913773908eac0213797b9a3e22876a9d2bc7230fc4933a4e6488531e0ca78a9b053b095b5344fd4bdbb6295d54e44447d832e298c13509471a12d9a154efb09817719bcc2245dbcbf17c061610d657c1123ac54c20585a0861908867d9efaf440aaa91a10b146e9e712d1d6b1f22942653e867fc69974c22100455b9b0c1245ab53db0208d3d41020be02b5044ce5169ece1a1df54a933602d1a0b114004d7a120a9e93c0b511cd2f0fd53c0fbb2610c5e8d22d9469876d1fe12bdf63390b9ce209394a921072c51da1a781adcee68423a53745c7dde17b649a69d6961ad52c996858880620035c05554655f76461ac1fce809f91d443821637c4196509f7cad1d32644cbafe8c8980695407588d1a4205c6e5940c409b10b1c674b7707f94d180c562988116fb75c3011c4add62e7a67d1d7f614c86b137809bf0f18a3a4e7e69d9fd913cba81a4df7a4a2c32f87490e3e282d0bfa75c890fff860a61616e086265410ed5168d541b6b2e5dbe64c0e26c9e3676f6e259721f08f3ec8f1a536a344bcae57127dd3d3c31398a4dc400b20794da6643e78faf0b84494fc91c7298689ccd08cfa66f2eca4e3630b0cd61cb3ebf21a91caa519770fe15abdbd149000db6df355c3c0e8066e7c2779c3e9c0b028e0842f80987ef2b919ae9ca94f048e4b7a8144a4b650a45309a8ce0bf603ad859971e35e25679d3242643526a315fe4bcf4330b70b90d899243f84f7ab8842ee11bf1bb3f7b25447ee3ac840fec2f3b7eeaeffcaa488964d63e9010e954106c0e281baef641c71773844e5c246983426404bd0376083b200f78fe5f088d592548931470262eaf6ea69f86154d70ac848fde662018d2ab1575b5c9e9e41d7c5a2572d6b55bb06232e1dcc2ba369e2b1469831422b5c2fab8e3ecf0bba14dd26d44cf37cbd30a5992beac0563aaaab7d8e14945dec59656051a76d0e4c49b9f99a07811abf70549965354cc008adf691ab374c7888466234aa09e7948347cc0ebd6129dac70341441ed953eb1d882222f40f27181a6c9c89a4574dfab5cba8c2710aaf9c2914d0fb1e2ec0cdcc3879b508126b22c47745d0e844c3df3f82953b26437409439ef9eb7d4c87a26fa439ef0278532bcb7a01824683675af8cb0cf202d4b248d9f675e0fa152acf2f201070eeaaea0c144d15f32208acd1d942f383be1556da02842c46ebe0969f8e119a80e8a6bc58f2f3993e26d4cb95cb33015954ed85d8c6b2ab354c356b8bafad2c7c028f687f3f7798552f7cd6b7e94d37878b5bcf0769efefd916c150cbc1e372c2470c50f3b33a9cd5a0d768ccb33befdc2a350c559d3a824225649a348c2d7f29ca3e28fd7e4c0862d46516c08c4a34f824606615d5fe999100e97e0bc352db0dfd39b67ee064208aad134bd549355abb128d644a3cd5a853f61d54eed018e58bc590371b601a58c248aea682b05dac7c295e3cebf9eefa4a383f0e51dfd09532b75c4e3b140424937403b73340a132b43e77c42e59158be8c1be1fd2bf26587bf8c6a1f954560b793bf60ab2adf42af8412d96dfba0d52c8230645b41112b65cd26d7427444102a0d954f0dae927c0a1cc90b19ed3871c283c9a8aa7007b4cdb1b9efb3ba2934510988a1b686a9e793cac8ae37d27753d379cbcd8ae78c69a2080dcacb2aeb50bb2d03c092169874ca32b91384b6868060644f0b2056419fdd4721ca844a321a27ef5640d9f91164f6435c340c74f9542cc2ade019e2f691a7eda229b440a8718628ca9fcc99153688b88eed7813b41d6ff04c0d0e4770283545661324237c547a37c1112ca35df942a241289b0f224ecd349f872e2bc7eb2a25d9c14acda31d6bc84799c37c80cedf88466f23170e6d979204c6c0634b625c97a6d6147c562c5d9ab2d8ed88920047b787f1733b87645aea2bd2228921eff0251c3902a6aa6c3e327f7d4e9c87b2ba82f3c331669fe9944425c4f58a16e3f09f61e820b97c0940d98630116001fb8c5decf8db90d833ae6394182f12a4bd7abfcad137d731b1020234decc75253f2f28685badebb66c7f32633c2085c81d31d8863c20db7322197b1d33137ec477a7e26476fbea796531d2b88ded2ef5a36311bcfe5602681a8d3c7afce6cf8869496b86deb94d5f3f8845423cea032cd8c69ec35238a857d6858cba960915bb2048aca99572231afa42ab319cac796a348d36cb33dff1dd5ab7ab4ba1e8ef6715ec092fcb906d6e97727080b0fdd202c99942782258bc6d2ef6a7068740d1d1689c3f730446aa207a1c7f775e29deaa4f3e4493461cbc32a00d1ce196a32a59c3c00ec13ef2d4fb6d53972e701c117da899a37e671d8559ab5a2dbcd49bfc4612d04a918315c5544b8a08beffd4f3e42dfffc8c3c86a21157dad2bcee478038e1a46caf81f0b4a900e7e6aab10d935ad9f194b3bff9095c8214961a302d6b9168b2eb35fcd0f9ec643d652b54893a7af8ad200b2b0aa9396b813d764324d96804e6ce5627f998b3c3987bf3167603db8ca1ca8087f92ac87b09aca0384e0b39b539883b4927bc0e284a341fb8e96998400dbfa1ab69d89ce9e86567f384678d613f70e0afe19b56e7da424c9a68a39d0af04e2aead1c671b173d860d5e1e54eca6a6cd6ea71fc4742ad88d154f32bd85338aa58ffa5b2900cc82ea149e31833a00cca5a0cff25db8d44410817719762d906e56c8f8081d5adfe1946696184ddd10f4dbbb3b4572247095cd1e45b976e790cb54eb007bd92ca5d48cdc23a7d817c3567b4d29010b43d601549ab5480f5d83331bd14183df35f43559fa343701a7c9bc918385d2d7199d150471ccf5cac19792e4259a1627ce11f9aad16eed612e518bfba8f436cc54372138be6a884a1b5a428f1856e5598433634ccd9e82d7c30c6b13c64a091d9b5384302e99b9bf67ef85b057c466b101121d0df6c8b17ea40e3f34651c80e5aac13cce67aaf91b9418ae73299e8bf894a74365dc889b91bd1415df1c39d564ca41f4e1cf5d516a960fd6b6a7b2d7762219d71ecd2145e302b7d816824836fe2b0f5ad81932d9a392439871d8c7c331ff2e38d7d8c8ef3315b9473c4c113b79f67d10dde52deca065ac06c3e44a73a6c5cf69d50d9b2f3ec34ffecf98d55a383886406f1313085de6877d5bf44ef5433113bdae47be66f4989a671bd57bac1373f36b7413bcfca6566972d692b463bebcbc625c61352de89379cb42105d9ad24869bc408a91e1eaee273819a7b1238a650f94252bc055b2e7bc22baccbae26bbbb906cf35b1e65dc9df6abf7473945ce2bfcf077c1ee38b4865293753083bcbb8e3e4cb7a8c87aabfaaa49932639eb434f9dccf3c97195c65198856e3eeb4cd0ab5686e803373da8db004874a4340da4ec879bcb3db9330277898c89a88b039351273f1e5ee5c062e27865e185a2f2374b5840163f132e08e907a599e53fef1ca6620d74257a802a04f16d73bbcdc75a891d9a9cf12c4679e174c0873b075c025688f22b5235a770b546c1381032ec1a218d873049aa8115d78f661c0bbf00ffed6bd4e6e651007a885924b53e8902b17826800fe783cf606ccf8e9c4b2731cc80fca3f5877696326d1729d9bbeb7fbae600b3c3b9b7461a640b454d53d5c565ecf154bba3a77fc73be1a21f25e4038dd5bf2133afd824d5318a94d92e4a9c22f90a7106f0759774c8b4853cb06c1eeb8da709b876bcaa4e2467b762055b1ec202b9253c8341106c5a7141122367de8875bb3123af6af5e6aa1a06ac4f2a3a19e33b1b34b60acd0137f985e36f53f279a30e96a7465bf5b270f9284d48f7f1a2f2aec05b6854c123d17a9f9de223f9f523589933fba5fec07cc455e825e15e8f4c97197c9bf9b1e98a319633df5d324ba33a0be7073c833406d38e2740f9c88914825c276c9f3e3c96907a8c6e651f9222758b7a6082f3dcef350a403584d5d3371b5535ba7cc4a2166259e702adf6bf0943197a9869b76a8878605fd3403392537f36dbbf86fff93e856af1ba8e40becedf7edd6cef672e06d4f34143efc9204f01a9e224c442348b84db2f968c42366e7716a7b43e7f2a109892afbd170b562c33f3fb4deb5f6893e14b651ef183442a3d7761a3067e1886250fc227cf42d935c51d79ac57294fa2637f553e8437dccea6e7e2796c01ac78d74508184cb710916fc78260714b07ea1433cff66d620b874e01f1ff8b90e078d2dd7aadc63e327a6cef3902bdec1557262a8c3fa755af297982948c3add24f332ea34bfc01a0307f397df87e47eff436240e4f788c8020d4eb157f4ca72d4e96cb642822bf56df8dbb162516a92289639e25d3b8b0460d1985ffdf223d9961ee17603619275282c785565dd83fbcbae0229bf30b6797c47572dcdc0139ba88a48e47ace90a40b84d7f94ee79555f181d5a8a4a3abbc85323233a18fff1bab715275b2ccfeededa58bd933a1d791c2220e84d53772fd08ec24dbf0166f883d1396f45815d224be4c46223cff95a6c6dd250657757c78d02c59162a5bf000185793287de8d935f2348089014d181c598c99c02950f29060126dc0aad14378e1e09cc5c08442b1c53064781cfad30f49014874cb6a19d33e8e337bf9f0aaf0c397a56fab4bd59eec5ba2c5c38cfa0ec9b92441bc41662cfa4328c7da26f29945fea63893b88538cb30cea382676438ccc9a76659c8b05fe08a2b2c56f646a49cd84fbedb55babe4c833054da6f50c6e64237e8cbe454d5849e82d7d090ebc40bf3d7879825cead77f041c5d3af946e347ee46227d62e7eae97a4e4afaee442f45c42eef72fbb0bdb678f5b1149807e981bdfd2e467aef8d039bd414db27a9c0b4a38b839b5f69c192837a8d5935b20f7664f5cc9ad4fa3e0fb645b93622a6839145e18d0d8a1c3448a5b2b3575c8c853bd4ab366c213a17c79334290e0e55fa93e7b54e5890cabf605e9160017c37b7301500b23b1f28295196ba2e1182f4223b2e96bb560c5be381f854d6242e4e00421b30b206e6a4e70256dc15feebc65eafa1a50b9f764069156e08c9ccb3ea0c38652791233c48d2957c65d6428d940dc716b389a31ba4669d92e363c3d236e5fa7ff4ae34e4f165efb54c99f873310936a0372d92b4494d74180de60d65982d08445d693d5b710a78408aa1860645a4a334732352a5645e42ac9f74c8b7559979a356d03b3c7deea7387f76612b5a51fb35155cc9a4594b19cc9e9d528f226cebd32b97b6da6095231694e937b64611f48393d4ff2a911575f60cd23f05b0bdf4073ad20b6978bff0eed0bb4160ece62770b1ff6b7225cf6e9b96548ee78462d47fbcc7541cce1e285e01773007a19ebb21b606a5b929f38fcca09a8780bd121c692f71ab50eb7936e97c0228213ee9dd784f16f423a222be14df83ad7ba44279b33ce0e44a954cfe081632eaba3411ca7e99310e996e86267f5eb301a16308274763f251752e883a41f34bdd7cbae55b95477cec3fbbdb474389e05aac54e6a380116027813c31b30526073d937789c33f4d2c8b576d4e1d3f8bbfe0ac4991ad3d188ebc21f78ab26ac9a3d3f06ead515bf868f7239b752f27588ba8d20bc72ff1b31f7caa691c1a7a1485a4c80f458243d54339215159736ae5eb0776b2d528ba46c5c48db4551ec192aa121d312aaf258254562eb878013785a37c31c896e63b95dcd860fa21ed8868cf3e4f44817b7fc3bcf076fc021a4481d2c7975e062cc110f5aaf9c9714c2dacc4c67f7602cce0e4e18264cf8ca1c0949a9401a77e13b11534df89dacceae7f390d43563bffe59e800457e5fb283d0228dcc06e5b482307176f632c9bd6df382617605ffcca5be263a1892cc8bb1d3e49ca2b522cf497233f98b6cc0f656fb7b15018314f2fe2887d80345bf331a0e28819b3c9433c5ffb192c13f5fd5b964a7cd925468fc0e52821de95abedfc40ff19a316b8b53731d9eeb128d6e3f53be7d0d9cd0b50923e330e108771fec24c02262fde3559c2edc75761ebd76b298b5e9ccb7bfa3e13092a550185f4533954bfe3b117066be7b0078ff0543f2540c85ed1bd1a188c962dbfb9bc3a0978d74085605df9dd35bf8ee94cb13e5f85e8d6bb7eb9c4c40e9a4c886fdc99d02758367c07a802005f0e7a2d6db80b3b4694209fef15b9ca1168e2656e623bd0637e3202fbb24f29fb3969131a45eb92e4ac2046d059e5f919908bfec0b30b25bb01a0d7f724c6b505bf75c9ef4a6671e8bb3fcbaa1b42cedf2ce71f5a0c63d5e2982abc68283bbb49ac691688d22a6ae46df6f7cb4b8705089cf36db02724eb0ee04e7afc153889512aadbfe5d165f80330f5d9381f7db309b421ec7b5a87fd5e29013fe8464bf9f50004e121cfc2df3c2b41b21892030b6192ac9e752f91f2f616b049f9b178df7fd0c661c2a53a2f5e863584834612441328f8fcdc43879ed94c89418684e24cf20c05f33d4eacf8bc3664c164a10fa26fcd1523b15bbb0d2bd869a01f8abaabce30b7e95f8cf6a431748909c550761b332c5db8a67864aa7b287176de700edaed308e009b48e250123e02cb46044dc16c413a7912c8d237b7318fcd16def4f69707e5cf614ec1029ff0df5c47582e9a85cc2dc325cab0ed6702b1d809afaeede79db6c8e78de7ef269c359a40bc6fb6c30429fd01100590af9aaad3303decf67d27718034eac238ad780c9186b4ee3a74395355f5e306519d7b920cdcb602881edf750ce776baa9cc394e0f3e979511a41150d2aacaaecfbd6fc44203fabd3057fb05e5bc2bd0427ad08bb39eb20f80e4c5493816e5d589a7d7b40fbde1fde49c060f5c9c1e50c27808bac2b6858fc521de32a26e397a73e8edc170dde9cec534589daa0948b98ba868e931938989c41c8ac077bf0ce30a522360631fa576ee8f62e7e81c2d04aa23e51339b7b6f52ff0c704e2ee5885dd8f67fd5b2ec35a30006c1c0162fafbffec7eb114f37321a4da7cbe67ac008b293dc76d48a6bcdbe8711072b8f964ab483e5796fc406828bbc2f7cbe976a8fb41b88a1f356a5de263759060e1e055b910b6cb894fbb131d518aba7d22d522c9af5f228e1e2c8d3ffbc8822b9352bd2feb009826abf785ec0c8568497dae3c80e55d9bf79bc9ff839d2181e36f463eabd149b69ec99e8b6f55dcc7c49c3c3400c8645b9bc8bf247074d5241c8d024f9658b005dabf27446c87d08e212be74ccb13f6959d9fcb50a9d9e100f38639f466e40f3458dc2b4e56948db38e0063ead8359debbb2c97d53b6f5d6cb703b202bbed410b328d662fafb1946f5a12f8ffe53f11d7214c8ec47500f030985ff7d59c98fb6e4d99b4eae448999295e799f5fc3e5b3c2915c90da5129d9ac4a87ec547124af1230c8b02af6e17b2a0176c540b991605810d80a5913e01992cac93d90c1a7300fac1031781ae818e28cc3f7587c0f07872cfb4d23a0fda8ec6e0691042488e70e030843980d759044767d1a7339a4897a9c6f5c2960e74c31c4bc36f4f7f5045ba1a2e680c8327a27937dd1a57667cd01c3ec5920bc0f1c050eeed6e4645e10e7758b5d141a01cd32dbd01cfa42bc0cc5f34e28f1eaf789900f60f54be678c65ac631afca8ec1df50fdeabffb7f70d9409c73935a1dd863eff16c2515cd957a214cd12ea32f9b82c237d08952b119b0f1714681844f204fd1c57df133701df8322caab034bf80a84b16bec6138ad9619ddd2ceaf402e558a9343886ecca9c7b1013a9cb9b97e3cda293750b5dbe7fed470a311090c7a76be5df694d2476af5e324f543af6f097c7ac55cc41de8d552ae55a1a2730184f252f51a5fd275ff8d0d7df32ccb92444d027c4dcfd0ffe6d8a42facb2f437f6ce2c89f079efd4ad16dbe371eb06c597d03cf59028c866680d257e3b2b6d67f110fbc6a75b88fe981a52b2cdb7714098c79297b2f261a760d715111971cddaf331c581e5a75742c8b401c057a22b2d3de867a933d2856c4468617c50da44a424e657ed861edca74ac62f997b8fd338e627f19930258a0035e687deb6f79a0a6b53e5fee985ef2adff2088f404564955eae475e5b0e216ef9b8d391a919b94a8c74b03528901f0ab81f10a0ed8dea75923f383cee1ad5ed7642076031ba258a4d0988d7dfc307ca73e595b83a45527e5f527b3d2fbb922130d7e9bf16e3deceb15ca86acb0727ef60f6cce12a407fd9fcbb6203d76a2ca1122f0be26f1f3b5e6459b1bddba629c1f719eee7a2b437842cbe5fbb1dffb8d52061e47a8edf143e285853cece1a715f0c28edb7d98c00e4cfca67fd51ba1a240b8da8b3f05cfd5604a1deba83ce2c7f591c46b4bf872d27879e71f972d9104fd590a80056ee54951722f50895b835bce08394f6d4557f5f8af648023d0f151e67be1d25201d873b276139dd1cf0e68fa44cc41632e28d7a06728bb9296741287933b8abef6378873428e25b635e347224408f99d06b27d729bed79c0ee410da5b276766bfe5614a87ca4b72b82a3558dd3b291bba64c3a8f0ba410b6340c9843b5ad113f2e80a0ab4f150a39055e505d5753c8277da6dc214ed4a76b100f670b84c289c3bb46cfbe211fcbc4e11b71ff0dd6899d4f6ab8726391e4d60c1f843cf16366fd39bceec6a6fb464a1e55b682b3eef7aa668286a779b6790eabb9449892a6bcff9f4b1791368a287e7c728e9927379893b69bdc725135d0cb916c7e1d78ed0e255511309e5419f8608851916fba9f1df24c3eeb464c30d7e43c069c62b21474a4242e7d903b1feaedb8cd1d256342bf5b6c5399f5298053a4c496933a43df357ff9a3c194ece840924401cd40d78856397006ca11c7682a72d1226320290cdbb15a4ff749a5897b712bd75db53ba8ded2bfa21c8f97cced17fc2bb53b8b484bcada8a977958df572e0f604680224970c174279c4a41b20a344930cf222c9849fa3412f630762f9571512d94afff3046f9d9b53378a2313f0c4709de4b1d4fc8525fb0e94d992d864f3679127a88b9c46857aa35079b983a2fe4d44c8b162cfbaf1285763e67a3a258c3d1f52f37aa5c6942c4349fbfdc5566dcc6b707af783e6db8431b719b26d1ddfbac228761c072325deec2396d00318bf2852816015c07c6ad0a0aef86de9a4ec053e1711c864c168613ad764301b537f79bfe172b9a4d3a4ee19ee5ea88a4a5eb55b180c8aacd639dc0b3b3c436f1efe87597be2529d81efbf358fe174b37e7eaca96062035f5a02a618abb7a28674273a47fb9dc8fda99856fde8629fc807802175e3c152e6da3ff28c8680168a7dd23e2b75aabfe0accf37556f8908743c7885c5abb62f1391f7a9a88fd493df71ecffddfa11b89d4e7cc05d9c54a3299fedbd5bd03fd54f608ec767002a9e2595d9624fb4afaa93a41352428eaf0827d85b9ebf35a66873a4eb91512b3725c443a29d5e8ff38645d58500cb7de3517f1c20e0642d8f3d8d3bc829aeeb6c60f72f8da2b9072352af58c03a0b33e4fe0a48d41bc8a902f8efa717933a1828e9236cb097681e4ce8593c436cc47134a6acc68a2bb839f31eea0a29012da23df359a619f100afb8e4b458e31f98a1ac08db341219fd89277aa99d90e1ccdd03269b5b80aa563be3fa8446ad3b2bfd73c1fc63d56d9ad8a40970709bbff27673d26686a55b00664455e427a2a85ce6857c7c8481c0d23e5eea0d4ca9f0039db7653660b7afedee2124b023e325546d13f4d7c20c76358cdcb18c8cf12a65efd8520819b8baa9d71fe6582bffc7707afa24fdcc6bc8bb3851660542c367f6195504335ab08060d58a47f0b851cdc91c884a400200c1e104b04dc3d2d6aff5b39f4ff4e0bfc0e67c25cc09884fb733e33293bf5a2c166f7fdb947a0ff93512d24d9f52fbac77dec9f5e9ccf053500d3033c570b5f18564a1096b20fa81cfe9377b0f0b93352d429e28fde3090e280a593414c272f12bb51b806791f5c08e29972db34884dec9e7f8045423a15f022e7f687d26cb94df64b745cd9e8d9c157b2a83ae37d6ebe662b0e6c861fc47a468cde2b2f26eef648a155ab1afb428be2cc2b6fbd1eb7b11093042d3c1384b0e546c1757ff543c4922d1ab1aeec0d802a386b2bbd8513ccbc90648fdd45e8da71ea9a7c70054629f4784dfb852c299294900b0fa6fc0b56be1f5d1cbe1011bc6d5aa6fd4a7ead309d35f954d60c81da809a0b2bd3e21c6762b535f4b682239f13b0c176c121751d61f6d4a52b19dd722b236344de8aa3a60e66f0f3c04a604ba5f5b6c66286f20e32b49022081ab7a23f9ba41d8291681f0639c9c2546c95765c569de5902ffca4bd5e5f4bff5ba0897d091010721464a742e7c5f115613b4f5d70c01552ae693c7879633729c12012ceb9d7c61624c58f7d9ead586c80fef1b25760aaf6140f2bbc4b7f2580955523ff311001e8b0159b5a02270f095c9c5aef0d65415c727b6a7973fc5785ebbd1aeeee827d680659a6a465e8ebc3d1b2d809855b5ecd8e1b45659bd940dc2e3b29f3204bff8ec8bcb96fb6fa443a1d5da52da25122e61773996d09912474a2332583dcf5893e0d99034de77e84adbd6e516bf592fe6e5e0f78c6a2c7f9fc288a9648d359a43f4e6da8ad18ffba0a2abcf1025f39eadc00e2789fcef34fccab92e55b05423e377ebab91e4f52eddd456afe532ae3841714d4f17404623b0fc06ad02735fcbf3bc0af308433dcb9bd2ffaa497844d12bec2686d65cfcbcf6004b36e74b5b2fe4838285c2f0747ea825c297fb1db32bfd53e6c100aa2e803d215a173bd24ddc1c96df29d71b924e3f4f459fa41dc7b8b8cf0d3a0eacb44500158b46bc121fd4bfa84d68c449f633de5bf63afdac6d283d1aa9d4d3011cad2671d821253e231ca7d5d74491f5a3c4d9e06253ce347e71521c7f5444f99c73a0db8347fea51ea12e27a774f7acc22d4e8bc7527500da28d4d6faabad6e02c973877b63fe7422a311da88bc39c92b04f6915e59c8db6a81ff488a535a84380b353c180e82504573531362fdc303f67c6adebfc86b87306dd3b20fbeaf4348a712adee7f5d49b63eecc90cad25d11113277b09da2919afda1f09576779bd9f26b8774b5162308de6781552ec8c1f21429d717755ad594c53c54054c209e8761d0e5d34468c31ae385ca03df17b683914c8bdba0e8b794fd61c71430cf51b725bfba1e858129ea3600505d004a8c6712a57774a951f36bf170dbb5f5b3076a0ee2803b8d83fa9a49b395b240ecf1c95abc7acfc4236d979ee0597bb756eb95d65d14d099dc494473232ad9f4e53784c918f6d2a48a506b3c14ec689ff8461446273618459bde103adab1f9ac581bdc41e12ebc71463080d9b290daaf84af3a278854e74cb63b05cc46f2e709acc1d23cee5fb28a4398174fa61f7dd96946b7313bb07f1272826359e9434cbcdd54dd1f2b827bbf5179d8a1f42160f6113421795cd697394b05a79478216e516c3b36abc9cb6885eef7d90aa711d89a67d2e9090417bc66e68d98d11e4a67a3f34f71b9014ef64ca671656b61e65cbeb147d3962f236ffc3409cc3adb7039999db034c71935af7f1013b8471267717f27c5120919355817388efb8fe8e5d304e041c96384e81660cbd3cf8e16b6a205b1bfeb3a8c413de9bdd83884680b5c3c24eb1139f5c930cfd3732bb846607a5b69df385506ef03f8197a668b894c6f6e4395c417d1a972dc1479f139c0f2b4b78e4288eca556d734f84ac2108e0f9b212a09bcdee6355a5c960f05f742705d1fc6b381902fb97239e7023b4d949ae696c49ddee6ff2cb5725d3943cdf1d79ae4d9eff9fe4c29710c3d1dc23062e4b4c6e0e63557e3e8d4cfe76aa6aacae5ebd45fafe1375750a229425ec88fabfe468ba6bee47bed2700c988ac27ddd7da34204a283b231f16fc8601054cc9f953e0daec5ba9ce893786325181f20f02a85200407452ddbe27f8c3d74acc1237f36945ee410a92e76de73beddc06846ca73f8311c3f462a315e30137142f7476dfa5b6836b278e1bc95c58bbbc612807ae7c3edfd44a515960ea099e5aa2710ef30ad6aeb48b62fa1df97a5ac079d4432392aa167b1c1685448fad27ff16b20209a911e78d90e7fcb4cc1a2f1b7becdf8911780bca8e1b59212f9a6580fbccbceefdc600775957fede742a29b4ca99c22dc0be790364cecaeafb21e66717d552748f6253b5dedbc1ae3127aaa851751291713358ed4ee646e83f0327626b3e1124006c0f6711789c1bf2a3d448809dc49633bd9d9c252a0dbfebe3d0cef75b5756cf11c647c03de86e7f1cc7b04daf2f899df03f33fe33ae53c3b9c82c9fb642017f468cf5091d26f8448e0cf17b1ab64043ea12e798bebcb48e4fcd8da70f11e1d075f93e6b3c54fa71488e8a1bf4a1f77c6f9d0c46b878a6a55f0eed14beab6d0405437917bccf514356c921735d08876074a4c3df4c051b55fbbe6d07dc5eb5385893dcb9374090b2203850eda10859a3f3b67be2f0d98a08b3252ce1bb1aed21864ba880fad7102859cd26675a4693e93b5d975923bbff438692f87629261aeb6cf3c41fbea485b34da21fa063e456d316c2e6089fa0b85aa0ad3c5be3b112a1dbe022b5f84da5753e95b056d31cf839e90078da6145f7532b2dd7ecc52f34e607cbd8b17971da428cf1ea4f340616448f3138a8e1ebec6bdb30b2fc39c05f84e4334bda3a8b781523f8bdc0d03efd2a938b0ecae4de2657ff86077dc1eae70b9e6a92795733520ae64d9fd8ffa77c779734a39894982da616a6801c7d1ec08d11ad4634060f43f36d460beb1f4a0908d9994b4fa0acb046c0db0b87b35098cdd5fe8ba6579280766e530e77ff457a0fe2ba7d600a76e8af33ca5c26deaf80d3f0fcc0b772896625f6aa16e55362245e91184be17a872dd328d29a6cc07d3c58c30f29769e39ed697ce10c42ca6c22c2b7a9603ca7e86c9eac2e13d6a965c2f69da5397100844e5440e4a1f78816710317f0df11153d3269874fc37226b0e9d2fbc7b9e0fc8926f5aafd6ec16092fe6b4912bebff5ddb788ba535b19d213a799011a749bfbec0b903fbcea4c029b249ff87687f924a68601f0cd8678595c43dd6cf73bd888c2bce361624b71d4c9a43819064ee17a871facf26cc111afa818a50be1aa198d4b8e8e857966173d1e0af3264d51198beddc1977a142b2a72c3960ae9c15a8a8bec9516cb95212b2d85e4435c57d12fa74db96ac83e9a4302b6c97ccdf8145dc0d92c993fa0a98447dccd1c3a8a5c7e48c3da1e30b51f9008785d367270d51e23e89b5135f086837d9bd5abf0d1b57deb4045be551c00c2fa1c0323bed5b38156c8991e32c5dd182c1d7a016be528f5c0cdcb4083bfd2632e9c5f15920181a5b2908bf39a8870a11a3294bad33daed86fb0a4c2d880ce368d5313c062757d54cc075aad1cfacc23968ab128c992c4213a1ccb7a7c90a389b54f62d867e7f64c1552a57aa6eb1eeb94ad654c16888193d0a7b2036dd871bb3209e43b5ccddc421629ba37b901f9d73a30ef44db8ee6dcee7ac46df82734b10246ad5cc63cbbb966ef90b92305da58b5da518361b52b94324741370c6de46235d501871a350e5c37f0435cb7ee765e164a0cfea8453a9387041207ad578069b804911f230c63ca1f1f890a189d1e0b3a62d4204fc19751a2442b5c4e016c331f29689e3831e8897f8bdeefb5f45cca0bec38dc357d056fefac4b0c26e00bee268b81730cc662b31623a1b8f36fc060c3c4447c7262c53210ba61e535636f782b9ed3858ebe96b17e8737176715ccee635c6af3b28b4cf956cc85942ffdb96a3d97d8288e6ea30ba21d907d3852b7757068c0f0aab03bdf9d104a138f90ac1535e27745a5bbb5b3b54d0b8e117e3de2a0e3487563fd6b9db307a78984f794b9e321360c08ab2b4c50f82e13adc600b6742ea88f2804856ba6c75a60d672aef5e5ce8499c86b92b8e4650896920ca00861a8c7046cac670e1551da8e1e8b748ed2a849d1433ffc2624c6a5b02459655c30c01d03b8e7a80b57b300d5167a6d1056d45b9e35eec8d1fac9225f201c98d53b058b83e8bbf407f949cca1c8e8f8599e0d1b484157d0d329504f7c3ab63811080408ce46bffdf42ffc3c0358e6244ba6baaa4dc87c2f9c35bbebd4f0cd7174954d3a9b73c5c866a5bf79629f7dee0375a2e958f7d38757c583317ac272fde7bc2f0579e64d375f75b9cd91ef5ec896672792fdac38995c5ec3994b32e2c2087fb2005d463bec64cdb7f4a6903c79b120bd282d0c590e1e4ce715eac8fe57df3e8d2c789b67371754980253e3c848b70876a15c57b5f1b6cab3f3db275c6282b13922cd06b8d59702885d6c1338d85b7b74c20d11badb9dea0c109bb5a901193a1cde47b4f2cd5c3e9aed61c13eaebc95f81a917f8b50296e4a8b32bca65f91416799e3eb0d2b9b397fcb043bd025bed6d753bc0d5893f1b6555f89cd99fc8425a78eb4d48fc0315fb2b41a93b19d3ad8cdbaa0f5b43a3c8091561f129c03f5c6d4b536856370821db1f89ac08e409e2a0709eda1cd35d5f1a1e242e7c4b4881493ae8689cd6ed81ad041e6dc6af0190a95343e12ca2f2a69d6c041cf50dcfbe788a7dbdcd0b4a08d843389510aa53952bafe854900ab31380507e14c503035a810435fea16512d677796ee56795806c42650806ce66ac3bdbecb51df88dad7c615147890fcb09d08f1d26d7803b7ef046d8b43315ce462e1bdebd5f2c42aef31d0bbd8b96a53392e143ed537f5dcaa21a140e2589e60b6fe876277b22869bab13d7a96bfbe0c955049f5392a9d28108e3285d14de424c95f1da8ba450575309c08922bca613503422376f14c0b601cec093dbd28493d6f029cbdd5c1a49a02b966a207c22c6d422596830d96a67cb5547ee0544d9ee7dd2cc1ee427d30d8c4e283a8ff0e381bced3da02060787b78967e73d2c0207cc60085f68134224db5f0642e0865432e91f5a1567d144ae82e735d68e18b7fdc8fefa2d03cad2a22f77cffa40c2a33dca87864d0a03f37cc9e5f43f25e662971619d5039ab633a7e112ce5dbf8bce5108dfa8989c0854b75d0a68c47524c88cc1eb9eb2f35a300fb2d49af5d14c2d9d9d5ade1b0512b444a85e5956bd8669fd021e5278243f9bd5e7fcb0d3371d2e92fdba9fd2e2838c90c104da75ca718e8a6b44c3b91da6566d15095d4aec5b1e30ae36a56b04b94999ac5410bd8bab616de7a877fc5c9b69cc713489ad546adaf984f7d710b44cdf8dd44661f345a883f19dccdc7eba0d6b4b3eaf2a812537851e0c7919c2a0fffed148243cdeef5410afc3c9d87bbe5d146109596d702bf83507987ced08b7a444c88d55c441c1fc0a2f9f0faef2961626c8257117cbf2748ecc62337731c62cb978c55bda49e5079afb8c1e0af66ef3114dfcdff4cc997e4026a670187bac897c7d1ef75cc4b336e0341eadb6c8aabfc9d54bf8a574ad2256d6b462d7adda446789a86c3065f8b5170b476505dc1d7a93b21fe44ed4319c8509f0807cf3cf37878cefe68e6fc06b78c7782a192fae6fcd9add10dcd69ce90d8d460316ce56dffca0a2f9e40ce39c3e22cf2182fc086e170f1aa20434b118493d9a55180f40d37800ed891492b5618b805a74d2a7cc48897cf5361b6c57a5a0714349405e83d5b067aae0c3d4e6771f969118e4b3f93ef194f2c9d8aa217bdc01e75762fa391482199797b2dc443bb2cc5213c0a5df50b0a18af79758863cca2dc3c6879fd693d7a1ac6c96914b1e44010daa9c3b77efcff2e8303fd7e0e219d87f143fa36ba0be0dcdc01f2c2cb3b5a87613691a17bf3999ed78ba65d540bbd0de15488e8d923da100a3cbe055e79fe1e124abe4666e5179abe4c45df5a9a25f7a45b8ae9d06aaad2c3c60a51f471359cbd6146d99972a7e808527a2fef13f92da9d9a333b074b834989059483c55d96358e8e620c6d6fed865a81a0d472fa14b79643d50af5802ada18f794cd8018f7ead5f07279e0fa073595c8308e0732973e3b6118e8a37f04a3679d05b0424620c2c778617761f9d5c3e3b80f602103539484919557931549c53698d24ae658f5dc40994730ecba99027abebb22b2cfc9fffb436b4ca3359504`\n"
  },
  {
    "path": "pkg/services/templateBOOTy.go",
    "content": "package services\n\nimport (\n\t\"encoding/json\"\n\t\"net\"\n\n\t\"github.com/plunder-app/BOOTy/pkg/plunderclient/types\"\n\t\"github.com/vishvananda/netlink\"\n)\n\n//BuildBOOTYconfig - Creates a new presseed configuration using the passed data\nfunc (config *HostConfig) BuildBOOTYconfig() string {\n\ta := types.BootyConfig{}\n\n\t// set the required action\n\ta.Action = config.BOOTYAction\n\n\t// Default to false if not in configuration\n\tif config.Compressed == nil {\n\t\ta.Compressed = false\n\t} else {\n\t\ta.Compressed = *config.Compressed\n\t}\n\n\t// Parse the strings\n\tsubnet := net.ParseIP(config.Subnet)\n\tip := net.ParseIP(config.IPAddress)\n\n\t// Change into a cidr\n\tcidr := net.IPNet{\n\t\tIP:   ip,\n\t\tMask: subnet.DefaultMask(),\n\t}\n\taddr, _ := netlink.ParseAddr(cidr.String())\n\n\t// Set configuration\n\tif addr != nil {\n\t\ta.Address = addr.String()\n\t\ta.Gateway = config.Gateway\n\t}\n\n\t// READ\n\ta.DestinationDevice = config.DestinationDevice\n\ta.SourceImage = config.SourceImage\n\t// WRITE\n\ta.DesintationAddress = config.DestinationAddress\n\ta.SourceDevice = config.SourceDevice\n\n\t// Default to false if not in configuration\n\tif config.GrowPartition == nil {\n\t\ta.GrowPartition = 0\n\t} else {\n\t\ta.GrowPartition = *config.GrowPartition\n\t}\n\ta.LVMRootName = config.LVMRootName\n\n\t// Default to false if not in configuration\n\tif config.ShellOnFail == nil {\n\t\ta.DropToShell = false\n\t} else {\n\t\ta.DropToShell = *config.ShellOnFail\n\t}\n\n\ta.DropToShell = *config.ShellOnFail\n\ta.NameServer = config.NameServer\n\n\tb, _ := json.Marshal(a)\n\treturn string(b)\n}\n"
  },
  {
    "path": "pkg/services/templateESXi.go",
    "content": "package services\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// bootcfgHead const, this is the basis for the configuration that will be modified per use-case for the boot.cfg\n// The main modifications with regards to this template is a replace-all on the module and kernel path\nconst bootcfg67u2 = `bootstate=0\ntitle=Loading Plunder ESXi installer\ntimeout=5\nprefix=http://%s/vsphere\nkernelopt=runweasel\nbuild=\nupdated=0\nkernel=b.b00`\n\n// The Modules list all of the required modules needed to deploy vSphere\nconst 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`\n\n// kickstart67u2 const, this is the template for the actual installation of ESXi\nconst kickstart67u2 = `accepteula \ninstall --firstdisk --overwritevmfs \nrootpw %s\nreboot\n# vmserialnum --esx=PUT IN YOUR LICENSE KEY\n \n#network configuration \nnetwork --bootproto=static --addvmportgroup=1 --ip=%s --netmask=%s --gateway=%s --nameserver=%s --hostname=%s\n \n# run the following command only on the firstboot\n%%firstboot --interpreter=busybox\n \n \n# enable & start remote ESXi Shell (SSH)\nvim-cmd hostsvc/enable_ssh\nvim-cmd hostsvc/start_ssh\n \n# enable & start ESXi Shell (TSM)\nvim-cmd hostsvc/enable_esx_shell\nvim-cmd hostsvc/start_esx_shell\n \n# enable High Performance\n# http://www.virtuallyghetto.com/2012/08/configuring-esxi-power-management.html\nesxcli system settings advanced set --option=/Power/CpuPolicy --string-value=\"High Performance\" \n \n# supress ESXi Shell shell warning - Thanks to Duncan (http://www.yellow-bricks.com/2011/07/21/esxi-5-suppressing-the-localremote-shell-warning/)\nesxcli system settings advanced set -o /UserVars/SuppressShellWarning -i 1\n \n#Disable ipv6\nesxcli network ip set --ipv6-enabled=0\n \n# NTP Configuration (thanks to http://www.virtuallyghetto.com)\ncat > /etc/ntp.conf << __NTP_CONFIG__\nrestrict default kod nomodify notrap noquerynopeer\nrestrict 127.0.0.1\nserver 129.6.15.28\nserver 129.6.15.29\nserver 129.6.15.30\n \n__NTP_CONFIG__\n \n/sbin/chkconfig ntpd on\n`\n\n//BuildESXiConfig - Creates a new presseed configuration using the passed data\nfunc (config *HostConfig) BuildESXiConfig() string {\n\tmodules := strings.Replace(modules67us, \"/\", \"\", -1)\n\tvSphereConfig := fmt.Sprintf(\"%s\\n%s\", fmt.Sprintf(bootcfg67u2, config.RepositoryAddress), modules)\n\treturn vSphereConfig\n}\n\n//BuildESXiKickStart - Creates a new presseed configuration using the passed data\nfunc (config *HostConfig) BuildESXiKickStart() string {\n\n\t// vSphere Kickststart\n\tvKickStart := fmt.Sprintf(kickstart67u2, config.Password, config.IPAddress, config.Subnet, config.Gateway, config.NameServer, config.ServerName)\n\n\treturn vKickStart\n}\n"
  },
  {
    "path": "pkg/services/templateKickstart.go",
    "content": "package services\n\nimport \"fmt\"\n\n// This initial template will be modifiable based upon the build requirements\nconst kickstartFile = `\ninstall\ncdrom\nlang en_US.UTF-8\nkeyboard us\nunsupported_hardware\nnetwork --bootproto=dhcp --hostname centos-7.pelmet.loc\nrootpw vagrant\nfirewall --disabled\nselinux --permissive\ntimezone Europe/Prague\nunsupported_hardware\nbootloader --location=mbr\ntext\nskipx\nzerombr\nclearpart --all --initlabel\n\n#Disk partitioning information\npart /boot --fstype ext4 --size=2048\npart swap  --asprimary   --size=8192\npart /     --fstype ext4 --size=1 --grow\n\nauth --enableshadow --passalgo=sha512 --kickstart\nfirstboot --disabled\neula --agreed\nservices --enabled=NetworkManager,sshd\nreboot\nuser --name=vagrant --plaintext --password vagrant --groups=vagrant,wheel\n\nrepo --name=base --baseurl=http://mirror.centos.org/centos/7.3.1611/os/x86_64/\nrepo --name=epel-release --baseurl=http://anorien.csc.warwick.ac.uk/mirrors/epel/7/x86_64/\nrepo --name=elrepo-kernel --baseurl=http://elrepo.org/linux/kernel/el7/x86_64/\nrepo --name=elrepo-release --baseurl=http://elrepo.org/linux/elrepo/el7/x86_64/\nrepo --name=elrepo-extras --baseurl=http://elrepo.org/linux/extras/el7/x86_64/\n\n%packages --ignoremissing --excludedocs\n@Base\n@Core\n@Development Tools\nkernel-ml\nkernel-ml-devel\nkernel-ml-tools\nkernel-ml-tools-libs\nkernel-ml-headers\nopenssh-clients\nexpect\nmake\nperl\npatch\ndkms\ngcc\nbzip2\nsudo\nopenssl-devel\nreadline-devel\nzlib-devel\nnet-tools\nvim\nwget\ncurl\nrsync\nepel-release\nansible\nlibselinux-python\n-abrt-libs\n-abrt-tui\n-abrt-cli\n-abrt\n-abrt-addon-python\n-abrt-addon-ccpp\n-abrt-addon-kerneloops\n-kernel\n-kernel-devel\n-kernel-tools-libs\n-kernel-tools\n-kernel-headers\n-aic94xx-firmware\n-atmel-firmware\n-b43-openfwwf\n-bfa-firmware\n-ipw2100-firmware\n-ipw2200-firmware\n-ivtv-firmware\n-iwl100-firmware\n-iwl105-firmware\n-iwl135-firmware\n-iwl1000-firmware\n-iwl2000-firmware\n-iwl2030-firmware\n-iwl3160-firmware\n-iwl3945-firmware\n-iwl4965-firmware\n-iwl5000-firmware\n-iwl5150-firmware\n-iwl6000-firmware\n-iwl6000g2a-firmware\n-iwl6000g2b-firmware\n-iwl6050-firmware\n-iwl7260-firmware\n-libertas-usb8388-firmware\n-libertas-sd8686-firmware\n-libertas-sd8787-firmware\n-ql2100-firmware\n-ql2200-firmware\n-ql23xx-firmware\n-ql2400-firmware\n-ql2500-firmware\n-rt61pci-firmware\n-rt73usb-firmware\n-xorg-x11-drv-ati-firmware\n-zd1211-firmware\n-iprutils\n-fprintd-pam\n-intltool\n\n# unnecessary firmware\n-aic94xx-firmware\n-atmel-firmware\n-b43-openfwwf\n-bfa-firmware\n-ipw2100-firmware\n-ipw2200-firmware\n-ivtv-firmware\n-iwl100-firmware\n-iwl1000-firmware\n-iwl3945-firmware\n-iwl4965-firmware\n-iwl5000-firmware\n-iwl5150-firmware\n-iwl6000-firmware\n-iwl6000g2a-firmware\n-iwl6050-firmware\n-libertas-usb8388-firmware\n-ql2100-firmware\n-ql2200-firmware\n-ql23xx-firmware\n-ql2400-firmware\n-ql2500-firmware\n-rt61pci-firmware\n-rt73usb-firmware\n-xorg-x11-drv-ati-firmware\n-zd1211-firmware\n%end\n\n%post\nyum update -y\nyum install -y sudo\necho \"vagrant        ALL=(ALL)       NOPASSWD: ALL\" >> /etc/sudoers.d/vagrant\nsed -i \"s/^.*requiretty/#Defaults requiretty/\" /etc/sudoers\n/bin/echo 'UseDNS no' >> /etc/ssh/sshd_config\nyum clean all\n\n/bin/mkdir /home/vagrant/.ssh\n/bin/chmod 700 /home/vagrant/.ssh\n/bin/echo -e 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key' > /home/vagrant/.ssh/authorized_keys\n/bin/chown -R vagrant:vagrant /home/vagrant/.ssh\n/bin/chmod 0400 /home/vagrant/.ssh/*\n\n%end\n`\n\n// BuildKickStartConfig - Creates a new presseed configuration using the passed data\nfunc (config *HostConfig) BuildKickStartConfig() string {\n\treturn fmt.Sprintf(\"%s%s%s%s%s%s\", preseed, preseedDisk, preseedNet, preseedPkg, preseedUsers, preseedCmd)\n}\n"
  },
  {
    "path": "pkg/services/templatePreseed.go",
    "content": "package services\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Preseed const, this is the basis for the configuration that will be modified per use-case\nconst preseedHead = `\n# Force debconf priority to critical.\ndebconf debconf/priority select critical\n# Override default frontend to Noninteractive\ndebconf debconf/frontend select Noninteractive\n\n# Preseeding only locale sets language, country and locale.\nd-i debian-installer/locale string en_US\n\n# Disable automatic (interactive) keymap detection.\nd-i console-setup/ask_detect boolean false\nd-i keyboard-configuration/layoutcode string us\n\n### Clock and time zone setup\nd-i clock-setup/utc boolean true\nd-i time/zone string Europe/GMT\nd-i clock-setup/ntp boolean true\nd-i clock-setup/ntp-server string 1.pl.pool.ntp.org\n\n### Preseed Early\nd-i preseed/early_command string kill-all-dhcp; netcfg\n`\n\nconst preseedNet = `\n### Network configuration\nd-i netcfg/wireless_wep string\n\n# Set network interface or 'auto'\nd-i netcfg/choose_interface select %s\n\n# Any hostname and domain names assigned from dhcp take precedence over\nd-i netcfg/get_gateway string %s\nd-i netcfg/get_ipaddress string %s\nd-i netcfg/get_nameservers string %s\nd-i netcfg/get_netmask string %s\nd-i netcfg/use_dhcp string\nd-i netcfg/disable_dhcp boolean true\n\nd-i netcfg/get_hostname string ubuntu\nd-i netcfg/get_domain string internal\n\nd-i netcfg/hostname string %s`\n\nconst preseedLVMDisk = `\nd-i partman-auto/method string lvm\n\n# If one of the disks that are going to be automatically partitioned\n# contains an old LVM configuration, the user will normally receive a\n# warning. This can be preseeded away...\nd-i partman-lvm/device_remove_lvm boolean true\n\n# The same applies to pre-existing software RAID array:\nd-i partman-md/device_remove_md boolean true\n\n# And the same goes for the confirmation to write the lvm partitions.\nd-i partman-lvm/confirm boolean true\n\n# This makes partman automatically partition without confirmation, provided\n# that you told it what to do using one of the methods above.\nd-i partman-partitioning/confirm_write_new_label boolean true\nd-i partman/choose_partition select finish\nd-i partman/confirm boolean true\nd-i partman/confirm_nooverwrite boolean true\n\n\n# This makes partman automatically partition without confirmation.\nd-i partman-md/confirm boolean true\n# LVM confifirmation\nd-i partman-lvm/confirm boolean true\nd-i partman-lvm/confirm_nooverwrite boolean true\nd-i partman-partitioning/confirm_write_new_label boolean true\nd-i partman/choose_partition select finish\nd-i partman/confirm boolean true\nd-i partman/confirm_nooverwrite boolean true\nd-i partman-basicfilesystems/no_swap boolean false\n\n### Finishing up the installation\nd-i finish-install/reboot_in_progress note\nd-i cdrom-detect/eject boolean true\n\n### Preseeding other packages\npopularity-contest popularity-contest/participate boolean false\n`\n\nconst preseedLVMDiskRecipe = `\nd-i partman-auto/choose_recipe select parlayfs\nd-i partman-auto/expert_recipe string                         \\\nparlayfs ::                                                   \\\n\t\t269 269 269 ext4 $primary{ } $bootable{ }             \\\n\t\t$defaultignore{ }                                     \\\n\t\t$lvmignore{ }                                         \\\n        mountpoint{ /boot }                                   \\\n        method{ format }                                      \\\n        format{ }                                             \\\n\t\tuse_filesystem{ }                                     \\\n        filesystem{ ext4 }                                    \\\n        .                                                     \\\n        900 10000 -1 ext4 $lvmok{ }                           \\\n        mountpoint{ / }                                       \\\n        lv_name{ lv_root }                                    \\\n        in_vg { vg-root }                                     \\\n        method{ format }                                      \\\n        format{ }                                             \\\n        use_filesystem{ }                                     \\\n        filesystem{ ext4 }                                    \\\n        .\n`\n\nconst preseedLVMDiskRecipe2 = `\nd-i partman-auto/choose_recipe select parlayfs\nd-i partman-auto/expert_recipe string                         \\\nparlayfs ::                                                   \\\n269 269 269 ext4 $primary{ } $bootable{ } $defaultignore{ } $lvmignore{ } mountpoint{ /boot } method{ format } format{ } use_filesystem{ } filesystem{ ext4 } . \\\n900 10000 -1 ext4 $lvmok{ } mountpoint{ / } lv_name{ root } in_vg { ubuntu-vg } method{ format } format{ } use_filesystem{ } filesystem{ ext4 } .\n`\n\nconst preseedLVMDiskDisableSwap = `\n# will result in a zero swapfile being created\nd-i partman-swapfile/percentage string 0\nd-i partman-swapfile/size string 0\n`\nconst preseedDiskAtomic = `\nd-i partman-auto/choose_recipe select atomic\n`\n\nconst preseedDisk = `\n### Partitions\nd-i partman/mount_style select label\n\n### Boot loader installation\nd-i grub-installer/only_debian boolean true\nd-i grub-installer/with_other_os boolean true\n\n### Finishing up the installation\nd-i finish-install/reboot_in_progress note\nd-i cdrom-detect/eject boolean true\n\n### Preseeding other packages\npopularity-contest popularity-contest/participate boolean false\n\n### GRUB\ngrub-pc grub-pc/hidden_timeout  boolean true\ngrub-pc grub-pc/timeout string  0\nd-i grub-installer/bootdev string /dev/sda\n\n### Regular, primary partitions\nd-i partman-auto/disk string /dev/sda\n\n#d-i partman/alignment string cylinder\nd-i partman/confirm_write_new_label boolean true\n\nd-i partman-basicfilesystems/choose_label string gpt\nd-i partman-basicfilesystems/default_label string gpt\n\nd-i partman-partitioning/choose_label string gpt\nd-i partman-partitioning/default_label string gpt\nd-i partman/choose_label string gpt\nd-i partman/default_label string gpt\n#d-i partman-partitioning/confirm_write_new_label boolean true\nd-i partman-basicfilesystems/no_swap boolean false\nd-i partman/choose_partition select finish\nd-i partman/confirm boolean true\nd-i partman/confirm_nooverwrite boolean true\n\nd-i partman-auto/method string regular\n\nd-i partman-auto/choose_recipe select parlayfs\nd-i partman-auto/expert_recipe string         \\\n   parlayfs ::                      \\\n      1 1 1 free                              \\\n         $bios_boot{ }                        \\\n         method{ biosgrub } .                 \\\n      200 200 200 fat32                       \\\n         $primary{ }                          \\\n         method{ efi } format{ } .            \\\n      512 512 512 ext3                        \\\n         $primary{ } $bootable{ }             \\\n         method{ format } format{ }           \\\n         use_filesystem{ } filesystem{ ext3 } \\\n         mountpoint{ /boot } .                \\\n      1000 20000 -1 ext4                      \\\n         $primary{ }                          \\\n         method{ format } format{ }           \\\n         use_filesystem{ } filesystem{ ext4 } \\\n         mountpoint{ / } .                    \\\n`\n\nconst swap = `      65536 65536 65536 linux-swap            \\\n$primary{ }                          \\\nmethod{ swap } format{ } .`\n\nconst noswap = `\npartman-basicfilesystems partman-basicfilesystems/no_swap boolean false\n`\n\nconst preseedUsers = `\n### Account setup\nd-i passwd/root-login boolean false\nd-i passwd/make-user boolean true\nd-i passwd/user-fullname string %s\nd-i passwd/username string %s\n\nd-i passwd/user-password password %s\nd-i passwd/user-password-again password %s\nd-i user-setup/allow-password-weak boolean true\nd-i user-setup/encrypt-home boolean false\n`\n\nconst preseedPkg = `\n### Apt setup\nd-i apt-setup/restricted boolean true\nd-i apt-setup/universe boolean false\ndi- apt-setup/security_host %s\nd-i apt-setup/security_path string %s\nd-i mirror/http/hostname string %s\nd-i mirror/http/directory string %s\nd-i mirror/country string manual\nd-i mirror/http/proxy string\n\n### Base system installation\nd-i base-installer/install-recommends boolean false\n\n### Package selection\ntasksel tasksel/first multiselect\ntasksel/skip-tasks multiselect server\nd-i pkgsel/ubuntu-standard boolean false\n\n# Allowed values: none, safe-upgrade, full-upgrade\nd-i pkgsel/upgrade select none\nd-i pkgsel/ignore-incomplete-language-support boolean true\nd-i pkgsel/include string %s\n\n# Language pack selection\nd-i pkgsel/install-language-support boolean false\nd-i pkgsel/language-pack-patterns string\nd-i pkgsel/language-packs multiselect\n# or ...\n#d-i pkgsel/language-packs multiselect en, pl\n#d-i debian-installer/allow_unauthenticated boolean true\n\n# Policy for applying updates. May be \"none\" (no automatic updates),\n# \"unattended-upgrades\" (install security updates automatically), or\n# \"landscape\" (manage system with Landscape).\nd-i pkgsel/update-policy select unattended-upgrades\nd-i pkgsel/updatedb boolean false\n`\n\nconst preseedCmd = `\nd-i preseed/late_command string \\\n    in-target sed -i 's/^%%sudo.*$/%%sudo ALL=(ALL:ALL) NOPASSWD: ALL/g' /etc/sudoers; \\\n    in-target /bin/sh -c \"echo 'Defaults env_keep += \\\"SSH_AUTH_SOCK\\\" >> /etc/sudoers\"; \\\n    in-target mkdir -p /home/%s/.ssh; \\\n    in-target /bin/sh -c \"echo '%s' >> /home/%s/.ssh/authorized_keys\"; \\\n    in-target chown -R %s:%s /home/%s/; \\\n\tin-target chmod -R go-rwx /home/%s/.ssh/authorized_keys; \\\n\tin-target sudo sed -i '/ swap / s/^/#/' /etc/fstab\n`\n\n//BuildPreeSeedConfig - Creates a new presseed configuration using the passed data\nfunc (config *HostConfig) BuildPreeSeedConfig() string {\n\n\tvar key []byte\n\tvar err error\n\n\t// Check the key has been populated\n\tif config.SSHKey == \"\" {\n\t\tlog.Errorf(\"This server [%s] is being deployed with no SSH Key\", config.ServerName)\n\t} else {\n\t\t// Decode the base64 into the SSH key\n\t\tkey, err = base64.StdEncoding.DecodeString(config.SSHKey)\n\t\tif err != nil {\n\t\t\tlog.Errorf(err.Error())\n\t\t}\n\t}\n\n\tvar parsedDisk string\n\n\tif *config.LVMEnable {\n\t\t// We're using LVM, check if swap should be disabled or not\n\t\tif *config.SwapDisabled {\n\t\t\tparsedDisk = preseedLVMDisk + preseedLVMDiskRecipe2 + preseedLVMDiskDisableSwap\n\t\t} else {\n\t\t\tparsedDisk = preseedLVMDisk + preseedLVMDiskRecipe\n\t\t}\n\t} else {\n\t\tif *config.SwapDisabled {\n\t\t\tparsedDisk = preseedDisk + noswap\n\t\t} else {\n\t\t\tparsedDisk = preseedDisk + swap\n\t\t}\n\t}\n\n\tparsedNet := fmt.Sprintf(preseedNet, config.Adapter, config.Gateway, config.IPAddress, config.NameServer, config.Subnet, config.ServerName)\n\tparsedPkg := fmt.Sprintf(preseedPkg, config.RepositoryAddress, config.MirrorDirectory, config.RepositoryAddress, config.MirrorDirectory, config.Packages)\n\tparsedCmd := fmt.Sprintf(preseedCmd, config.Username, key, config.Username, config.Username, config.Username, config.Username, config.Username)\n\tparsedUsr := fmt.Sprintf(preseedUsers, config.Username, config.Username, config.Password, config.Password)\n\treturn fmt.Sprintf(\"%s%s%s%s%s%s\", preseedHead, parsedDisk, parsedNet, parsedPkg, parsedUsr, parsedCmd)\n}\n"
  },
  {
    "path": "pkg/services/templateUtils.go",
    "content": "package services\n\nimport (\n\t\"encoding/base64\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"strings\"\n)\n\n// ReadKeyFromFile - will attempt to read an sshkey from a file and populate the struct\nfunc (c *HostConfig) ReadKeyFromFile() (string, error) {\n\tvar buffer []byte\n\tif _, err := os.Stat(c.SSHKeyPath); !os.IsNotExist(err) {\n\t\tbuffer, err = ioutil.ReadFile(c.SSHKeyPath)\n\t\tif err != nil {\n\t\t\t// Unable to read the file\n\t\t\treturn \"\", err\n\t\t}\n\t} else {\n\t\t// File doesn't exist\n\t\treturn \"\", err\n\t}\n\n\t// TrimRight will remove the carriage return from the end of the buffer\n\tsingleLine := strings.TrimRight(string(buffer), \"\\r\\n\")\n\treturn singleLine, nil\n}\n\n// This will attempt to parse an SSH file in the host config and load it as a base64 encoded KEY\nfunc (c *HostConfig) parseSSH() error {\n\t// If a file is specified then lets read it and base64 the results (as long as a key doesn't already exist)\n\tif c.SSHKeyPath != \"\" && c.SSHKey == \"\" {\n\t\tdata, err := c.ReadKeyFromFile()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc.SSHKey = base64.StdEncoding.EncodeToString([]byte(data))\n\t}\n\treturn nil\n}\n\n// PopulateFromGlobalConfiguration - This will read a deployment configuration and attempt to fill any missing fields from the global config\nfunc (c *HostConfig) PopulateFromGlobalConfiguration(globalConfig HostConfig) {\n\t// NETWORK CONFIGURATION\n\n\t// Inherit the global Gateway\n\tif c.Gateway == \"\" {\n\t\tc.Gateway = globalConfig.Gateway\n\t}\n\n\t// Inherit the global Subnet\n\tif c.Subnet == \"\" {\n\t\tc.Subnet = globalConfig.Subnet\n\t}\n\n\t// Inherit the global Name Server (DNS)\n\tif c.NameServer == \"\" {\n\t\tc.NameServer = globalConfig.NameServer\n\t}\n\n\tif c.Adapter == \"\" {\n\t\tc.Adapter = globalConfig.Adapter\n\t}\n\n\t// Disk Configuration\n\n\tif c.LVMEnable == nil && globalConfig.LVMEnable != nil {\n\t\tc.LVMEnable = globalConfig.LVMEnable\n\t} else {\n\t\tdisabled := false\n\t\tc.LVMEnable = &disabled\n\t}\n\n\tif c.SwapDisabled == nil && globalConfig.SwapDisabled != nil {\n\t\tc.SwapDisabled = globalConfig.SwapDisabled\n\t} else {\n\t\tdisabled := false\n\t\tc.SwapDisabled = &disabled\n\t}\n\n\t// REPOSITORY CONFIGURATION\n\n\t// Inherit the global Repository address\n\tif c.RepositoryAddress == \"\" {\n\t\tc.RepositoryAddress = globalConfig.RepositoryAddress\n\t}\n\n\t// Inherit the global Repository Mirror directory (typically /ubuntu)\n\tif c.MirrorDirectory == \"\" {\n\t\tc.MirrorDirectory = globalConfig.MirrorDirectory\n\t}\n\n\t// USER CONFIGURATION\n\n\t// Inherit the global Username\n\tif c.Username == \"\" {\n\t\tc.Username = globalConfig.Username\n\t}\n\n\t// Inherit the global Password\n\tif c.Password == \"\" {\n\t\tc.Password = globalConfig.Password\n\t}\n\n\t// Inherit the global SSH Key Path\n\tif c.SSHKeyPath == \"\" {\n\t\tc.SSHKeyPath = globalConfig.SSHKeyPath\n\t}\n\n\t// Package Configuration\n\n\t// Inherit the global package selection\n\tif c.Packages == \"\" {\n\t\tc.Packages = globalConfig.Packages\n\t}\n\n\t// BOOTy configuration (TODO CAN NOT LEAVE THIS HERE)\n\n\tif c.BOOTYAction == \"\" {\n\t\tc.BOOTYAction = globalConfig.BOOTYAction\n\t}\n\n\tif c.Compressed == nil && globalConfig.Compressed != nil {\n\t\tc.Compressed = globalConfig.Compressed\n\t}\n\n\tif c.GrowPartition == nil && globalConfig.GrowPartition != nil {\n\t\tc.GrowPartition = globalConfig.GrowPartition\n\t}\n\n\tif c.LVMRootName == \"\" {\n\t\tc.LVMRootName = globalConfig.LVMRootName\n\t}\n\n\t// WRITE to server\n\tif c.DestinationDevice == \"\" {\n\t\tc.DestinationDevice = globalConfig.DestinationDevice\n\t}\n\n\tif c.SourceImage == \"\" {\n\t\tc.SourceImage = globalConfig.SourceImage\n\t}\n\n\t// READ from server\n\tif c.DestinationAddress == \"\" {\n\t\tc.DestinationAddress = globalConfig.DestinationAddress\n\t}\n\n\tif c.SourceDevice == \"\" {\n\t\tc.SourceDevice = globalConfig.SourceDevice\n\t}\n\n\t// TODO\n\tif c.ShellOnFail == nil && globalConfig.ShellOnFail != nil {\n\t\tc.ShellOnFail = globalConfig.ShellOnFail\n\t}\n}\n"
  },
  {
    "path": "pkg/services/types.go",
    "content": "package services\n\n// TYPE DEFINITIONS Below\n\n// BootController contains the settings that define how the remote boot will\ntype BootController struct {\n\tAdapterName *string `json:\"adapter\"` // A physical adapter to bind to e.g. en0, eth0\n\n\t// Servers\n\tEnableDHCP *bool `json:\"enableDHCP\"` // Enable Server\n\t//DHCP Configuration\n\tDHCPConfig dhcpConfig `json:\"dhcpConfig,omitempty\"`\n\n\t// TFTP / HTTP configuration\n\tEnableTFTP  *bool   `json:\"enableTFTP\"`  // Enable Server\n\tTFTPAddress *string `json:\"addressTFTP\"` // Should ideally be the IP of the adapter\n\tEnableHTTP  *bool   `json:\"enableHTTP\"`  // Enable Server\n\tHTTPAddress *string `json:\"addressHTTP\"` // Should ideally be the IP of the adapter\n\n\t// TFTP Configuration\n\tPXEFileName *string `json:\"pxePath\"` // undionly.kpxe\n\n\t// Boot Configuration\n\tBootConfigs []BootConfig `json:\"bootConfigs\"` // Array of kernel configurations\n\n\thandler *DHCPSettings\n}\n\ntype dhcpConfig struct {\n\tDHCPAddress      string `json:\"addressDHCP\"`    // Should ideally be the IP of the adapter\n\tDHCPStartAddress string `json:\"startDHCP\"`      // The first available DHCP address\n\tDHCPLeasePool    int    `json:\"leasePoolDHCP\"`  // Size of the IP Address pool\n\tDHCPSubnet       string `json:\"subnetDHCP\"`     // Subnet for leases\n\tDHCPGateway      string `json:\"gatewayDHCP\"`    // Gateway to advertise\n\tDHCPDNS          string `json:\"nameserverDHCP\"` // DNS server to advertise\n}\n\n// BootConfig defines a named configuration for booting\ntype BootConfig struct {\n\tConfigName string `json:\"configName\"`\n\tConfigType string `json:\"configType\"`\n\n\t// iPXE file settings - exported\n\tKernel  string `json:\"kernelPath\"`\n\tInitrd  string `json:\"initrdPath\"`\n\tCmdline string `json:\"cmdline\"`\n\n\t// ISO Reader settings\n\tISOPath   string `json:\"isoPath,omitempty\"`\n\tISOPrefix string `json:\"isoPrefix,omitempty\"`\n}\n\n// DeploymentConfigurationFile - The bootstraps.Configs is used by other packages to manage use case for Mac addresses\ntype DeploymentConfigurationFile struct {\n\tGlobalServerConfig HostConfig         `json:\"globalConfig\"`\n\tConfigs            []DeploymentConfig `json:\"deployments\"`\n}\n\n// DeploymentConfig - is used to parse the files containing all server configurations\ntype DeploymentConfig struct {\n\tMAC        string     `json:\"mac\"`\n\tConfigName string     `json:\"bootConfigName,omitempty\"` // To be discovered in the controller BootConfig array\n\tConfigBoot BootConfig `json:\"bootConfig,omitempty\"`     // Array of kernel configurations\n\tConfigHost HostConfig `json:\"config\"`\n}\n\n// HostConfig - Defines how a server will be configured by plunder\ntype HostConfig struct {\n\n\t// Not required for the global configuration\n\tAdapter    string `json:\"adapter,omitempty\"`  // Adapter to be configured with networking address\n\tIPAddress  string `json:\"address,omitempty\"`  // Allocated IP address for a host (ignored for global)\n\tServerName string `json:\"hostname,omitempty\"` // Hostname to be applied to a server\n\n\t// Typically shared details\n\tGateway    string `json:\"gateway,omitempty\"`    // Default Gateway\n\tSubnet     string `json:\"subnet,omitempty\"`     // Subnet to be used for the host\n\tNameServer string `json:\"nameserver,omitempty\"` // Set the default nameserver for DNS\n\tNTPServer  string `json:\"ntpserver,omitempty\"`  // Time Server to be used\n\n\tLVMEnable    *bool `json:\"lvmEnabled,omitempty\"`   // Use LVM for the configuration\n\tSwapDisabled *bool `json:\"swapDisabled,omitempty\"` // Dont create swap partitions\n\n\tUsername string `json:\"username,omitempty\"`\n\tPassword string `json:\"password,omitempty\"`\n\n\t// RepositoryAddress is required for pre-seed / kickstart\n\tRepositoryAddress string `json:\"repoaddress,omitempty\"`\n\t// MirrorDirectory is an Ubuntu specific config\n\tMirrorDirectory string `json:\"mirrordir,omitempty\"`\n\n\t// SSHKeyPath will typically be referenced from a file ~/.ssh/id_rsa.pub\n\tSSHKeyPath string `json:\"sshkeypath,omitempty\"`\n\t// SSHKey is a full SSH Key in base 64\n\tSSHKey string `json:\"sshkey,omitempty\"`\n\n\t// Packages to be installed\n\tPackages string `json:\"packages,omitempty\"`\n\n\t// OS Image provisioning\n\tBOOTYAction string `json:\"bootyAction,omitempty\"`\n\tCompressed  *bool  `json:\"compressed,omitempty\"`\n\n\t// Write image to disk from remote address\n\tSourceImage       string `json:\"sourceImage,omitempty\"`\n\tDestinationDevice string `json:\"destinationDevice,omitempty\"`\n\n\t// Read image from disk from remote address\n\tSourceDevice       string `json:\"sourceDevice,omitempty\"`\n\tDestinationAddress string `json:\"destinationAddress,omitempty\"`\n\n\t// Post tasks - Once the image has been deployed\n\n\t// Volume modifications (LVM2)\n\tGrowPartition *int   `json:\"growPartition,omitempty\"`\n\tLVMRootName   string `json:\"lvmRootName,omitempty\"`\n\n\t// Troubleshooting\n\tShellOnFail *bool `json:\"shellOnFail,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/ssh/go.mod",
    "content": "module github.com/plunder-app/plunder/pkg/ssh\n\ngo 1.12\n"
  },
  {
    "path": "pkg/ssh/sshClient.go",
    "content": "package ssh\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// StartConnection -\nfunc (c *HostSSHConfig) StartConnection() (*ssh.Client, error) {\n\tvar err error\n\n\thost := c.Host\n\tif !strings.ContainsAny(c.Host, \":\") {\n\t\thost = host + \":22\"\n\t}\n\n\tlog.Debugf(\"Beginning connection to [%s] with user [%s] and timeout [%d]\", c.Host, c.User, c.ClientConfig.Timeout)\n\tc.Connection, err = ssh.Dial(\"tcp\", host, c.ClientConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn c.Connection, nil\n}\n\n// StopConnection -\nfunc (c *HostSSHConfig) StopConnection() error {\n\tif c.Connection != nil {\n\t\treturn c.Connection.Close()\n\t}\n\treturn fmt.Errorf(\"Connection not established\")\n}\n\n// StartSession -\nfunc (c *HostSSHConfig) StartSession() (*ssh.Session, error) {\n\tvar err error\n\tc.Connection, err = c.StartConnection()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc.Session, err = c.Connection.NewSession()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn c.Session, err\n}\n\n// StopSession -\nfunc (c *HostSSHConfig) StopSession() {\n\tif c.Session != nil {\n\t\tc.Session.Close()\n\t}\n}\n\n// To string\nfunc (c HostSSHConfig) String() string {\n\treturn c.User + \"@\" + c.Host\n}\n\n//FindHosts - This will take an array of hosts and find the matching HostSSH Configuration\nfunc FindHosts(parlayHosts []string) ([]HostSSHConfig, error) {\n\tvar hostArray []HostSSHConfig\n\tfor x := range parlayHosts {\n\t\tfound := false\n\t\tfor y := range Hosts {\n\n\t\t\t//TODO : Probably needs strings.ToLower() (needs testing)\n\t\t\tif parlayHosts[x] == Hosts[y].Host {\n\t\t\t\thostArray = append(hostArray, Hosts[y])\n\t\t\t\tfound = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tif found == false {\n\t\t\treturn nil, fmt.Errorf(\"Host [%s] has no SSH credentials\", parlayHosts[x])\n\t\t}\n\t}\n\treturn hostArray, nil\n}\n"
  },
  {
    "path": "pkg/ssh/sshCommand.go",
    "content": "package ssh\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// SingleExecute - This will execute a command on a single host\nfunc SingleExecute(cmd, pipefile, pipecmd string, host HostSSHConfig, to int) CommandResult {\n\tvar configs []HostSSHConfig\n\tconfigs = append(configs, host)\n\tresult := ParalellExecute(cmd, pipefile, pipecmd, configs, to)\n\treturn result[0]\n}\n\n//ParalellExecute - This will execute the same command in paralell across multiple hosts\nfunc ParalellExecute(cmd, pipefile, pipecmd string, hosts []HostSSHConfig, to int) []CommandResult {\n\tvar cmdResults []CommandResult\n\t// Run parallel ssh session (max 10)\n\tresults := make(chan CommandResult, 10)\n\tvar d time.Duration\n\n\t// Calculate the timeout\n\tif to == 0 {\n\t\t// If no timeout then default to one year (TODO)\n\t\td = time.Duration(8760) * time.Hour\n\t} else {\n\t\td = time.Duration(to) * time.Second\n\t}\n\n\t// Set the timeout\n\ttimeout := time.After(d)\n\t// Execute command on hosts\n\tfor _, host := range hosts {\n\t\tgo func(host HostSSHConfig) {\n\t\t\tres := new(CommandResult)\n\t\t\tres.Host = host.Host\n\n\t\t\tif pipefile != \"\" {\n\t\t\t\tif text, err := host.ExecuteCmdWithStdinFile(cmd, pipefile); err != nil {\n\t\t\t\t\t// Report any returned values\n\t\t\t\t\tres.Error = err\n\t\t\t\t\tres.Result = text\n\t\t\t\t} else {\n\t\t\t\t\tres.Result = text\n\t\t\t\t}\n\t\t\t} else if pipecmd != \"\" {\n\t\t\t\tif text, err := host.ExecuteCmdWithStdinCmd(cmd, pipecmd); err != nil {\n\t\t\t\t\t// Report any returned values\n\t\t\t\t\tres.Error = err\n\t\t\t\t\tres.Result = text\n\t\t\t\t} else {\n\t\t\t\t\tres.Result = text\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif text, err := host.ExecuteCmd(cmd); err != nil {\n\t\t\t\t\t// Report any returned values\n\t\t\t\t\tres.Error = err\n\t\t\t\t\tres.Result = text\n\t\t\t\t} else {\n\t\t\t\t\tres.Result = text\n\t\t\t\t}\n\t\t\t}\n\t\t\tresults <- *res\n\t\t}(host)\n\t}\n\n\tfor i := 0; i < len(hosts); i++ {\n\t\tselect {\n\t\tcase res := <-results:\n\t\t\t// Append the results of a succesfull command\n\t\t\tcmdResults = append(cmdResults, res)\n\t\tcase <-timeout:\n\t\t\t// In the event that a command times out then append the details\n\t\t\tfailedCommand := CommandResult{\n\t\t\t\tHost:   hosts[i].Host,\n\t\t\t\tError:  fmt.Errorf(\"Command Timed out\"),\n\t\t\t\tResult: \"\",\n\t\t\t}\n\t\t\tcmdResults = append(cmdResults, failedCommand)\n\n\t\t}\n\t}\n\treturn cmdResults\n}\n\n// ExecuteCmd -\nfunc (c *HostSSHConfig) ExecuteCmd(cmd string) (string, error) {\n\tif c.Session == nil {\n\t\tif _, err := c.StartSession(); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tb, err := c.Session.CombinedOutput(cmd)\n\n\treturn string(b), err\n}\n\n// ExecuteCmdWithStdinFile -\nfunc (c *HostSSHConfig) ExecuteCmdWithStdinFile(cmd, filePath string) (string, error) {\n\tif c.Session == nil {\n\t\tif _, err := c.StartSession(); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\t// get a stdin pipe\n\tsi, err := c.Session.StdinPipe()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// get a stdout pipe\n\tso, err := c.Session.StdoutPipe()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// open file as an io.reader\n\t// Also resolve the absolute path just incase\n\tabsPath, _ := filepath.Abs(filePath)\n\tfile, err := os.Open(absPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Start a command on our remote session, this should be something that is expecting stdin\n\tif err := c.Session.Start(cmd); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// do the actual work\n\tn, err := io.Copy(si, file)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// Close the stdin as we've finised transmitting the data\n\tsi.Close()\n\n\tlog.Debugf(\"Copied %d bytes over the stdin pipe\", n)\n\t// wait for process to finishe\n\tif err := c.Session.Wait(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Read all the data from the bu\n\tvar b []byte\n\tif b, err = ioutil.ReadAll(so); err != nil {\n\t\treturn \"\", err\n\n\t}\n\treturn string(b), nil\n\n}\n\n// ExecuteCmdWithStdinCmd -\nfunc (c *HostSSHConfig) ExecuteCmdWithStdinCmd(cmd, localCmd string) (string, error) {\n\tif c.Session == nil {\n\t\tif _, err := c.StartSession(); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\t// get a stdin pipe\n\tsi, err := c.Session.StdinPipe()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// get a stdout pipe\n\tso, err := c.Session.StdoutPipe()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Start a command on our remote session, this should be something that is expecting stdin\n\tif err := c.Session.Start(cmd); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Start our local command that should be exposing something through stdout\n\tlocalExecCmd := exec.Command(\"bash\", \"-c\", localCmd)\n\tlocalso, err := localExecCmd.StdoutPipe()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\terr = localExecCmd.Start()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// do the actual work\n\tn, err := io.Copy(si, localso)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Close the stdin/stdout as we've finised transmitting the data\n\tsi.Close()\n\tlocalso.Close()\n\n\tlog.Debugf(\"Copied %d bytes over the stdin pipe\", n)\n\n\t// Wait for local process to finish\n\terr = localExecCmd.Wait()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// wait for remote process to finish\n\tif err := c.Session.Wait(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Read all the data from the bu\n\tvar b []byte\n\tif b, err = ioutil.ReadAll(so); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(b), nil\n\n}\n"
  },
  {
    "path": "pkg/ssh/sshConfig.go",
    "content": "package ssh\n\nimport (\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// Hosts - The array of all hosts once loaded\nvar Hosts []HostSSHConfig\n\n// HostSSHConfig - The struct of an SSH connection\ntype HostSSHConfig struct {\n\tHost         string\n\tUser         string\n\tTimeout      int\n\tClientConfig *ssh.ClientConfig\n\tSession      *ssh.Session\n\tConnection   *ssh.Client\n}\n\n// CommandResult - This is returned when running commands against servers\ntype CommandResult struct {\n\tHost   string // Host that the command was being ran against\n\tError  error  // Errors that may have been returned\n\tResult string // The CLI results\n}\n\n// SetPassword - Turn a password string into an SSH auth method\nfunc SetPassword(password string) []ssh.AuthMethod {\n\treturn []ssh.AuthMethod{ssh.Password(password)}\n}\n"
  },
  {
    "path": "pkg/ssh/sshImport.go",
    "content": "package ssh\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/mitchellh/go-homedir\"\n\t\"github.com/plunder-app/plunder/pkg/services\"\n\t\"golang.org/x/crypto/ssh\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// cachedGlobalKey caches the content of the gloal SSH key to save on excessing file operations\nvar cachedGlobalKey ssh.AuthMethod\n\n// cachedUsername caches the content of the gloal Username on the basis that a lot of key based ops will share the same user\nvar cachedUsername string\n\n// The init function will look for the default key and the default user\n\nfunc init() {\n\tu, err := user.Current()\n\tif err != nil {\n\t\tlog.Warnf(\"Failed to find current user, if this is overridden by a deployment configuration this error can be ignored\")\n\t}\n\n\t// If the above call hasn't errored, then u shouldn't be nil\n\tif u != nil {\n\t\tcachedUsername = u.Username\n\t}\n\n\tcachedGlobalKey, err = findDefaultKey()\n\tif err != nil {\n\t\tlog.Warnf(\"Failed to find default ssh key, if this is overridden by a deployment configuration this error can be ignored\")\n\t}\n}\n\n// AddHost will append additional hosts to the host array that the ssh package will use\nfunc AddHost(address, keypath, username string) error {\n\tsshHost := HostSSHConfig{\n\t\tHost: address,\n\t}\n\n\t// If a username exists use that, alternatively use the cached entry\n\tif username != \"\" {\n\t\tsshHost.User = username\n\t} else if cachedUsername != \"\" {\n\t\tsshHost.User = cachedUsername\n\t} else {\n\t\treturn fmt.Errorf(\"No username data for SSH authentication has been entered or loaded\")\n\t}\n\n\t// Find additional keys that may exist in the same location\n\tvar keys []ssh.AuthMethod\n\n\tif keypath != \"\" {\n\t\tkey, err := findPrivateKey(keypath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tkeys = append(keys, key)\n\t} else {\n\t\tif cachedGlobalKey != nil {\n\t\t\tkeys = append(keys, cachedGlobalKey)\n\t\t} else {\n\t\t\treturn fmt.Errorf(\"Host [%s] has no key specified\", address)\n\t\t}\n\t}\n\n\t// Default timeout for connecting to a host is five seconds (TODO)\n\tsshHost.ClientConfig = &ssh.ClientConfig{\n\t\tUser:            sshHost.User,\n\t\tAuth:            keys,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tTimeout:         2 * time.Second,\n\t}\n\n\tHosts = append(Hosts, sshHost)\n\n\treturn nil\n}\n\n// ImportHostsFromDeployment - This will parse a deployment (either file or HTTP post)\nfunc ImportHostsFromDeployment(deployment services.DeploymentConfigurationFile) error {\n\n\tif len(deployment.Configs) == 0 {\n\t\treturn fmt.Errorf(\"No deployment configurations found\")\n\t}\n\n\t// Find keys that are in the same places as the public Key\n\tif deployment.GlobalServerConfig.SSHKeyPath != \"\" {\n\t\tvar err error\n\t\t// Find if the private key from the global configuration\n\t\tcachedGlobalKey, err = findDefaultKey()\n\t\tif err != nil {\n\t\t\tlog.Debugf(\"Failed to find default key, using Public key to find private\")\n\t\t\tcachedGlobalKey, err = findPrivateKey(deployment.GlobalServerConfig.SSHKeyPath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t} else {\n\t\tlog.Debugln(\"No global configuration has been loaded, will default to local users keys\")\n\t}\n\n\t// Find a global username to use, in place of an empty config\n\tif deployment.GlobalServerConfig.Username != \"\" {\n\t\tcachedUsername = deployment.GlobalServerConfig.Username\n\t} else {\n\t\tlog.Debugf(\"No global configuration has been loaded, default to user [%s]\", cachedUsername)\n\t}\n\t// Parse the deployments\n\tfor i := range deployment.Configs {\n\t\tvar sshHost HostSSHConfig\n\n\t\tsshHost.Host = deployment.Configs[i].ConfigHost.IPAddress\n\n\t\tif deployment.Configs[i].ConfigHost.Username != \"\" {\n\t\t\tsshHost.User = deployment.Configs[i].ConfigHost.Username\n\t\t} else {\n\t\t\tsshHost.User = deployment.GlobalServerConfig.Username\n\t\t}\n\n\t\t// Find additional keys that may exist in the same location\n\t\tvar keys []ssh.AuthMethod\n\t\tif deployment.Configs[i].ConfigHost.SSHKeyPath != \"\" {\n\t\t\t// Look up default key\n\t\t\tkey, err := findDefaultKey()\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugf(\"Failed to find default key, using Public key to find private\")\n\t\t\t\tkey, err = findPrivateKey(deployment.Configs[i].ConfigHost.SSHKeyPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkeys = append(keys, key)\n\t\t} else {\n\t\t\tif cachedGlobalKey != nil {\n\t\t\t\tkeys = append(keys, cachedGlobalKey)\n\t\t\t} else {\n\t\t\t\treturn fmt.Errorf(\"Host [%s] has no key specified\", deployment.Configs[i].ConfigHost.IPAddress)\n\t\t\t}\n\t\t}\n\n\t\t// Default timeout for connecting to a host is five seconds (TODO)\n\t\tsshHost.ClientConfig = &ssh.ClientConfig{\n\t\t\tUser:            sshHost.User,\n\t\t\tAuth:            keys,\n\t\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\t\tTimeout:         2 * time.Second,\n\t\t}\n\n\t\tHosts = append(Hosts, sshHost)\n\t}\n\n\treturn nil\n}\n\n// ImportHostsFromRawDeployment - This will parse a deployment (either file or HTTP post)\nfunc ImportHostsFromRawDeployment(config []byte) error {\n\n\tvar deployment services.DeploymentConfigurationFile\n\n\terr := json.Unmarshal(config, &deployment)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn ImportHostsFromDeployment(deployment)\n\n}\n\n// findDefaultKey - This will look in the users $HOME/.ssh/ for a key to add\nfunc findDefaultKey() (ssh.AuthMethod, error) {\n\thome, err := homedir.Dir()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn findPrivateKey(fmt.Sprintf(\"%s/.ssh/id_rsa\", home))\n}\n\n// readKeyFile - Reads a public key from a file\nfunc findPrivateKey(publicKey string) (ssh.AuthMethod, error) {\n\t// Typically turn id_rsa.pub -> id_rsa\n\tprivateKey := strings.TrimSuffix(publicKey, filepath.Ext(publicKey))\n\n\tb, err := ioutil.ReadFile(privateKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tkey, err := ssh.ParsePrivateKey(b)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ssh.PublicKeys(key), nil\n}\n\n// readKeyFile - Reads a public key from a file\nfunc readKeyFile(keyfile string) (ssh.AuthMethod, error) {\n\tb, err := ioutil.ReadFile(keyfile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tkey, err := ssh.ParsePrivateKey(b)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ssh.PublicKeys(key), nil\n}\n\n// ReadKeyFiles - will read an array of keys from disk\nfunc ReadKeyFiles(keyFiles []string) ([]ssh.AuthMethod, error) {\n\tmethods := []ssh.AuthMethod{}\n\n\tfor _, keyname := range keyFiles {\n\t\tpkey, err := readKeyFile(keyname)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif pkey != nil {\n\t\t\tmethods = append(methods, pkey)\n\t\t}\n\t}\n\n\treturn methods, nil\n}\n"
  },
  {
    "path": "pkg/ssh/sshTransfer.go",
    "content": "package ssh\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/pkg/sftp\"\n)\n\n// ParalellDownload - Allow downloading a file over SFTP from multiple hosts in parallel\nfunc ParalellDownload(hosts []HostSSHConfig, source, destination string, to int) []CommandResult {\n\tvar cmdResults []CommandResult\n\t// Run parallel ssh session (max 10)\n\tresults := make(chan CommandResult, 10)\n\n\tvar d time.Duration\n\n\t// Calculate the timeout\n\tif to == 0 {\n\t\t// If no timeout then default to one year (TODO)\n\t\td = time.Duration(8760) * time.Hour\n\t} else {\n\t\td = time.Duration(to) * time.Second\n\t}\n\n\t// Set the timeout\n\ttimeout := time.After(d)\n\n\t// Execute command on hosts\n\tfor _, host := range hosts {\n\t\tgo func(host HostSSHConfig) {\n\t\t\tres := new(CommandResult)\n\t\t\tres.Host = host.Host\n\n\t\t\tif err := host.DownloadFile(source, destination); err != nil {\n\t\t\t\tres.Error = err\n\t\t\t} else {\n\t\t\t\tres.Result = \"Download completed\"\n\t\t\t}\n\t\t\tresults <- *res\n\t\t}(host)\n\t}\n\n\tfor i := 0; i < len(hosts); i++ {\n\t\tselect {\n\t\tcase res := <-results:\n\t\t\t// Append the results of a succesfull command\n\t\t\tcmdResults = append(cmdResults, res)\n\t\tcase <-timeout:\n\t\t\t// In the event that a command times out then append the details\n\t\t\tfailedCommand := CommandResult{\n\t\t\t\tHost:   hosts[i].Host,\n\t\t\t\tError:  fmt.Errorf(\"Download Timed out\"),\n\t\t\t\tResult: \"\",\n\t\t\t}\n\t\t\tcmdResults = append(cmdResults, failedCommand)\n\n\t\t}\n\t}\n\treturn cmdResults\n}\n\n// DownloadFile -\nfunc (c HostSSHConfig) DownloadFile(source, destination string) error {\n\tvar err error\n\tc.Connection, err = c.StartConnection()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// New SFTP client\n\tsftp, err := sftp.NewClient(c.Connection)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer sftp.Close()\n\n\t// Open remote source\n\tsftpSource, err := sftp.Open(source)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer sftpSource.Close()\n\n\t// Open local destination\n\tlocalDestination, err := os.Create(destination)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer localDestination.Close()\n\n\t//\n\t_, err = sftpSource.WriteTo(localDestination)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// An error here isn't cause for alarm, any new transaction should create a new connection\n\t_ = c.StopConnection()\n\n\treturn nil\n}\n\n// ParalellUpload - Allow uploading a file over SFTP to multiple hosts in parallel\nfunc ParalellUpload(hosts []HostSSHConfig, source, destination string, to int) []CommandResult {\n\tvar cmdResults []CommandResult\n\t// Run parallel ssh session (max 10)\n\tresults := make(chan CommandResult, 10)\n\n\tvar d time.Duration\n\n\t// Calculate the timeout\n\tif to == 0 {\n\t\t// If no timeout then default to one year (TODO)\n\t\td = time.Duration(8760) * time.Hour\n\t} else {\n\t\td = time.Duration(to) * time.Second\n\t}\n\n\t// Set the timeout\n\ttimeout := time.After(d)\n\n\t// Execute command on hosts\n\tfor _, host := range hosts {\n\t\tgo func(host HostSSHConfig) {\n\t\t\tres := new(CommandResult)\n\t\t\tres.Host = host.Host\n\n\t\t\tif err := host.UploadFile(source, destination); err != nil {\n\t\t\t\tres.Error = err\n\t\t\t} else {\n\t\t\t\tres.Result = \"Upload completed\"\n\t\t\t}\n\t\t\tresults <- *res\n\t\t}(host)\n\t}\n\n\tfor i := 0; i < len(hosts); i++ {\n\t\tselect {\n\t\tcase res := <-results:\n\t\t\t// Append the results of a succesfull command\n\t\t\tcmdResults = append(cmdResults, res)\n\t\tcase <-timeout:\n\t\t\t// In the event that a command times out then append the details\n\t\t\tfailedCommand := CommandResult{\n\t\t\t\tHost:   hosts[i].Host,\n\t\t\t\tError:  fmt.Errorf(\"Upload Timed out\"),\n\t\t\t\tResult: \"\",\n\t\t\t}\n\t\t\tcmdResults = append(cmdResults, failedCommand)\n\n\t\t}\n\t}\n\treturn cmdResults\n}\n\n// UploadFile -\nfunc (c HostSSHConfig) UploadFile(source, destination string) error {\n\tvar err error\n\tc.Connection, err = c.StartConnection()\n\tif err != nil {\n\t\treturn err\n\t}\n\t// New SFTP client\n\tsftp, err := sftp.NewClient(c.Connection)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer sftp.Close()\n\n\t// Open remote source\n\tsftpDestination, err := sftp.Create(destination)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer sftpDestination.Close()\n\n\t// Open local destination\n\tlocalSource, err := os.Open(source)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer localSource.Close()\n\n\t// copy source file to destination file\n\t_, err = io.Copy(sftpDestination, localSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// An error here isn't cause for alarm, any new transaction should create a new connection\n\t_ = c.StopConnection()\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/utils/go.mod",
    "content": "module github.com/plunder-app/plunder/pkg/utils\n\ngo 1.12\n"
  },
  {
    "path": "pkg/utils/ipxe.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Static URL for retrieving the bootloader\nconst iPXEURL = \"https://boot.ipxe.org/undionly.kpxe\"\n\n// This header is used by all configurations\nconst iPXEHeader = `#!ipxe\ndhcp\necho .\necho .\necho .\necho .\necho +-------------------- Plunder -------------------------------\necho | \necho |    address.: ${net0/ip}\necho |    mac.....: ${net0/mac}  \necho |    gateway.: ${net0/gateway} \necho +------------------------------------------------------------\necho .\necho .\necho .\necho .`\n\n//////////////////////////////\n//\n// Helper Functions\n//\n//////////////////////////////\n\n// IPXEReboot -\nfunc IPXEReboot() string {\n\tscript := `\necho MAC ADDRESS is set to reboot, plunder will reboot the server in 5 seconds\nsleep 5\nreboot\n`\n\treturn iPXEHeader + script\n}\n\n// IPXEAutoBoot -\nfunc IPXEAutoBoot() string {\n\tscript := `\necho Unknown MAC address, PXE boot will keep retrying until configuration changes\n:retry_boot\nautoboot || goto retry_boot\n`\n\treturn iPXEHeader + script\n}\n\n// IPXEPreeseed - This will build an iPXE boot script for Debian/Ubuntu\nfunc IPXEPreeseed(webserverAddress, kernel, initrd, cmdline string) string {\n\tscript := `\nkernel http://%s/%s auto=true url=http://%s/${mac:hexhyp}.cfg priority=critical %s netcfg/choose_interface=${netX/mac}\ninitrd http://%s/%s\nboot\n`\n\t// Replace the addresses inline\n\tbuildScript := fmt.Sprintf(script, webserverAddress, kernel, webserverAddress, cmdline, webserverAddress, initrd)\n\n\treturn iPXEHeader + buildScript\n}\n\n// IPXEKickstart - This will build an iPXE boot script for RHEL/CentOS\nfunc IPXEKickstart(webserverAddress, kernel, initrd, cmdline string) string {\n\tscript := `\nkernel http://%s/%s auto=true url=http://%s/${mac:hexhyp}.cfg priority=critical %s \ninitrd http://%s/%s\nboot\n`\n\t// Replace the addresses inline\n\tbuildScript := fmt.Sprintf(script, webserverAddress, kernel, webserverAddress, cmdline, webserverAddress, initrd)\n\n\treturn iPXEHeader + buildScript\n}\n\n// IPXEVSphere - This will build an iPXE boot script for VMware vSphere/ESXi\nfunc IPXEVSphere(webserverAddress, kernel, cmdline string) string {\n\tscript := `\nkernel http://%s/%s -c http://%s/${mac:hexhyp}.cfg  ks=http://%s/${mac:hexhyp}.ks %s\nboot\n`\n\t// Replace the addresses inline\n\tbuildScript := fmt.Sprintf(script, webserverAddress, kernel, webserverAddress, webserverAddress, cmdline)\n\n\treturn iPXEHeader + buildScript\n}\n\n// IPXEBOOTy - This will build an iPXE boot script for the BOOTy boot loader\nfunc IPXEBOOTy(webserverAddress, kernel, initrd, cmdline string) string {\n\tscript := `\nkernel http://%s/%s BOOTYURL=http://%s %s\ninitrd http://%s/%s\nboot\n`\n\t// Replace the addresses inline\n\tbuildScript := fmt.Sprintf(script, webserverAddress, kernel, webserverAddress, cmdline, webserverAddress, initrd)\n\n\treturn iPXEHeader + buildScript\n}\n\n// IPXEAnyBoot - This will build an iPXE boot script for anything wanting to PXE boot\nfunc IPXEAnyBoot(webserverAddress string, kernel, initrd, cmdline string) string {\n\tscript := `\nkernel http://%s/%s auto=true url=http://%s/${mac:hexhyp}.cfg %s \ninitrd http://%s/%s\nboot\n`\n\t// Replace the addresses inline\n\tbuildScript := fmt.Sprintf(script, webserverAddress, kernel, webserverAddress, cmdline, webserverAddress, initrd)\n\n\treturn iPXEHeader + buildScript\n}\n\n// PullPXEBooter - This will attempt to download the iPXE bootloader\nfunc PullPXEBooter() error {\n\tlog.Infoln(\"Beginning of iPXE download... \")\n\n\t// Create the file\n\tout, err := os.Create(\"undionly.kpxe\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer out.Close()\n\n\t// Get the data\n\tresp, err := http.Get(iPXEURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\t// Writer the body to file\n\t_, err = io.Copy(out, resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Infoln(\"Completed\")\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/utils/nic.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"net\"\n)\n\n// FindIPAddress - this will find the address associated with an adapter\nfunc FindIPAddress(addrName string) (string, string, error) {\n\tvar address string\n\tlist, err := net.Interfaces()\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\tfor _, iface := range list {\n\t\taddrs, err := iface.Addrs()\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\t\tfor _, a := range addrs {\n\t\t\tif ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {\n\t\t\t\tif ipnet.IP.To4() != nil {\n\t\t\t\t\taddress = ipnet.IP.String()\n\t\t\t\t\t// If we're not searching for a specific adapter return the first one\n\t\t\t\t\tif addrName == \"\" {\n\t\t\t\t\t\treturn iface.Name, address, nil\n\t\t\t\t\t} else\n\t\t\t\t\t// If this is the correct adapter return the details\n\t\t\t\t\tif iface.Name == addrName {\n\t\t\t\t\t\treturn iface.Name, address, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\treturn \"\", \"\", fmt.Errorf(\"Unknown interface [%s]\", addrName)\n}\n\n// FindAllIPAddresses - Will return all IP addresses for a server\nfunc FindAllIPAddresses() ([]net.IP, error) {\n\tvar IPS []net.IP\n\tifaces, err := net.Interfaces()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, i := range ifaces {\n\t\taddrs, err := i.Addrs()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, addr := range addrs {\n\t\t\tvar ip net.IP\n\t\t\tswitch v := addr.(type) {\n\t\t\tcase *net.IPNet:\n\t\t\t\tip = v.IP\n\t\t\tcase *net.IPAddr:\n\t\t\t\tip = v.IP\n\t\t\t}\n\t\t\tif ip != nil {\n\t\t\t\tIPS = append(IPS, net.IP(ip))\n\t\t\t}\n\t\t\t// process IP address\n\t\t}\n\t}\n\treturn IPS, nil\n}\n\n//ConvertIP -\nfunc ConvertIP(ipAddress string) ([]byte, error) {\n\t// net.ParseIP has returned IPv6 sized allocations o_O\n\tfixIP := net.ParseIP(ipAddress)\n\tif fixIP == nil {\n\t\treturn nil, fmt.Errorf(\"Couldn't parse the IP address: %s\", ipAddress)\n\t}\n\tif len(fixIP) > 4 {\n\t\treturn fixIP[len(fixIP)-4:], nil\n\t}\n\treturn fixIP, nil\n}\n"
  },
  {
    "path": "pkg/utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"encoding/hex\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"os/signal\"\n\t\"sync\"\n)\n\n//WaitForCtrlC - This function is the loop that will catch a Control-C keypress\nfunc WaitForCtrlC() {\n\tvar endWaiter sync.WaitGroup\n\tendWaiter.Add(1)\n\tvar signalChannel chan os.Signal\n\tsignalChannel = make(chan os.Signal, 1)\n\tsignal.Notify(signalChannel, os.Interrupt)\n\tgo func() {\n\t\t<-signalChannel\n\t\tendWaiter.Done()\n\t}()\n\tendWaiter.Wait()\n}\n\n//FileToHex - this is a helper function to allow embedding files into .go files\nfunc FileToHex(filePath string) (sl string, err error) {\n\n\tbs, err := ioutil.ReadFile(filePath)\n\tif err != nil {\n\t\treturn\n\t}\n\tsl = hex.EncodeToString(bs)\n\treturn\n\n}\n"
  },
  {
    "path": "plugin/docker/docker.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/plunder-app/plunder/pkg/parlay/parlaytypes\"\n)\n\nconst pluginInfo = `This plugin is used to managed docker automation`\n\ntype image struct {\n\n\t// Image details\n\tImageNames []string `json:\"imageName\"`\n\tImageFiles []string `json:\"imageFile\"`\n\n\tDockerLocalSudo  bool `json:\"localSudo\"`\n\tDockerRemoteSudo bool `json:\"remoteSudo\"`\n}\n\ntype tag struct {\n\n\t// A list of sources and target tags\n\tSourceNames []string `json:\"sourceNames,omitempty\"`\n\tTargetNames []string `json:\"targetNames,omitempty\"`\n\n\t// These two fields are used to change out a tag (e.g. version number) or the repository itself\n\tTargetTag  string `json:\"imageTag,omitempty\"`\n\tTargetRepo string `json:\"imageRepo,omitempty\"`\n}\n\n// Dummy main function\nfunc main() {}\n\n// ParlayActionList - This should return an array of actions\nfunc ParlayActionList() []string {\n\treturn []string{\n\t\t\"docker/image\",\n\t\t\"docker/tag\"}\n}\n\n// ParlayActionDetails - This should return an array of action descriptions\nfunc ParlayActionDetails() []string {\n\treturn []string{\n\t\t\"This action automates the management of docker images\",\n\t\t\"This action manages the tagging of docker images\"}\n}\n\n// ParlayPluginInfo - returns information about the plugin\nfunc ParlayPluginInfo() string {\n\treturn pluginInfo\n}\n\n// ParlayUsage - Returns the json that matches the specific action\n// <- action is a string that defines which action the usage information should be\n// <- raw - raw JSON that will be manipulated into a correct struct that matches the action\n// -> err is any error that has been generated\nfunc ParlayUsage(action string) (raw json.RawMessage, err error) {\n\n\t// This example plugin only has the code for \"exampleAction/test\" however this switch statement\n\t// should handle all exposed actions from the plugin\n\tswitch action {\n\tcase \"docker/image\":\n\t\ta := image{\n\t\t\tImageFiles: []string{\"./my_image.tar.gz\", \"./my__other_image.tar.gz\"},\n\t\t\tImageNames: []string{\"gcr.io/my_image:latest\", \"gcr.io/my_other_image:latest\"},\n\t\t}\n\t\t// In order to turn a struct into an map[string]interface we need to turn it into JSON\n\n\t\treturn json.Marshal(a)\n\tcase \"docker/tag\":\n\t\ta := tag{\n\t\t\tSourceNames: []string{\"gcr.io/my_image:latest\"},\n\t\t\tTargetNames: []string{\"internal_repo/my_image:1.0\"},\n\t\t}\n\t\t// In order to turn a struct into an map[string]interface we need to turn it into JSON\n\n\t\treturn json.Marshal(a)\n\tdefault:\n\t\treturn raw, fmt.Errorf(\"Action [%s] could not be found\", action)\n\t}\n}\n\n// ParlayExec - Parses the action and the data that the action will consume\n// <- action a string that details the action to be executed\n// <- raw - raw JSON that will be manipulated into a correct struct that matches the action\n// -> actions are an array of generated actions that the parser will then execute\n// -> err is any error that has been generated\nfunc ParlayExec(action, host string, raw json.RawMessage) (actions []parlaytypes.Action, err error) {\n\n\t// This example plugin only has the code for \"exampleAction/test\" however this switch statement\n\t// should handle all exposed actions from the plugin\n\tswitch action {\n\tcase \"docker/image\":\n\t\tvar img image\n\t\t// Unmarshall the JSON into the struct\n\t\terr = json.Unmarshal(raw, &img)\n\t\treturn img.generateImageActions(host), err\n\tcase \"docker/tag\":\n\t\tvar t tag\n\t\t// Unmarshall the JSON into the struct\n\t\terr = json.Unmarshal(raw, &t)\n\t\treturn t.generateTagActions(host)\n\tdefault:\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "plugin/docker/docker_actions.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"path\"\n\n\t\"github.com/plunder-app/plunder/pkg/parlay/parlaytypes\"\n)\n\nfunc (i *image) generateImageActions(host string) []parlaytypes.Action {\n\tvar generatedActions []parlaytypes.Action\n\tvar a parlaytypes.Action\n\tvar dockerRemoteString, dockerLocalString string\n\n\t// This should be set to true if sudo (NOPASSWD) is enabled and required on the local host\n\tif i.DockerLocalSudo == true {\n\t\tdockerLocalString = \"sudo docker save\"\n\t} else {\n\t\tdockerLocalString = \"docker save\"\n\t}\n\n\t// This should be set to true if sudo (NOPASSWD) is enabled and required on the remote host\n\tif i.DockerRemoteSudo == true {\n\t\tdockerRemoteString = \"sudo docker\"\n\t} else {\n\t\tdockerRemoteString = \"docker\"\n\t}\n\n\tif len(i.ImageFiles) != 0 {\n\t\t// If we've specified a file (tarball, or tar+gzip) we cat then pipe over SSH to a docker load\n\n\t\tfor y := range i.ImageFiles {\n\t\t\ta = parlaytypes.Action{\n\t\t\t\tActionType:      \"command\",\n\t\t\t\tCommand:         fmt.Sprintf(\"%s load \", dockerRemoteString),\n\t\t\t\tCommandPipeFile: i.ImageFiles[y],\n\t\t\t\tName:            fmt.Sprintf(\"Upload container image %s to remote docker host\", path.Base(i.ImageFiles[y])),\n\t\t\t}\n\t\t\tgeneratedActions = append(generatedActions, a)\n\t\t}\n\t} else if len(i.ImageNames) != 0 {\n\n\t\t// If we've specified a an existing image from the local docker image store then we \"save\" it (pipe to stdin)\n\t\t// then we can cat then pipe over SSH to a docker load\n\t\tfor y := range i.ImageNames {\n\n\t\t\ta = parlaytypes.Action{\n\t\t\t\tActionType:     \"command\",\n\t\t\t\tCommand:        fmt.Sprintf(\"%s load\", dockerRemoteString),\n\t\t\t\tCommandPipeCmd: fmt.Sprintf(\"%s %s\", dockerLocalString, i.ImageNames[y]),\n\t\t\t\tName:           fmt.Sprintf(\"Upload container image %s to remote docker host\", i.ImageNames[y]),\n\t\t\t}\n\t\t\tgeneratedActions = append(generatedActions, a)\n\t\t}\n\t}\n\n\treturn generatedActions\n}\n\nfunc (t *tag) generateTagActions(host string) ([]parlaytypes.Action, error) {\n\n\tif len(t.SourceNames) != len(t.TargetNames) {\n\t\treturn nil, fmt.Errorf(\"The number of images to retag doesn't match the number of tags\")\n\t}\n\tvar generatedActions []parlaytypes.Action\n\n\t// Iterate through all of the images and create retagging actions\n\tfor y := range t.SourceNames {\n\t\t// Generate the retag action\n\t\tvar a = parlaytypes.Action{\n\t\t\tActionType:  \"command\",\n\t\t\tCommand:     fmt.Sprintf(\"sudo docker tag %s %s\", t.SourceNames[y], t.TargetNames[y]),\n\t\t\tCommandSudo: \"root\",\n\t\t\tName:        fmt.Sprintf(\"Retag %s --> %s\", t.SourceNames[y], t.TargetNames[y]),\n\t\t}\n\n\t\tgeneratedActions = append(generatedActions, a)\n\t}\n\treturn generatedActions, nil\n}\n"
  },
  {
    "path": "plugin/example.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/plunder-app/plunder/pkg/parlay/parlaytypes\"\n)\n\nconst pluginInfo = `This example plugin is used to demonstrate the structure of a plugin`\n\n// pluginAction - defines the struct that is unique to the\ntype pluginTestAction struct {\n\tCredentials string `json:\"credentials\"`\n\tAddress     string `json:\"address\"`\n}\n\n// Dummy main function\nfunc main() {}\n\n// ParlayActionList - This should return an array of actions\nfunc ParlayActionList() []string {\n\treturn []string{\n\t\t\"exampleAction/test\",\n\t\t\"exampleAction/demo\",\n\t\t\"exampleAction/example\"}\n}\n\n// ParlayActionDetails - This should return an array of action descriptions\nfunc ParlayActionDetails() []string {\n\treturn []string{\n\t\t\"This action handles the testing part of the example plugin\",\n\t\t\"This action handles the demonstration of the example plugin\",\n\t\t\"This action handles an example of the example plugin!\"}\n}\n\n// ParlayPluginInfo - returns information about the plugin\nfunc ParlayPluginInfo() string {\n\treturn pluginInfo\n}\n\n//ParlayActions -\nfunc ParlayActions(action string, iface interface{}) []parlaytypes.Action {\n\tvar actions []parlaytypes.Action\n\ta := parlaytypes.Action{\n\t\tCommand: \"example/test\",\n\t}\n\tactions = append(actions, a)\n\treturn actions\n}\n\n// ParlayUsage - Returns the json that matches the specific action\n// <- action is a string that defines which action the usage information should be\n// <- raw - raw JSON that will be manipulated into a correct struct that matches the action\n// -> err is any error that has been generated\nfunc ParlayUsage(action string) (raw json.RawMessage, err error) {\n\n\t// This example plugin only has the code for \"exampleAction/test\" however this switch statement\n\t// should handle all exposed actions from the plugin\n\tswitch action {\n\tcase \"exampleAction/test\":\n\t\ta := pluginTestAction{\n\t\t\tCredentials: \"AAABBBCCCCDDEEEE\",\n\t\t\tAddress:     \"172.0.0.1\",\n\t\t}\n\t\t// In order to turn a struct into an map[string]interface we need to turn it into JSON\n\n\t\treturn json.Marshal(a)\n\tdefault:\n\t\treturn raw, fmt.Errorf(\"Action [%s] could not be found\", action)\n\t}\n}\n\n// ParlayExec - Parses the action and the data that the action will consume\n// <- action a string that details the action to be executed\n// <- raw - raw JSON that will be manipulated into a correct struct that matches the action\n// -> actions are an array of generated actions that the parser will then execute\n// -> err is any error that has been generated\nfunc ParlayExec(action, host string, raw json.RawMessage) (actions []parlaytypes.Action, err error) {\n\n\tvar t pluginTestAction\n\t// Unmarshall the JSON into the struct\n\tjson.Unmarshal(raw, &t)\n\t// We can now use the fields as part of the struct\n\n\t// This example plugin only has the code for \"exampleAction/test\" however this switch statement\n\t// should handle all exposed actions from the plugin\n\tswitch action {\n\tcase \"exampleAction/test\":\n\t\ta := parlaytypes.Action{\n\t\t\tName:       \"Echo the address\",\n\t\t\tActionType: \"command\",\n\t\t\tCommand:    fmt.Sprintf(\"echo %s\", t.Address),\n\t\t}\n\t\tactions = append(actions, a)\n\n\t\ta.Name = \"Echo the Credentials\"\n\t\ta.Command = fmt.Sprintf(\"echo %s\", t.Credentials)\n\t\tactions = append(actions, a)\n\n\t\treturn\n\tdefault:\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "plugin/kubeadm/kubeadm.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/plunder-app/plunder/pkg/parlay/parlaytypes\"\n)\n\nconst pluginInfo = `This plugin is used to managed kubeadm automation`\n\n// This defines the etcd kubeadm file (should use the kubernetes packages to define at a later point)\nconst etcdKubeadm = `apiVersion: \"kubeadm.k8s.io/%s\"\nkind: ClusterConfiguration\netcd:\n    local:\n        serverCertSANs:\n        - \"%s\"\n        peerCertSANs:\n        - \"%s\"\n        extraArgs:\n            initial-cluster: %s=https://%s:2380,%s=https://%s:2380,%s=https://%s:2380\n            initial-cluster-state: new\n            name: %s\n            listen-peer-urls: https://%s:2380\n            listen-client-urls: https://%s:2379\n            advertise-client-urls: https://%s:2379\n            initial-advertise-peer-urls: https://%s:2380`\n\n// This defines the manager kubeadm file (should use the kubernetes packages to define at a later point)\n\nconst managerKubeadm = `apiVersion: kubeadm.k8s.io/v1beta1\nkind: ClusterConfiguration\nkubernetesVersion: %s\napiServer:\n  certSANs:\n  - \"%s\"\ncontrolPlaneEndpoint: \"%s:%d\"\netcd:\n    external:\n        endpoints:\n        - https://%s:2379\n        - https://%s:2379\n        - https://%s:2379\n        caFile: /etc/kubernetes/pki/etcd/ca.crt\n        certFile: /etc/kubernetes/pki/apiserver-etcd-client.crt\n        keyFile: /etc/kubernetes/pki/apiserver-etcd-client.key`\n\ntype etcdMembers struct {\n\t// Hostnames\n\tHostname1 string `json:\"hostname1,omitempty\"`\n\tHostname2 string `json:\"hostname2,omitempty\"`\n\tHostname3 string `json:\"hostname3,omitempty\"`\n\n\t// Addresses\n\tAddress1 string `json:\"address1,omitempty\"`\n\tAddress2 string `json:\"address2,omitempty\"`\n\tAddress3 string `json:\"address3,omitempty\"`\n\n\t// Intialise a Certificate Authority\n\tInitCA bool `json:\"initCA,omitempty\"`\n\n\t// Set kubernetes API version\n\tAPIVersion string `json:\"apiversion,omitempty\"`\n}\n\ntype managerMembers struct {\n\t// ETCD Nodes\n\tETCDAddress1 string `json:\"etcd01,omitempty\"`\n\tETCDAddress2 string `json:\"etcd02,omitempty\"`\n\tETCDAddress3 string `json:\"etcd03,omitempty\"`\n\n\t// Version of Kubernetes\n\tVersion string `json:\"kubeVersion,omitempty\"`\n\n\t// Load Balancer details (needed for initialising the first master)\n\t//loadBalancer\n\n\t// Stacked - means ETCD nodes are stacked on managers (false by default)\n\tStacked bool `json:\"stacked,omitempty\"`\n}\n\n// Dummy main function\nfunc main() {}\n\n// ParlayActionList - This should return an array of actions\nfunc ParlayActionList() []string {\n\treturn []string{\n\t\t\"kubeadm/etcd\",\n\t\t\"kubeadm/master\"}\n}\n\n// ParlayActionDetails - This should return an array of action descriptions\nfunc ParlayActionDetails() []string {\n\treturn []string{\n\t\t\"This action automates the provisioning of a the first etcd node and certificates for the remaining two nodes\",\n\t\t\"This action handles the configuration of the first master node\"}\n}\n\n// ParlayPluginInfo - returns information about the plugin\nfunc ParlayPluginInfo() string {\n\treturn pluginInfo\n}\n\n// ParlayUsage - Returns the json that matches the specific action\n// <- action is a string that defines which action the usage information should be\n// <- raw - raw JSON that will be manipulated into a correct struct that matches the action\n// -> err is any error that has been generated\nfunc ParlayUsage(action string) (raw json.RawMessage, err error) {\n\n\t// This example plugin only has the code for \"exampleAction/test\" however this switch statement\n\t// should handle all exposed actions from the plugin\n\tswitch action {\n\tcase \"kubeadm/etcd\":\n\t\ta := etcdMembers{\n\t\t\tHostname1:  \"etcd01.local\",\n\t\t\tHostname2:  \"etcd02.local\",\n\t\t\tHostname3:  \"etcd03.local\",\n\t\t\tInitCA:     true,\n\t\t\tAPIVersion: \"v1beta1\",\n\t\t\tAddress1:   \"10.0.101\",\n\t\t\tAddress2:   \"10.0.102\",\n\t\t\tAddress3:   \"10.0.103\",\n\t\t}\n\t\t// In order to turn a struct into an map[string]interface we need to turn it into JSON\n\n\t\treturn json.Marshal(a)\n\tdefault:\n\t\treturn raw, fmt.Errorf(\"Action [%s] could not be found\", action)\n\t}\n}\n\n// ParlayExec - Parses the action and the data that the action will consume\n// <- action a string that details the action to be executed\n// <- raw - raw JSON that will be manipulated into a correct struct that matches the action\n// -> actions are an array of generated actions that the parser will then execute\n// -> err is any error that has been generated\nfunc ParlayExec(action, host string, raw json.RawMessage) (actions []parlaytypes.Action, err error) {\n\n\t// This example plugin only has the code for \"exampleAction/test\" however this switch statement\n\t// should handle all exposed actions from the plugin\n\tswitch action {\n\tcase \"kubeadm/etcd\":\n\t\tvar etcdStruct etcdMembers\n\t\t// Unmarshall the JSON into the struct\n\t\terr = json.Unmarshal(raw, &etcdStruct)\n\t\treturn etcdStruct.generateActions(), err\n\tdefault:\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "plugin/kubeadm/kubeadm_actions.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/plunder-app/plunder/pkg/parlay/parlaytypes\"\n)\n\nfunc (e *etcdMembers) generateActions() []parlaytypes.Action {\n\tvar generatedActions []parlaytypes.Action\n\tvar a parlaytypes.Action\n\tif e.InitCA == true {\n\t\t// Ensure that a new Certificate Authority is generated\n\t\t// Create action\n\t\ta = parlaytypes.Action{\n\t\t\t// Generate etcd server certificate\n\t\t\tActionType:  \"command\",\n\t\t\tCommand:     fmt.Sprintf(\"kubeadm init phase certs etcd-ca\"),\n\t\t\tCommandSudo: \"root\",\n\t\t\tName:        \"Initialise Certificate Authority\",\n\t\t}\n\t\tgeneratedActions = append(generatedActions, a)\n\t}\n\n\t// Default to < 1.12 API version\n\tif e.APIVersion == \"\" {\n\t\te.APIVersion = \"v1beta1\"\n\t}\n\t// Generate the configuration directories\n\ta.ActionType = \"command\"\n\ta.Command = fmt.Sprintf(\"mkdir -m 777 -p /tmp/%s/ /tmp/%s/ /tmp/%s/\", e.Address1, e.Address2, e.Address3)\n\ta.Name = \"Generate temporary directories\"\n\tgeneratedActions = append(generatedActions, a)\n\n\t// Generate the kubeadm configuration files\n\n\t// Node 0\n\ta.Name = \"build kubeadm config for node 0\"\n\ta.Command = fmt.Sprintf(\"echo '%s' > /tmp/%s/kubeadmcfg.yaml\", e.buildKubeadm(e.APIVersion, e.Hostname1, e.Address1), e.Address1)\n\tgeneratedActions = append(generatedActions, a)\n\n\t// Node 1\n\ta.Name = \"build kubeadm config for node 1\"\n\ta.Command = fmt.Sprintf(\"echo '%s' > /tmp/%s/kubeadmcfg.yaml\", e.buildKubeadm(e.APIVersion, e.Hostname2, e.Address2), e.Address2)\n\tgeneratedActions = append(generatedActions, a)\n\n\t// Node 2\n\ta.Name = \"build kubeadm config for node 2\"\n\ta.Command = fmt.Sprintf(\"echo '%s' > /tmp/%s/kubeadmcfg.yaml\", e.buildKubeadm(e.APIVersion, e.Hostname3, e.Address3), e.Address3)\n\tgeneratedActions = append(generatedActions, a)\n\n\t// Add certificate actions\n\tgeneratedActions = append(generatedActions, e.generateCertificateActions([]string{e.Address3, e.Address2, e.Address1})...)\n\treturn generatedActions\n}\n\nfunc (e *etcdMembers) buildKubeadm(api, host, address string) string {\n\tvar kubeadm string\n\t// Generates a kubeadm for setting up the etcd yaml\n\tkubeadm = fmt.Sprintf(etcdKubeadm, api, address, address, e.Hostname1, e.Address1, e.Hostname2, e.Address2, e.Hostname3, e.Address3, host, address, address, address, address)\n\treturn kubeadm\n}\n\n// generateCertificateActions - Hosts need adding in backward to the array i.e. host 2 -> host 1 -> host 0\nfunc (e *etcdMembers) generateCertificateActions(hosts []string) []parlaytypes.Action {\n\tvar generatedActions []parlaytypes.Action\n\tvar a parlaytypes.Action\n\n\ta.Command = \"mkdir -p /etc/kubernetes/pki\"\n\ta.CommandSudo = \"root\"\n\ta.Name = \"Ensure that PKI directory exists\"\n\ta.ActionType = \"command\"\n\tgeneratedActions = append(generatedActions, a)\n\n\tfor i, v := range hosts {\n\t\t// Tidy any existing client certificates\n\t\ta.ActionType = \"command\"\n\t\ta.Command = \"find /etc/kubernetes/pki -not -name ca.crt -not -name ca.key -type f -delete\"\n\t\ta.Name = \"Remove any existing client certificates before attempting to generate any new ones\"\n\t\tgeneratedActions = append(generatedActions, a)\n\n\t\t// Generate etcd server certificate\n\t\ta.ActionType = \"command\"\n\t\ta.Command = fmt.Sprintf(\"kubeadm init phase certs etcd-server --config=/tmp/%s/kubeadmcfg.yaml\", v)\n\t\ta.Name = fmt.Sprintf(\"Generate etcd server certificate for [%s]\", v)\n\t\tgeneratedActions = append(generatedActions, a)\n\n\t\t// Generate peer certificate\n\t\ta.Command = fmt.Sprintf(\"kubeadm init phase certs etcd-peer --config=/tmp/%s/kubeadmcfg.yaml\", v)\n\t\ta.Name = fmt.Sprintf(\"Generate peer certificate for [%s]\", v)\n\t\tgeneratedActions = append(generatedActions, a)\n\n\t\t// Generate health check certificate\n\t\ta.Command = fmt.Sprintf(\"kubeadm init phase certs etcd-healthcheck-client --config=/tmp/%s/kubeadmcfg.yaml\", v)\n\t\ta.Name = fmt.Sprintf(\"Generate health check certificate for [%s]\", v)\n\t\tgeneratedActions = append(generatedActions, a)\n\n\t\t// Generate api-server client certificate\n\t\ta.Command = fmt.Sprintf(\"kubeadm init phase certs apiserver-etcd-client --config=/tmp/%s/kubeadmcfg.yaml\", v)\n\t\ta.Name = fmt.Sprintf(\"Generate api-server client certificate for [%s]\", v)\n\t\tgeneratedActions = append(generatedActions, a)\n\n\t\t// These steps are only required for the first two hosts\n\t\tif i != (len(hosts) - 1) {\n\t\t\t// Archive the certificates and the kubeadm configuration in a host specific archive name\n\t\t\ta.Command = fmt.Sprintf(\"tar -cvzf /tmp/%s.tar.gz $(find /etc/kubernetes/pki -type f) /tmp/%s/kubeadmcfg.yaml\", v, v)\n\t\t\ta.Name = fmt.Sprintf(\"Archive generated certificates [%s]\", v)\n\t\t\tgeneratedActions = append(generatedActions, a)\n\n\t\t\t// Download the archive files to the local machine\n\t\t\ta.ActionType = \"download\"\n\t\t\ta.Source = fmt.Sprintf(\"/tmp/%s.tar.gz\", hosts[i])\n\t\t\ta.Destination = fmt.Sprintf(\"/tmp/%s.tar.gz\", hosts[i])\n\t\t\ta.Name = fmt.Sprintf(\"Retrieve the certificate bundle for [%s]\", v)\n\t\t\tgeneratedActions = append(generatedActions, a)\n\t\t} else {\n\t\t\t// This is the final host, grab the certificates for use by a manager\n\t\t\ta.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\")\n\t\t\ta.Name = fmt.Sprintf(\"Archive generated certificates [%s]\", v)\n\t\t\tgeneratedActions = append(generatedActions, a)\n\n\t\t\t// Download the archive files to the local machine\n\t\t\ta.ActionType = \"download\"\n\t\t\ta.Source = \"/tmp/managercert.tar.gz\"\n\t\t\ta.Destination = \"/tmp/managercert.tar.gz\"\n\t\t\ta.Name = \"Retrieving the Certificates for the manager nodes\"\n\t\t\tgeneratedActions = append(generatedActions, a)\n\t\t}\n\t}\n\treturn generatedActions\n}\n\n// At some point the functions for the various kubeadm arease will be split into seperate files to ease management\nfunc (m *managerMembers) generateActions() []parlaytypes.Action {\n\tvar generatedActions []parlaytypes.Action\n\tvar a parlaytypes.Action\n\tif m.Stacked == false {\n\t\t// Not implemented yet TODO\n\t\treturn nil\n\t}\n\n\t// Upload the initial etcd certificates to the first manager node\n\ta = parlaytypes.Action{\n\t\t// Upload etcd server certificate\n\t\tActionType:  \"upload\",\n\t\tSource:      \"/tmp/managercert.tar.gz\",\n\t\tDestination: \"/tmp/managercert.tar.gz\",\n\t\tName:        \"Upload etcd server certificate to first manager\",\n\t}\n\tgeneratedActions = append(generatedActions, a)\n\n\t// Install the certificates for etcd\n\ta.Name = \"Installing the etcd certificates\"\n\ta.ActionType = \"command\"\n\ta.CommandSudo = \"root\"\n\ta.Command = fmt.Sprintf(\"tar -xvzf /tmp/managercert.tar.gz -C /\")\n\tgeneratedActions = append(generatedActions, a)\n\n\t// Generate the kubeadm configuration file\n\ta.Name = \"Generating the Kubeadm file for the first manager node\"\n\ta.Command = fmt.Sprintf(\"echo '%s' > /tmp/kubeadmcfg.yaml\", m.buildKubeadm())\n\tgeneratedActions = append(generatedActions, a)\n\n\t// Initialise the first node\n\ta.Name = \"Initialise the first control plane node\"\n\ta.Command = \"kubeadm init --config /tmp/kubeadmcfg.yaml\"\n\tgeneratedActions = append(generatedActions, a)\n\n\treturn generatedActions\n}\n\nfunc (m *managerMembers) buildKubeadm() string {\n\tvar kubeadm string\n\t// Generates a kubeadm for setting up the etcd yaml\n\tkubeadm = fmt.Sprintf(managerKubeadm, m.Version, \"LB HOSTNAME FIXME\", \"LB HOSTNAME FIXME\", 1000000, m.ETCDAddress1, m.ETCDAddress2, m.ETCDAddress3)\n\treturn kubeadm\n}\n"
  },
  {
    "path": "testing.sh",
    "content": "#!/bin/bash\n\necho \"This script will step through a number of tests agains plunder to ensure that functionality is as expected\"\necho \"Building plunder with [go build]\"\n\nINSECURE=\"-k\"\nPLUNDERURL=\"https://localhost:60443\"\n\ngo build\n\nretVal=$?\nif [ $retVal -ne 0 ]; then\n    echo \"Error at go build\"\n    exit\nfi\n\necho \"Check for no version information\"\nv=$(./plunder version | grep Version | awk '{ print $2 }')\nrm plunder\n\nif [ -z \"$v\" ]\nthen\n      echo \"Version is empty\"\nelse\n      echo \"Version is NOT empty\"\nfi\n\necho \"Building plunder with [make build]\"\n\nmake build\n\nretVal=$?\nif [ $retVal -ne 0 ]; then\n    echo \"Error at make build\"\n    exit\nfi\n\necho \"Check for version information\"\nv=$(./plunder version | grep Version | awk '{ print $2 }')\nif [ -z \"$v\" ]\nthen\n      echo \"Version is empty\"\nelse\n      echo \"Version is NOT empty [$v]\"\nfi\n\necho \"Plunder server configuration, temporary output will live in ./testing\"\nmkdir testing\n./plunder config server -p > ./testing/server_test_config.json\n./plunder config deployment -p > ./testing/deployment_config.json\n./plunder config server -o yaml > ./testing/server_test_config.yaml\n./plunder config deployment -o yaml > ./testing/deployment_config.yaml\necho \"Generating API Server certificates in ~./plunderserver.yaml\"\n./plunder config apiserver server\n\necho \"Creating alternative configuration with services enabled\"\nsed '/enableHTTP/s/false/true/' ./testing/server_test_config.json > ./testing/server_test_http_config.json\n\n\necho \"Examining detected configuration\"\necho \"Checking for Adapter\"\nv=$(grep adapter ./testing/server_test_config.json | awk ' {print $2 }' | tr -d '\"' | tr -d ',')\nif [ -z \"$v\" ]\nthen\n      echo \"Adapter is empty\"\nelse\n      echo \"Adapter is NOT empty [$v]\"\nfi\n\necho \"Checking for Gateway Address\"\nv=$(grep gatewayDHCP ./testing/server_test_config.json | awk ' {print $2 }' | tr -d '\"' | tr -d ',')\nif [ -z \"$v\" ]\nthen\n      echo \"Gateway is empty\"\nelse\n      echo \"Gateway is NOT empty [$v]\"\nfi\n\ni=$(id -u)\nn=$(id -un)\nif [[ $i -gt 0 ]]\nthen\n      echo \"Testing as current user [NAME = $n / ID = $i]\"\n      echo \"Starting with disabled configuration\"\n      ./plunder server --config ./testing/server_test_config.json\n      retVal=$?\n      if [ $retVal -ne 0 ]; then\n          echo \"Plunder correctly didn't start\"\n      fi\n      echo \"Starting with enabled HTTP configuration (check OSX)\"\n      sudo ./plunder server --config ./testing/server_test_http_config.json &\n      retVal=$?\n      if [ $retVal -ne 0 ]; then\n          echo \"Plunder correctly didn't start\"\n          exit 1\n      fi\n      echo \"Sleeping for 3 seconds to ensure plunder has started\"\n      sleep 3\n      echo \"Print Configuration info\"; echo \"--------------------------\"\n      curl $INSECURE $PLUNDERURL/config; echo \"\"\n      echo \"Print Deployments info\"; echo \"--------------------------\"\n      curl $INSECURE $PLUNDERURL/deployments; echo \"\"\n      echo \"POST JSON Deployment to Plunder API\"\n      curl $INSECURE -X POST -d \"@./testing/deployment_config.json\" $PLUNDERURL/deployments\n      echo \"Print (UPDATED) Deployment info\"; echo \"--------------------------\"\n      curl $INSECURE $PLUNDERURL/deployments; echo \"\"\n      echo \"POST YAML Deployment to Plunder API\"\n      curl $INSECURE -X POST --data-binary \"@./testing/deployment_config.yaml\" $PLUNDERURL/deployments -H \"Content-type: text/x-yaml\"\n      echo \"Print (UPDATED) Deployment info\"; echo \"--------------------------\"\n      curl $INSECURE $PLUNDERURL/deployments; echo \"\"\n      sudo kill -9 $( ps -ef | grep -i plunder | grep -v -e 'sudo' -e 'grep' | awk '{ print $2 }')     \n      wait $! 2>/dev/null\n      sleep 1\nelse \n      echo \"Skipping permission tests as running as root\"\nfi\n\necho \"The following tests rely on sudo, with NOPASSWD enabled\"\n\necho \"Starting with disabled configuration\"\n\nretVal=$?\nif [ $retVal -ne 0 ]; then\n    echo \"Error at make build\"\n    exit\nfi\n\necho \"To remote [./testing/] directory, and [./plunder] binary\"\necho \"rm -rf ./testing/ ./plunder\""
  }
]