├── .gitignore ├── CODEOWNERS ├── LICENSE ├── Makefile ├── README.md ├── assets ├── example.png └── mount.png ├── build └── docker │ └── doryd │ ├── Dockerfile │ └── Dockerfile.staged ├── cmd ├── dory │ ├── dory.go │ ├── dory.json │ ├── dory_test.go │ └── test │ │ ├── broken.json │ │ ├── errors.json │ │ ├── flipped.json │ │ └── good.json └── doryd │ ├── Dockerfile │ └── doryd.go ├── common ├── chain │ ├── chain.go │ └── chain_test.go ├── connectivity │ ├── client.go │ └── client_test.go ├── docker │ ├── dockerlt │ │ ├── dockerlt.go │ │ └── plugin.go │ └── dockervol │ │ └── dockervol.go ├── jconfig │ ├── broken.json │ ├── jconfig.go │ ├── jconfig_test.go │ └── test.json ├── k8s │ ├── flexvol │ │ ├── flexvol.go │ │ ├── handler.go │ │ └── handler_test.go │ └── provisioner │ │ ├── claim.go │ │ ├── claim_test.go │ │ ├── class.go │ │ ├── monitor.go │ │ ├── provisioner.go │ │ ├── provisioner_test.go │ │ └── volume.go ├── linux │ ├── bmount.go │ ├── bmount_test.go │ ├── mount.go │ └── selinux.go ├── model │ └── types.go └── util │ ├── cmd.go │ ├── cmd_test.go │ ├── file.go │ ├── logger.go │ └── logger_test.go ├── docs ├── README.md ├── dory │ └── README.md ├── doryd │ └── README.md └── plugins │ └── README.md ├── examples ├── dep-doryd-1.8.yaml ├── dep-doryd.yaml └── ds-doryd.yaml ├── glide.lock └── glide.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | 4 | *.test 5 | *.out 6 | 7 | /pkg/* 8 | /bin/* 9 | /src/github.com/* 10 | 11 | .vscode 12 | 13 | cmd/dory/dory 14 | vendor/* 15 | 16 | dory 17 | dory.sha256sum 18 | doryd 19 | doryd.sha256sum 20 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @raunakkumar @rgcostea @suneeth51 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and 27 | configuration files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object 31 | code, generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, 34 | made available under the License, as indicated by a copyright notice that is 35 | included in or attached to the work (an example is provided in the Appendix 36 | below). 37 | 38 | "Derivative Works" shall mean any work, whether in Source or Object form, that 39 | is based on (or derived from) the Work and for which the editorial revisions, 40 | annotations, elaborations, or other modifications represent, as a whole, an 41 | original work of authorship. For the purposes of this License, Derivative Works 42 | shall not include works that remain separable from, or merely link (or bind by 43 | name) to the interfaces of, the Work and Derivative Works thereof. 44 | 45 | "Contribution" shall mean any work of authorship, including the original 46 | version of the Work and any modifications or additions to that Work or 47 | Derivative Works thereof, that is intentionally submitted to Licensor for 48 | inclusion in the Work by the copyright owner or by an individual or Legal 49 | Entity authorized to submit on behalf of the copyright owner. For the purposes 50 | of this definition, "submitted" means any form of electronic, verbal, or 51 | written communication sent to the Licensor or its representatives, including 52 | but not limited to communication on electronic mailing lists, source code 53 | control systems, and issue tracking systems that are managed by, or on behalf 54 | of, the Licensor for the purpose of discussing and improving the Work, but 55 | excluding communication that is conspicuously marked or otherwise designated in 56 | writing by the copyright owner as "Not a Contribution." 57 | 58 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 59 | of whom a Contribution has been received by Licensor and subsequently 60 | incorporated within the Work. 61 | 62 | 2. Grant of Copyright License. 63 | 64 | Subject to the terms and conditions of this License, each Contributor hereby 65 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 66 | irrevocable copyright license to reproduce, prepare Derivative Works of, 67 | publicly display, publicly perform, sublicense, and distribute the Work and 68 | such Derivative Works in Source or Object form. 69 | 70 | 3. Grant of Patent License. 71 | 72 | Subject to the terms and conditions of this License, each Contributor hereby 73 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 74 | irrevocable (except as stated in this section) patent license to make, have 75 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 76 | such license applies only to those patent claims licensable by such Contributor 77 | that are necessarily infringed by their Contribution(s) alone or by combination 78 | of their Contribution(s) with the Work to which such Contribution(s) was 79 | submitted. If You institute patent litigation against any entity (including a 80 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 81 | Contribution incorporated within the Work constitutes direct or contributory 82 | patent infringement, then any patent licenses granted to You under this License 83 | for that Work shall terminate as of the date such litigation is filed. 84 | 85 | 4. Redistribution. 86 | 87 | You may reproduce and distribute copies of the Work or Derivative Works thereof 88 | in any medium, with or without modifications, and in Source or Object form, 89 | provided that You meet the following conditions: 90 | 91 | You must give any other recipients of the Work or Derivative Works a copy of 92 | this License; and 93 | You must cause any modified files to carry prominent notices stating that You 94 | changed the files; and 95 | You must retain, in the Source form of any Derivative Works that You 96 | distribute, all copyright, patent, trademark, and attribution notices from the 97 | Source form of the Work, excluding those notices that do not pertain to any 98 | part of the Derivative Works; and 99 | If the Work includes a "NOTICE" text file as part of its distribution, then any 100 | Derivative Works that You distribute must include a readable copy of the 101 | attribution notices contained within such NOTICE file, excluding those notices 102 | that do not pertain to any part of the Derivative Works, in at least one of the 103 | following places: within a NOTICE text file distributed as part of the 104 | Derivative Works; within the Source form or documentation, if provided along 105 | with the Derivative Works; or, within a display generated by the Derivative 106 | Works, if and wherever such third-party notices normally appear. The contents 107 | of the NOTICE file are for informational purposes only and do not modify the 108 | License. You may add Your own attribution notices within Derivative Works that 109 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 110 | provided that such additional attribution notices cannot be construed as 111 | modifying the License. 112 | You may add Your own copyright statement to Your modifications and may provide 113 | additional or different license terms and conditions for use, reproduction, or 114 | distribution of Your modifications, or for any such Derivative Works as a 115 | whole, provided Your use, reproduction, and distribution of the Work otherwise 116 | complies with the conditions stated in this License. 117 | 118 | 5. Submission of Contributions. 119 | 120 | Unless You explicitly state otherwise, any Contribution intentionally submitted 121 | for inclusion in the Work by You to the Licensor shall be under the terms and 122 | conditions of this License, without any additional terms or conditions. 123 | Notwithstanding the above, nothing herein shall supersede or modify the terms 124 | of any separate license agreement you may have executed with Licensor regarding 125 | such Contributions. 126 | 127 | 6. Trademarks. 128 | 129 | This License does not grant permission to use the trade names, trademarks, 130 | service marks, or product names of the Licensor, except as required for 131 | reasonable and customary use in describing the origin of the Work and 132 | reproducing the content of the NOTICE file. 133 | 134 | 7. Disclaimer of Warranty. 135 | 136 | Unless required by applicable law or agreed to in writing, Licensor provides 137 | the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 138 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 139 | including, without limitation, any warranties or conditions of TITLE, 140 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 141 | solely responsible for determining the appropriateness of using or 142 | redistributing the Work and assume any risks associated with Your exercise of 143 | permissions under this License. 144 | 145 | 8. Limitation of Liability. 146 | 147 | In no event and under no legal theory, whether in tort (including negligence), 148 | contract, or otherwise, unless required by applicable law (such as deliberate 149 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 150 | liable to You for damages, including any direct, indirect, special, incidental, 151 | or consequential damages of any character arising as a result of this License 152 | or out of the use or inability to use the Work (including but not limited to 153 | damages for loss of goodwill, work stoppage, computer failure or malfunction, 154 | or any and all other commercial damages or losses), even if such Contributor 155 | has been advised of the possibility of such damages. 156 | 157 | 9. Accepting Warranty or Additional Liability. 158 | 159 | While redistributing the Work or Derivative Works thereof, You may choose to 160 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 161 | other liability obligations and/or rights consistent with this License. 162 | However, in accepting such obligations, You may act only on Your own behalf and 163 | on Your sole responsibility, not on behalf of any other Contributor, and only 164 | if You agree to indemnify, defend, and hold each Contributor harmless for any 165 | liability incurred by, or claims asserted against, such Contributor by reason 166 | of your accepting any such warranty or additional liability. 167 | 168 | END OF TERMS AND CONDITIONS 169 | 170 | APPENDIX: How to apply the Apache License to your work 171 | 172 | To apply the Apache License to your work, attach the following boilerplate 173 | notice, with the fields enclosed by brackets "[]" replaced with your own 174 | identifying information. (Don't include the brackets!) The text should be 175 | enclosed in the appropriate comment syntax for the file format. We also 176 | recommend that a file or class name and description of purpose be included on 177 | the same "printed page" as the copyright notice for easier identification 178 | within third-party archives. 179 | 180 | Copyright [yyyy] [name of copyright owner] 181 | 182 | Licensed under the Apache License, Version 2.0 (the "License"); 183 | you may not use this file except in compliance with the License. 184 | You may obtain a copy of the License at 185 | 186 | http://www.apache.org/licenses/LICENSE-2.0 187 | 188 | Unless required by applicable law or agreed to in writing, software 189 | distributed under the License is distributed on an "AS IS" BASIS, 190 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 191 | See the License for the specific language governing permissions and 192 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # (c) Copyright 2017 Hewlett Packard Enterprise Development LP 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | VERSION = 1.1.0 16 | 17 | # Where our code lives 18 | PKG_PATH = ./common/ 19 | CMD_PATH = ./cmd/ 20 | 21 | # This is the last 8 char of the commit id we're building from 22 | COMMIT = $(shell git rev-parse HEAD| cut -b-8) 23 | 24 | # The version of make for OSX doesn't allow us to export, so 25 | # we add these variables to the env in each invocation. 26 | GOENV = GOPATH=$(GOPATH) PATH=$$PATH:$(GOPATH)/bin 27 | 28 | # Our target binary is for Linux. To build an exec for your local (non-linux) 29 | # machine, use go build directly. 30 | ifndef GOOS 31 | TEST_ENV = GOOS=linux GOARCH=amd64 32 | else 33 | TEST_ENV = GOOS=$(GOOS) GOARCH=amd64 34 | endif 35 | BUILD_ENV = GOOS=linux GOARCH=amd64 CGO_ENABLED=0 36 | 37 | # Add the version and hg commit id to the binary in the form of variables. 38 | LD_FLAGS = '-X main.Version=$(VERSION) -X main.Commit=$(COMMIT)' 39 | 40 | # gometalinter allows us to have a single target that runs multiple linters in 41 | # the same fashion. This variable controls which linters are used. 42 | LINTER_FLAGS = --vendor --disable-all --enable=vet --enable=vetshadow --enable=golint --enable=ineffassign --enable=goconst --enable=deadcode --enable=dupl --enable=varcheck --enable=gocyclo --enable=misspell --deadline=300s 43 | 44 | # list of packages 45 | PACKAGE_LIST = $(shell export $(GOENV) && go list ./$(PKG_PATH)...| grep -v vendor) 46 | # list of commands 47 | COMMAND_LIST = $(shell export $(GOENV) && go list ./$(CMD_PATH)...) 48 | 49 | # prefixes to make things pretty 50 | A1 = $(shell printf "»") 51 | A2 = $(shell printf "»»") 52 | A3 = $(shell printf "»»»") 53 | 54 | .PHONY: help 55 | help: 56 | @echo "Targets:" 57 | @echo " tools - Download and install go tooling required to build." 58 | @echo " vendor - Download dependancies." 59 | @echo " lint - Static analysis of source code. Note that this must pass in order to build." 60 | @echo " test - Run unit tests." 61 | @echo " clean - Remove binaries." 62 | @echo " debug - Display make's view of the world." 63 | @echo " dory - Build dory (FlexVolume driver)." 64 | @echo " doryd - Build doryd (Provisioner)." 65 | @echo " doryd_docker - Build doryd (Provisioner) docker image." 66 | 67 | .PHONY: debug 68 | debug: 69 | @echo "Debug:" 70 | @echo " packages: $(PACKAGE_LIST)" 71 | @echo " commands: $(COMMAND_LIST)" 72 | @echo " COMMIT: $(COMMIT)" 73 | @echo " GOPATH: $(GOPATH)" 74 | @echo " LD_FLAGS: $(LD_FLAGS)" 75 | @echo " BUILD_ENV: $(BUILD_ENV)" 76 | @echo " GOENV: $(GOENV)" 77 | 78 | tools: ; $(info $(A1) tools) 79 | @echo "$(A2) get gometalinter" 80 | export $(GOENV) && go get -u github.com/alecthomas/gometalinter 81 | @echo "$(A2) install gometalinter" 82 | export $(GOENV) && gometalinter --install 83 | @echo "$(A2) get glide" 84 | export $(GOENV) && go get -u github.com/Masterminds/glide 85 | export $(GOENV) && go install github.com/Masterminds/glide 86 | 87 | vendor: tools; $(info $(A1) vendor) 88 | @echo "$(A2) glide install" 89 | export $(GOENV) && glide install 90 | 91 | .PHONY: lint 92 | lint: ; $(info $(A1) lint) 93 | @echo "$(A2) lint $(CMD_PATH)" 94 | export $(GOENV) $(BUILD_ENV); gometalinter $(LINTER_FLAGS) $(CMD_PATH)... 95 | @echo "$(A2) lint $(PKG_PATH)" 96 | export $(GOENV) $(BUILD_ENV); gometalinter $(LINTER_FLAGS) $(PKG_PATH)... 97 | 98 | .PHONY: clean 99 | clean: ; $(info $(A1) clean) 100 | @echo "$(A2) remove dory" 101 | @rm -f dory 102 | @rm -f dory.sha256sum 103 | @echo "$(A2) remove doryd" 104 | @rm -f doryd 105 | @rm -f doryd.sha256sum 106 | 107 | .PHONY: test 108 | test: ; $(info $(A1) test) 109 | @echo "$(A2) Package unit tests" 110 | for pkg in $(PACKAGE_LIST); do echo "»»» Testing $$pkg:" && export $(GOENV) $(TEST_ENV) && go test -cover $$pkg; done 111 | @echo "$(A2) Command unit tests" 112 | for cmd in $(COMMAND_LIST); do echo "»»» Testing $$cmd:" && export $(GOENV) $(TEST_ENV) && go test -cover $$cmd; done 113 | 114 | dory: lint; $(info $(A1) dory) 115 | @echo "$(A2) build dory" 116 | export $(GOENV) $(BUILD_ENV) && go build -ldflags $(LD_FLAGS) $(CMD_PATH)dory/dory.go 117 | @echo "$(A2) sha256sum dory" 118 | sha256sum dory > dory.sha256sum 119 | @cat dory.sha256sum 120 | 121 | doryd: lint; $(info $(A1) dory) 122 | @echo "$(A2) build doryd" 123 | export $(GOENV) $(BUILD_ENV) && go build -ldflags $(LD_FLAGS) $(CMD_PATH)doryd/doryd.go 124 | @echo "$(A2) sha256sum doryd" 125 | sha256sum doryd > doryd.sha256sum 126 | @cat doryd.sha256sum 127 | 128 | .PHONY: doryd_docker 129 | doryd_docker: doryd; $(info $(A1) doryd_docker) 130 | @echo "$(A2) rm current doryd image" 131 | -docker image rm kube-storage-controller-dory:edge 132 | @echo "$(A2) build doryd image" 133 | docker build -t kube-storage-controller-dory:edge -f ./build/docker/doryd/Dockerfile . 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated: This repository no longer actively maintained 2 | 3 | Please use the following repositories [Flexvolume Driver](https://github.com/hpe-storage/flexvolume-driver) and [Kubernetes Dynamic Provisioner](https://github.com/hpe-storage/k8s-dynamic-provisioner) 4 | 5 | ## Kubernetes Flexvolume Driver and StorageClass Provisioner for Docker Volume Plugins 6 | 7 | Repository for Dory and Doryd: The [FlexVolume](https://kubernetes.io/docs/concepts/storage/volumes/#out-of-tree-volume-plugins) driver and [Dynamic Volume Provisioner](https://kubernetes.io/docs/concepts/storage/dynamic-provisioning/) for Kubernetes using *any* Docker Volume API compatible plugin. This is [Open Source Software](LICENSE) from [HPE DEV](https://developer.hpe.com). 8 | 9 | ## Dory 10 | 11 | Dory is a driver for the [Kubernetes FlexVolume](https://github.com/kubernetes/community/blob/master/contributors/devel/flexvolume.md) Volume type. This driver translates Flexvolume requests to [Docker Volume Plugin](https://docs.docker.com/engine/extend/plugins_volume/) requests. This allows the administrator to leverage [existing legacy Docker Volume Plugins](https://docs.docker.com/engine/extend/legacy_plugins/) or [existing managed Docker Volume Plugins](https://store.docker.com/search?category=volume&q=&type=plugin) in a Kubernetes cluster. Managed plugins require Docker 1.13. Dory provides the ability to 'just in time' provision storage as well as have the orchestrator automatically attach/mount and detach/unmount Persistent Volumes. 12 | 13 | * Dory [documentation](docs/dory/README.md) 14 | * Binary releases: 15 | * [Master](http://dl.bintray.com/hpe-storage/dory/dory-master) (latest) 16 | * [1.0](http://dl.bintray.com/hpe-storage/dory/dory-1.0) 17 | 18 | ## Doryd 19 | 20 | Doryd is a implementation of the [Out-of-tree provisioner](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/volume-provisioning.md) that dynamically provisions persistent storage using the [Docker Volume Plugin API](https://docs.docker.com/engine/extend/plugins_volume/). 21 | 22 | * Doryd [documentation](docs/doryd/README.md) 23 | * Binary releases: 24 | * [Master](http://dl.bintray.com/hpe-storage/dory/doryd-master) (latest) 25 | * Container image: 26 | * [doryd](https://hub.docker.com/r/nimblestorage/doryd/) 27 | 28 | ## Plugins 29 | 30 | To better help end-users navigate around the storage landcape we've composed a page to help keep track of what Docker Volume plugins are known to work well and some record keeping on issues/gotchas with the specific plugin. 31 | 32 | * [Plugins known to work](docs/plugins/README.md) 33 | 34 | ## Project 35 | 36 | Why is the project called Dory? Because [Dory speaks whale](https://www.google.com/search?q=Dory+speaks+whale). 37 | 38 | What about the [Container Storage Interface](https://github.com/container-storage-interface/)? The CSI is certainly the future for container storage. Dory provides a stop gap while the CSI specification is ratified, orchestrators begin supporting it, and implementations begin to surface. 39 | 40 | ## Thanks 41 | 42 | Thank you to [Chakravarthy Nelluri](https://github.com/chakri-nelluri) for all his work on [Flexvolume](https://github.com/kubernetes/kubernetes/commit/fa76de79e5d1670b8e6add30f0159c833534a298#diff-af00671c74d885ce20891c24516198e8) which has made this 'out of tree' work possible. 43 | 44 | Thank you to [Michael Mattsson](https://community.hpe.com/t5/user/viewprofilepage/user-id/1879662), TME extraordinaire, for his help testing and for writing a [blog post](https://community.hpe.com/t5/HPE-Nimble-Storage-Tech-Blog/Tech-Preview-Bringing-Nimble-Storage-to-Kubernetes-and-OpenShift/ba-p/6986748) about what we were up to. 45 | 46 | ## Licensing 47 | 48 | Dory and Doryd is licensed under the Apache License, Version 2.0. Please see [LICENSE](LICENSE) for the full license text. 49 | -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpe-storage/dory/9adae91dd705464031423778489a7d5fb8963bd8/assets/example.png -------------------------------------------------------------------------------- /assets/mount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpe-storage/dory/9adae91dd705464031423778489a7d5fb8963bd8/assets/mount.png -------------------------------------------------------------------------------- /build/docker/doryd/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | ADD [ "doryd", "/usr/local/bin/doryd" ] 3 | ENTRYPOINT [ "doryd" ] 4 | CMD [ "/etc/kubernetes/admin.conf", "dev.hpe.com" ] 5 | -------------------------------------------------------------------------------- /build/docker/doryd/Dockerfile.staged: -------------------------------------------------------------------------------- 1 | FROM golang as builder 2 | RUN git clone https://github.com/hpe-storage/dory 3 | WORKDIR dory 4 | RUN make gettools && \ 5 | make vendor && \ 6 | make doryd 7 | 8 | FROM alpine:latest 9 | COPY --from=builder /go/dory/bin/doryd /usr/local/bin/doryd 10 | ENTRYPOINT [ "doryd" ] 11 | CMD [ "/etc/kubernetes/admin.conf", "dev.hpe.com" ] 12 | -------------------------------------------------------------------------------- /cmd/dory/dory.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "github.com/hpe-storage/dory/common/docker/dockervol" 22 | "github.com/hpe-storage/dory/common/jconfig" 23 | flexvol "github.com/hpe-storage/dory/common/k8s/flexvol" 24 | "github.com/hpe-storage/dory/common/util" 25 | "os" 26 | "path/filepath" 27 | ) 28 | 29 | const ( 30 | cmdConfigChk = "config" 31 | //override options 32 | optDockerVolumePluginSocketPath = "dockerVolumePluginSocketPath" 33 | optStripK8sFromOptions = "stripK8sFromOptions" 34 | optLogFilePath = "logFilePath" 35 | optDebug = "logDebug" 36 | optCreateVolumes = "createVolumes" 37 | optEnable16 = "enable1.6" 38 | optFactorForConversion = "factorForConversion" 39 | optListOfStorageResourceOptions = "listOfStorageResourceOptions" 40 | optSupportsCapabilities = "supportsCapabilities" 41 | ) 42 | 43 | var ( 44 | // Version contains the current version added by the build process 45 | Version = "dev" 46 | // Commit contains the commit id added by the build process 47 | Commit = "unknown" 48 | 49 | dockerVolumePluginSocketPath = "/run/docker/plugins/nimble.sock" 50 | stripK8sFromOptions = true 51 | logFilePath = "/var/log/dory.log" 52 | debug = false 53 | createVolumes = true 54 | enable16 = false 55 | factorForConversion = 1073741824 56 | listOfStorageResourceOptions = []string{"size", "sizeInGiB"} 57 | supportsCapabilities = true 58 | ) 59 | 60 | func main() { 61 | if len(os.Args) < 2 { 62 | fmt.Println("Not enough args") 63 | return 64 | } 65 | 66 | driverCommand := os.Args[1] 67 | justCheckConfig := false 68 | if driverCommand == cmdConfigChk { 69 | justCheckConfig = true 70 | } 71 | 72 | overridden := initialize(os.Args[0], justCheckConfig) 73 | if justCheckConfig { 74 | return 75 | } 76 | 77 | util.OpenLogFile(logFilePath, 10, 4, 90, debug) 78 | defer util.CloseLogFile() 79 | pid := os.Getpid() 80 | util.LogInfo.Printf("[%d] entry : Driver=%s Version=%s-%s Socket=%s Overridden=%t", pid, filepath.Base(os.Args[0]), Version, Commit, dockerVolumePluginSocketPath, overridden) 81 | 82 | util.LogInfo.Printf("[%d] request: %s %v", pid, driverCommand, os.Args[2:]) 83 | dockervolOptions := &dockervol.Options{ 84 | SocketPath: dockerVolumePluginSocketPath, 85 | StripK8sFromOptions: stripK8sFromOptions, 86 | CreateVolumes: createVolumes, 87 | ListOfStorageResourceOptions: listOfStorageResourceOptions, 88 | FactorForConversion: factorForConversion, 89 | SupportsCapabilities: supportsCapabilities, 90 | } 91 | err := flexvol.Config(os.Args[0], dockervolOptions) 92 | var mess string 93 | if err != nil { 94 | mess = flexvol.BuildJSONResponse(&flexvol.Response{ 95 | Status: flexvol.FailureStatus, 96 | Message: fmt.Sprintf("Unable to communicate with docker volume plugin - %s", err.Error())}) 97 | } else { 98 | mess = flexvol.Handle(driverCommand, enable16, os.Args[2:]) 99 | } 100 | util.LogInfo.Printf("[%d] reply : %s %v: %v", pid, driverCommand, os.Args[2:], mess) 101 | 102 | fmt.Println(mess) 103 | } 104 | 105 | func initialize(name string, report bool) bool { 106 | override := false 107 | 108 | // don't log anything in initialize because we haven't open a log file yet. 109 | filePath := fmt.Sprintf("%s%s", name, ".json") 110 | c, err := jconfig.NewConfig(filePath) 111 | if err != nil { 112 | if report { 113 | fmt.Printf("Error processing %s - %s\n", filePath, err.Error()) 114 | } 115 | return false 116 | } 117 | 118 | s, err := c.GetStringWithError(optLogFilePath) 119 | if err == nil && s != "" { 120 | override = true 121 | logFilePath = s 122 | } else { 123 | configOptCheck(report, optLogFilePath, err) 124 | } 125 | 126 | s, err = c.GetStringWithError(optDockerVolumePluginSocketPath) 127 | if err == nil && s != "" { 128 | override = true 129 | dockerVolumePluginSocketPath = s 130 | } else { 131 | configOptCheck(report, optDockerVolumePluginSocketPath, err) 132 | } 133 | 134 | b, err := c.GetBool(optDebug) 135 | if err == nil { 136 | override = true 137 | debug = b 138 | } else { 139 | configOptCheck(report, optDebug, err) 140 | } 141 | 142 | b, err = c.GetBool(optSupportsCapabilities) 143 | if err == nil { 144 | override = true 145 | supportsCapabilities = b 146 | } else { 147 | configOptCheck(report, optSupportsCapabilities, err) 148 | } 149 | 150 | overrideFlexVol := initializeFlexVolOptions(c, report) 151 | if overrideFlexVol { 152 | override = true 153 | } 154 | 155 | return override 156 | } 157 | 158 | func initializeFlexVolOptions(c *jconfig.Config, report bool) bool { 159 | override := false 160 | 161 | b, err := c.GetBool(optStripK8sFromOptions) 162 | if err == nil { 163 | override = true 164 | stripK8sFromOptions = b 165 | } else { 166 | configOptCheck(report, optStripK8sFromOptions, err) 167 | } 168 | 169 | b, err = c.GetBool(optCreateVolumes) 170 | if err == nil { 171 | override = true 172 | createVolumes = b 173 | } else { 174 | configOptCheck(report, optCreateVolumes, err) 175 | } 176 | 177 | ss, err := c.GetStringSliceWithError(optListOfStorageResourceOptions) 178 | if ss != nil { 179 | override = true 180 | listOfStorageResourceOptions = ss 181 | } else { 182 | configOptCheck(report, optListOfStorageResourceOptions, err) 183 | } 184 | 185 | i, err := c.GetInt64SliceWithError(optFactorForConversion) 186 | if err == nil { 187 | override = true 188 | factorForConversion = int(i) 189 | } else { 190 | configOptCheck(report, optFactorForConversion, err) 191 | } 192 | 193 | e16, err := c.GetBool(optEnable16) 194 | if err == nil { 195 | override = true 196 | enable16 = e16 197 | } else { 198 | configOptCheck(report, optEnable16, err) 199 | } 200 | configOptDump(report) 201 | 202 | return override 203 | } 204 | 205 | func configOptCheck(report bool, optName string, err error) { 206 | if report { 207 | fmt.Printf("Error processing option '%s' - %s\n", optName, err.Error()) 208 | } 209 | } 210 | 211 | func configOptDump(report bool) { 212 | if !report { 213 | return 214 | } 215 | fmt.Printf("\nDriver=%s Version=%s-%s\nCurrent Config:\n", filepath.Base(os.Args[0]), Version, Commit) 216 | fmt.Printf("%30s = %s\n", optDockerVolumePluginSocketPath, dockerVolumePluginSocketPath) 217 | fmt.Printf("%30s = %t\n", optStripK8sFromOptions, stripK8sFromOptions) 218 | fmt.Printf("%30s = %s\n", optLogFilePath, logFilePath) 219 | fmt.Printf("%30s = %t\n", optDebug, debug) 220 | fmt.Printf("%30s = %t\n", optCreateVolumes, createVolumes) 221 | fmt.Printf("%30s = %t\n", optEnable16, enable16) 222 | fmt.Printf("%30s = %d\n", optFactorForConversion, factorForConversion) 223 | fmt.Printf("%30s = %v\n", optListOfStorageResourceOptions, listOfStorageResourceOptions) 224 | fmt.Printf("%30s = %t\n", optSupportsCapabilities, supportsCapabilities) 225 | 226 | } 227 | -------------------------------------------------------------------------------- /cmd/dory/dory.json: -------------------------------------------------------------------------------- 1 | { 2 | "logFilePath": "/var/log/dory.log", 3 | "logDebug": false, 4 | "stripK8sFromOptions": true, 5 | "dockerVolumePluginSocketPath": "/run/docker/plugins/nimble.sock", 6 | "createVolumes": true, 7 | "enable1.6": false, 8 | "listOfStorageResourceOptions" : ["size","sizeInGiB"], 9 | "factorForConversion": 1073741824, 10 | "defaultOptions": [{"mountConflictDelay": 30}, {"manager": "k8s"}] 11 | } -------------------------------------------------------------------------------- /cmd/dory/dory_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | var basicTests = []struct { 24 | name string 25 | override bool 26 | dockerVolumePluginSocketPath string 27 | stripK8sFromOptions bool 28 | logFilePath string 29 | debug bool 30 | createVolumes bool 31 | enable16 bool 32 | factorForConversion int 33 | listOfStorageResourceOptions []string 34 | supportsCapabilities bool 35 | }{ 36 | {"test/good", true, "/run/docker/plugins/nimble.sock", true, "/var/log/dory.log", false, true, false, 1073741824, []string{"size", "sizeInGiB"}, true}, 37 | {"test/flipped", true, "nimble", false, "some path", true, false, true, 14, []string{"size", "sizeInGiB", "w", "x", "y", "z"}, false}, 38 | {"test/broken", false, "/run/docker/plugins/nimble.sock", true, "/var/log/dory.log", false, true, false, 1073741824, []string{"size", "sizeInGiB"}, true}, 39 | {"test/errors", true, "21", true, "true", false, true, false, 1073741824, []string{"size", "sizeInGiB"}, true}, 40 | } 41 | 42 | // nolint: gocyclo 43 | func TestConfigFiles(t *testing.T) { 44 | for _, tc := range basicTests { 45 | t.Run(tc.name, func(t *testing.T) { 46 | //reset for each test 47 | dockerVolumePluginSocketPath = "/run/docker/plugins/nimble.sock" 48 | stripK8sFromOptions = true 49 | logFilePath = "/var/log/dory.log" 50 | debug = false 51 | createVolumes = true 52 | enable16 = false 53 | factorForConversion = 1073741824 54 | listOfStorageResourceOptions = []string{"size", "sizeInGiB"} 55 | supportsCapabilities = true 56 | 57 | override := initialize(tc.name, true) 58 | if override != tc.override { 59 | t.Error( 60 | "For", "override", 61 | "expected", tc.override, 62 | "got:", override, 63 | ) 64 | } 65 | if dockerVolumePluginSocketPath != tc.dockerVolumePluginSocketPath { 66 | t.Error( 67 | "For", "dockerVolumePluginSocketPath", 68 | "expected", tc.dockerVolumePluginSocketPath, 69 | "got:", dockerVolumePluginSocketPath, 70 | ) 71 | } 72 | if stripK8sFromOptions != tc.stripK8sFromOptions { 73 | t.Error( 74 | "For", "stripK8sFromOptions", 75 | "expected", tc.stripK8sFromOptions, 76 | "got:", stripK8sFromOptions, 77 | ) 78 | } 79 | if logFilePath != tc.logFilePath { 80 | t.Error( 81 | "For", "logFilePath", 82 | "expected", tc.logFilePath, 83 | "got:", logFilePath, 84 | ) 85 | } 86 | if debug != tc.debug { 87 | t.Error( 88 | "For", "debug", 89 | "expected", tc.debug, 90 | "got:", debug, 91 | ) 92 | } 93 | if createVolumes != tc.createVolumes { 94 | t.Error( 95 | "For", "createVolumes", 96 | "expected", tc.createVolumes, 97 | "got:", createVolumes, 98 | ) 99 | } 100 | if enable16 != tc.enable16 { 101 | t.Error( 102 | "For", "enable16", 103 | "expected", tc.enable16, 104 | "got:", enable16, 105 | ) 106 | } 107 | if factorForConversion != tc.factorForConversion { 108 | t.Error( 109 | "For", "factorForConversion", 110 | "expected", tc.factorForConversion, 111 | "got:", factorForConversion, 112 | ) 113 | } 114 | if len(listOfStorageResourceOptions) != len(tc.listOfStorageResourceOptions) { 115 | t.Error( 116 | "For", "listOfStorageResourceOptions", 117 | "expected", tc.listOfStorageResourceOptions, 118 | "got:", listOfStorageResourceOptions, 119 | ) 120 | } 121 | if supportsCapabilities != tc.supportsCapabilities { 122 | t.Error( 123 | "For", "supportsCapabilities", 124 | "expected", tc.supportsCapabilities, 125 | "got:", supportsCapabilities, 126 | ) 127 | } 128 | }) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /cmd/dory/test/broken.json: -------------------------------------------------------------------------------- 1 | { 2 | "logFilePath": bool, 3 | "logDebug": 32, 4 | "stripK8sFromOptions": 32, 5 | "dockerVolumePluginSocketPath": 21, 6 | "createVolumes": "oops" 7 | "enable1.6": 123.23 8 | "listOfStorageResourceOptions" : [ "234", 567], 9 | "factorForConversion": "oops" 10 | } 11 | -------------------------------------------------------------------------------- /cmd/dory/test/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "logFilePath": true, 3 | "logDebug": 32, 4 | "stripK8sFromOptions": 32, 5 | "dockerVolumePluginSocketPath": 21, 6 | "createVolumes": "oops", 7 | "enable1.6": 123.23, 8 | "factorForConversion": "oops" 9 | } 10 | -------------------------------------------------------------------------------- /cmd/dory/test/flipped.json: -------------------------------------------------------------------------------- 1 | { 2 | "logFilePath": "some path", 3 | "logDebug": true, 4 | "stripK8sFromOptions": false, 5 | "dockerVolumePluginSocketPath": "nimble", 6 | "createVolumes": false, 7 | "enable1.6": true, 8 | "listOfStorageResourceOptions" : ["size","sizeInGiB","w","x","y","z"], 9 | "factorForConversion": 14, 10 | "supportsCapabilities": false 11 | } 12 | -------------------------------------------------------------------------------- /cmd/dory/test/good.json: -------------------------------------------------------------------------------- 1 | { 2 | "logFilePath": "/var/log/dory.log", 3 | "logDebug": false, 4 | "stripK8sFromOptions": true, 5 | "dockerVolumePluginSocketPath": "/run/docker/plugins/nimble.sock", 6 | "createVolumes": true, 7 | "enable1.6": false, 8 | "listOfStorageResourceOptions" : ["size","sizeInGiB"], 9 | "factorForConversion": 1073741824, 10 | "supportsCapabilities": true 11 | } 12 | -------------------------------------------------------------------------------- /cmd/doryd/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ADD [ "bin/doryd", "/usr/local/bin/doryd" ] 3 | ENTRYPOINT [ "doryd" ] 4 | CMD [ "/etc/kubernetes/admin.conf", "hpe.com" ] -------------------------------------------------------------------------------- /cmd/doryd/doryd.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "github.com/hpe-storage/dory/common/k8s/provisioner" 23 | "github.com/hpe-storage/dory/common/util" 24 | "k8s.io/client-go/kubernetes" 25 | "k8s.io/client-go/rest" 26 | "k8s.io/client-go/tools/clientcmd" 27 | "os" 28 | ) 29 | 30 | func main() { 31 | 32 | // glog configuration control is a bit lacking (which is to say it doesn't exist), 33 | // so we simply hack the the value to true. 34 | flag.Lookup("logtostderr").Value.Set("true") 35 | 36 | if len(os.Args) < 1 { 37 | fmt.Println("Please specify the full path (including filename) to admin config file.") 38 | return 39 | } 40 | 41 | kubeConfig := os.Args[1] 42 | config, err := clientcmd.BuildConfigFromFlags("", kubeConfig) 43 | if err != nil { 44 | fmt.Printf("Error getting config from file %s - %s\n", kubeConfig, err.Error()) 45 | config, err = rest.InClusterConfig() 46 | if err != nil { 47 | fmt.Printf("Error getting config cluster - %s\n", err.Error()) 48 | os.Exit(1) 49 | } 50 | } 51 | 52 | provisionerName := "dev.hpe.com" 53 | if len(os.Args) > 2 { 54 | provisionerName = os.Args[2] 55 | } 56 | 57 | kubeClient, err := kubernetes.NewForConfig(config) 58 | if err != nil { 59 | fmt.Printf("Error getting client - %s\n", err.Error()) 60 | os.Exit(1) 61 | } 62 | util.OpenLog(true) 63 | 64 | stop := make(chan struct{}) 65 | p := provisioner.NewProvisioner(kubeClient, provisionerName, true, true) 66 | p.Start(stop) 67 | <-stop 68 | 69 | } 70 | -------------------------------------------------------------------------------- /common/chain/chain.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package chain 18 | 19 | import ( 20 | "fmt" 21 | "sync" 22 | "time" 23 | ) 24 | 25 | // Chain is a set of Runners that will be executed sequentially 26 | type Chain struct { 27 | maxRetryOnError int 28 | sleepBeforeRetry time.Duration 29 | commands []Runner 30 | output map[string]interface{} 31 | outputLock *sync.RWMutex 32 | step int 33 | err error 34 | rollbackErr error 35 | runLock *sync.Mutex 36 | done bool 37 | } 38 | 39 | // Runner describes a struct that can be run and rolled back 40 | type Runner interface { 41 | // Name is used for logging and locating the output of a previously run Runner 42 | Name() string 43 | // Run does the work and returns a value. If error is returned the chain fails (after retries) 44 | Run() (interface{}, error) 45 | // Rollback is used to undo whatever Run() did 46 | Rollback() error 47 | } 48 | 49 | // NewChain creates a new chain. 50 | // retries dictates how many times a Runner should be retried on error. 51 | // retrySleep is how long to sleep before retrying a failed Runner 52 | func NewChain(retries int, retrySleep time.Duration) *Chain { 53 | return &Chain{ 54 | commands: make([]Runner, 0), 55 | maxRetryOnError: retries, 56 | sleepBeforeRetry: retrySleep, 57 | output: make(map[string]interface{}), 58 | outputLock: &sync.RWMutex{}, 59 | runLock: &sync.Mutex{}, 60 | } 61 | } 62 | 63 | // AppendRunner appends a Runner to the Chain 64 | func (c *Chain) AppendRunner(cmd Runner) error { 65 | c.runLock.Lock() 66 | defer c.runLock.Unlock() 67 | 68 | if c.done { 69 | return fmt.Errorf("this chain has already executed") 70 | } 71 | 72 | c.commands = append(c.commands, cmd) 73 | return nil 74 | } 75 | 76 | // Execute runs the chain exactly once 77 | func (c *Chain) Execute() error { 78 | c.runLock.Lock() 79 | defer c.runLock.Unlock() 80 | 81 | err := c.setup() 82 | if err != nil { 83 | return err 84 | } 85 | 86 | c.done = true 87 | c.step = 0 88 | for i, command := range c.commands { 89 | if command == nil { 90 | continue 91 | } 92 | var out interface{} 93 | out, err = c.runWithRetry(i, command) 94 | if err != nil { 95 | c.err = err 96 | break 97 | } 98 | c.outputLock.Lock() 99 | c.output[command.Name()] = out 100 | c.outputLock.Unlock() 101 | } 102 | if c.err != nil { 103 | completed := c.commands[:c.step+1] 104 | for i := len(completed) - 1; i >= 0; i-- { 105 | if c.commands[i] == nil { 106 | continue 107 | } 108 | err := c.rollbackWithRetry(c.commands[i]) 109 | if err != nil { 110 | c.rollbackErr = err 111 | } 112 | } 113 | } 114 | return c.err 115 | } 116 | 117 | // Error returns the last error returned by a Runner 118 | func (c *Chain) Error() error { 119 | return c.err 120 | } 121 | 122 | //ErrorRollback returns the last error returned by a Runner 123 | func (c *Chain) ErrorRollback() error { 124 | return c.rollbackErr 125 | } 126 | 127 | // GetRunnerOutput returns the output from a Runner. 128 | // It is valid to pass *Chain to a Runner. The 129 | // Runner can then use *Chain.GetRunnerOutput(...) to reference 130 | // the output of Runners that executed before them. 131 | func (c *Chain) GetRunnerOutput(name string) interface{} { 132 | c.outputLock.RLock() 133 | defer c.outputLock.RUnlock() 134 | return c.output[name] 135 | } 136 | 137 | func (c *Chain) setup() error { 138 | if c.done { 139 | return fmt.Errorf("this chain has already executed") 140 | } 141 | 142 | for _, command := range c.commands { 143 | if command == nil { 144 | continue 145 | } 146 | if _, found := c.output[command.Name()]; found { 147 | return fmt.Errorf("unable to create Chain because cmd names are not unique (%s)", command.Name()) 148 | } 149 | // assign a place holder 150 | c.output[command.Name()] = nil 151 | } 152 | return nil 153 | } 154 | 155 | func (c *Chain) runWithRetry(step int, command Runner) (out interface{}, err error) { 156 | c.step = step 157 | for try := 0; try < c.maxRetryOnError+1; try++ { 158 | out, err = command.Run() 159 | if err == nil { 160 | return out, err 161 | } 162 | time.Sleep(c.sleepBeforeRetry) 163 | } 164 | return out, err 165 | } 166 | 167 | func (c *Chain) rollbackWithRetry(command Runner) (err error) { 168 | for try := 0; try < c.maxRetryOnError+1; try++ { 169 | err = command.Rollback() 170 | if err == nil { 171 | return err 172 | } 173 | time.Sleep(c.sleepBeforeRetry) 174 | } 175 | return err 176 | } 177 | -------------------------------------------------------------------------------- /common/chain/chain_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package chain 17 | 18 | import ( 19 | "fmt" 20 | "github.com/hpe-storage/dory/common/util" 21 | "testing" 22 | ) 23 | 24 | type testAdder struct { 25 | data int 26 | err bool 27 | chain *Chain 28 | } 29 | 30 | func (tr *testAdder) Run() (interface{}, error) { 31 | foo := 0 32 | otherData := tr.chain.GetRunnerOutput(fmt.Sprintf("testTask%d", tr.data-1)) 33 | if otherData != nil { 34 | foo = otherData.(int) 35 | } 36 | 37 | if tr.err { 38 | return tr.data, fmt.Errorf("bad news") 39 | } 40 | return tr.data + foo, nil 41 | } 42 | 43 | func (tr *testAdder) Rollback() error { 44 | if tr.err { 45 | return fmt.Errorf("rollback bad news") 46 | } 47 | return nil 48 | } 49 | 50 | func (tr *testAdder) Name() string { 51 | return fmt.Sprintf("testTask%d", tr.data) 52 | } 53 | 54 | var basicTests = []struct { 55 | name string 56 | testData []int 57 | testFails []bool 58 | testResults []int 59 | }{ 60 | {"4 commands - no error", []int{1, 2, 3, 4}, []bool{false, false, false, false}, []int{1, 3, 6, 10}}, 61 | {"4 commands - error[1]", []int{1, 2, 3, 4}, []bool{false, true, false, false}, []int{1, -1, -1, -1}}, 62 | {"5 commands - no error", []int{1, 2, 3, 4, 5}, []bool{false, false, false, false, false}, []int{1, 3, 6, 10, 15}}, 63 | {"5 commands - error[3]", []int{1, 2, 3, 4, 5}, []bool{false, false, false, true, false}, []int{1, 3, 6, -1, -1}}, 64 | {"6 commands - no error", []int{1, 2, 3, 4, 5, 6}, []bool{false, false, false, false, false, false}, []int{1, 3, 6, 10, 15, 21}}, 65 | {"6 commands - error[3]", []int{1, 2, 3, 4, 5, 6}, []bool{false, false, false, true, false, false}, []int{1, 3, 6, -1, -1, -1}}, 66 | } 67 | 68 | func TestBasic(t *testing.T) { 69 | util.OpenLog(true) 70 | 71 | for _, tc := range basicTests { 72 | t.Run(tc.name, func(t *testing.T) { 73 | testChain := NewChain(2, 0) 74 | for i := range tc.testData { 75 | testChain.AppendRunner(&testAdder{tc.testData[i], tc.testFails[i], testChain}) 76 | } 77 | errorCheck(tc.name, tc.testFails, testChain, t) 78 | 79 | err := testChain.Execute() 80 | if err == nil { 81 | t.Fatalf("%s: should not be able to execute the same chain twice", tc.name) 82 | } 83 | err = testChain.AppendRunner(nil) 84 | if err == nil { 85 | t.Fatalf("%s: should not be able to append runners to an executed chain", tc.name) 86 | } 87 | 88 | for i, result := range tc.testResults { 89 | if tc.testResults[i] == -1 { 90 | if testChain.GetRunnerOutput(fmt.Sprintf("testTask%d", i+1)) != nil { 91 | t.Fatalf("%s: result for index %d should be ; got %v", tc.name, i, testChain.GetRunnerOutput(fmt.Sprintf("testTask%d", i+1))) 92 | } 93 | } else if testChain.GetRunnerOutput(fmt.Sprintf("testTask%d", i+1)) != result { 94 | t.Fatalf("%s: result for index %d should be %v; got %v", tc.name, i, result, testChain.GetRunnerOutput(fmt.Sprintf("testTask%d", i+1))) 95 | } 96 | } 97 | }) 98 | } 99 | } 100 | 101 | func TestMistakes(t *testing.T) { 102 | util.OpenLog(true) 103 | 104 | testChain := NewChain(0, 0) 105 | testChain.AppendRunner(&testAdder{1, false, testChain}) 106 | testChain.AppendRunner(&testAdder{1, false, testChain}) 107 | err := testChain.Execute() 108 | if err == nil { 109 | t.Fatalf("%s: should not be able to execute the chain with two runners named the same thing", "TestMistakes - samename") 110 | } 111 | 112 | testChain = NewChain(0, 0) 113 | testChain.AppendRunner(nil) 114 | testChain.AppendRunner(&testAdder{1, true, testChain}) 115 | err = testChain.Execute() 116 | if err == nil { 117 | t.Fatalf("%s: should get an error for a chain with a nil runner with a failed command", "TestMistakes - nil task") 118 | } 119 | 120 | testChain = NewChain(0, 0) 121 | testChain.AppendRunner(nil) 122 | testChain.AppendRunner(&testAdder{1, false, testChain}) 123 | err = testChain.Execute() 124 | if err != nil { 125 | t.Fatalf("%s: should not get a error for a chain with a nil runner", "TestMistakes - nil task") 126 | } 127 | 128 | } 129 | 130 | func errorCheck(name string, b []bool, chain *Chain, t *testing.T) { 131 | err := chain.Execute() 132 | shouldFail := false 133 | for _, fail := range b { 134 | if fail { 135 | shouldFail = true 136 | break 137 | } 138 | } 139 | 140 | if err != nil { 141 | if !shouldFail { 142 | t.Fatalf("%s: result for test chain should be no error; got %v", name, err) 143 | } 144 | if chain.Error() != err { 145 | t.Fatalf("%s: chain error (%v) should match returned error (%v)", name, chain.Error(), err) 146 | } 147 | if chain.ErrorRollback() == nil { 148 | t.Fatalf("%s: rollback error (%v) should not be nil", name, chain.ErrorRollback()) 149 | } 150 | } else { 151 | if shouldFail { 152 | t.Fatalf("%s: result for test chain should be error; got %v", name, err) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /common/connectivity/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package connectivity 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "fmt" 23 | "github.com/hpe-storage/dory/common/util" 24 | "io" 25 | "net" 26 | "net/http" 27 | "strings" 28 | "time" 29 | ) 30 | 31 | const ( 32 | defaultTimeout = time.Duration(30) * time.Second 33 | ) 34 | 35 | //Request encapsulates a request to the Do* family of functions 36 | type Request struct { 37 | //Action to take, ie: GET, POST, PUT, PATCH, DELETE 38 | Action string 39 | //Path is the URI 40 | Path string 41 | //Payload to send (may be nil) 42 | Payload interface{} 43 | //Response to marshal into (may be nil) 44 | Response interface{} 45 | //ResponseError to marshal error into (may be nil) 46 | ResponseError interface{} 47 | } 48 | 49 | // Client is a simple wrapper for http.Client 50 | type Client struct { 51 | *http.Client 52 | pathPrefix string 53 | } 54 | 55 | // NewHTTPClient returns a client that communicates over ip using a 30 second timeout 56 | func NewHTTPClient(url string) *Client { 57 | return NewHTTPClientWithTimeout(url, defaultTimeout) 58 | } 59 | 60 | // NewHTTPClientWithTimeout returns a client that communicates over ip 61 | func NewHTTPClientWithTimeout(url string, timeout time.Duration) *Client { 62 | if timeout < 1 { 63 | timeout = defaultTimeout 64 | } 65 | return &Client{&http.Client{Timeout: timeout}, url} 66 | } 67 | 68 | // NewHTTPSClientWithTimeout returns a client that communicates over ip with tls : 69 | func NewHTTPSClientWithTimeout(url string, transport http.RoundTripper, timeout time.Duration) *Client { 70 | if timeout < 1 { 71 | timeout = defaultTimeout 72 | } 73 | return &Client{&http.Client{Timeout: timeout, Transport: transport}, url} 74 | } 75 | 76 | // NewHTTPSClient returns a new https client 77 | func NewHTTPSClient(url string, transport http.RoundTripper) *Client { 78 | return NewHTTPSClientWithTimeout(url, transport, defaultTimeout) 79 | } 80 | 81 | // NewSocketClient returns a client that communicates over a unix socket using a 30 second connect timeout 82 | func NewSocketClient(filename string) *Client { 83 | return NewSocketClientWithTimeout(filename, defaultTimeout) 84 | } 85 | 86 | // NewSocketClientWithTimeout returns a client that communicates over a unix file socket 87 | func NewSocketClientWithTimeout(filename string, timeout time.Duration) *Client { 88 | if timeout < 1 { 89 | timeout = defaultTimeout 90 | } 91 | tr := &http.Transport{ 92 | DisableCompression: true, 93 | } 94 | tr.Dial = func(_, _ string) (net.Conn, error) { 95 | return net.DialTimeout("unix", filename, timeout) 96 | } 97 | return &Client{&http.Client{Transport: tr, Timeout: timeout}, "http://unix"} 98 | } 99 | 100 | // DoJSON action on path. payload and response are expected to be structs that decode/encode from/to json 101 | // Example action=POST, path=/VolumeDriver.Create ... 102 | // Tries 3 times to get data from the server 103 | func (client *Client) DoJSON(r *Request) error { 104 | // make sure we have a root slash 105 | if !strings.HasPrefix(r.Path, "/") { 106 | r.Path = client.pathPrefix + "/" + r.Path 107 | } else { 108 | r.Path = client.pathPrefix + r.Path 109 | } 110 | 111 | var buf bytes.Buffer 112 | // encode the payload 113 | if r.Payload != nil { 114 | if err := json.NewEncoder(&buf).Encode(r.Payload); err != nil { 115 | return err 116 | } 117 | } 118 | 119 | // build request 120 | req, err := http.NewRequest(r.Action, r.Path, &buf) 121 | if err != nil { 122 | return err 123 | } 124 | req.Header.Add("Accept", "application/json") 125 | req.Close = true 126 | util.LogDebug.Printf("request: action=%s path=%s payload=%s", r.Action, r.Path, buf.String()) 127 | 128 | // execute the do 129 | res, err := doWithRetry(client, req) 130 | if err != nil { 131 | return err 132 | } 133 | defer res.Body.Close() 134 | 135 | // check the status code 136 | if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusCreated { 137 | //decode the body into the error response 138 | util.LogError.Printf("status code was %s for request: action=%s path=%s, attempting to decode error response.", res.Status, r.Action, r.Path) 139 | err = decode(res.Body, r.ResponseError, r) 140 | if err != nil { 141 | return err 142 | } 143 | return fmt.Errorf("status code was %s for request: action=%s path=%s, attempting to decode error response", res.Status, r.Action, r.Path) 144 | 145 | } 146 | 147 | err = decode(res.Body, r.Response, r) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | return nil 153 | } 154 | 155 | func doWithRetry(client *Client, request *http.Request) (*http.Response, error) { 156 | try := 0 157 | maxTries := 3 158 | for { 159 | response, err := client.Do(request) 160 | if err != nil { 161 | if try < maxTries { 162 | try++ 163 | time.Sleep(time.Duration(try) * time.Second) 164 | continue 165 | } 166 | return nil, err 167 | } 168 | util.LogDebug.Printf("response: %v, length=%v", response.Status, response.ContentLength) 169 | return response, nil 170 | } 171 | } 172 | 173 | func decode(rc io.ReadCloser, dest interface{}, r *Request) error { 174 | if rc != nil && dest != nil { 175 | if err := json.NewDecoder(rc).Decode(&dest); err != nil { 176 | util.LogError.Printf("unable to decode %v returned from action=%s to path=%s. error=%v", rc, r.Action, r.Path, err) 177 | return err 178 | } 179 | } 180 | return nil 181 | } 182 | -------------------------------------------------------------------------------- /common/connectivity/client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package connectivity 17 | 18 | import ( 19 | "fmt" 20 | "io/ioutil" 21 | "net" 22 | "net/http" 23 | "os" 24 | "testing" 25 | "time" 26 | ) 27 | 28 | const ( 29 | socket = "socket.socket" 30 | socket2 = "socket2.socket" 31 | requestJSON = "{\"ping\":\"junk\"}\n" 32 | pathString = "/woohoo" 33 | ) 34 | 35 | type answer struct { 36 | Pong string 37 | } 38 | 39 | type badnews struct { 40 | Info string 41 | } 42 | 43 | type question struct { 44 | Ping string `json:"ping,omitempty"` 45 | } 46 | 47 | type testHandler struct { 48 | t *testing.T 49 | } 50 | 51 | func (th *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 52 | 53 | switch r.URL.String() { 54 | case "/error": 55 | http.Error(w, "{\"info\":\"sending an error\"}", http.StatusInternalServerError) 56 | default: 57 | if pathString != r.URL.String() { 58 | th.t.Error( 59 | "For", "URL", 60 | "expected", pathString, 61 | "got", r.URL.String(), 62 | ) 63 | } 64 | buf, _ := ioutil.ReadAll(r.Body) 65 | if requestJSON != fmt.Sprintf("%s", buf) { 66 | th.t.Error( 67 | "For", "requestedJSON", 68 | "expected", requestJSON, 69 | "got", fmt.Sprintf("%s", buf), 70 | ) 71 | } 72 | fmt.Fprint(w, "{\"pong\":\"test\"}") 73 | } 74 | 75 | } 76 | 77 | type testTimeoutHandler struct { 78 | t *testing.T 79 | } 80 | 81 | func (th *testTimeoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 82 | time.Sleep(time.Second) 83 | fmt.Fprint(w, "{\"pong\":\"test\"}") 84 | } 85 | 86 | func TestSocket(t *testing.T) { 87 | // server 88 | os.Remove(socket) 89 | defer os.Remove(socket) 90 | server := http.Server{} 91 | server.Handler = &testHandler{t: t} 92 | unixListener, err := net.Listen("unix", socket) 93 | if err != nil { 94 | t.Error( 95 | "trying to listen. expected to start server!", 96 | "got error:", err, 97 | ) 98 | } 99 | go server.Serve(unixListener) 100 | 101 | //client 102 | client := NewSocketClient(socket) 103 | 104 | var foo answer 105 | err = client.DoJSON(&Request{"POST", pathString, &question{Ping: "junk"}, &foo, nil}) 106 | verifyFoo(err, foo, t) 107 | 108 | var bad badnews 109 | err = client.DoJSON( 110 | &Request{ 111 | Action: "POST", 112 | Path: "/error", 113 | Payload: &question{Ping: "junk"}, 114 | Response: &foo, 115 | ResponseError: &bad, 116 | }) 117 | verifyBadNews(err, bad, t) 118 | } 119 | 120 | func TestSocketTimeout(t *testing.T) { 121 | // server 122 | os.Remove(socket2) 123 | defer os.Remove(socket2) 124 | server := http.Server{} 125 | server.Handler = &testTimeoutHandler{t: t} 126 | unixListener, err := net.Listen("unix", socket2) 127 | if err != nil { 128 | t.Error( 129 | "trying to listen. expected to start server!", 130 | "got error:", err, 131 | ) 132 | } 133 | go server.Serve(unixListener) 134 | 135 | //client with timout 136 | client := NewSocketClientWithTimeout(socket2, time.Millisecond) 137 | var foo answer 138 | err = client.DoJSON(&Request{"POST", pathString, &question{Ping: "junk"}, &foo, nil}) 139 | if err == nil { 140 | t.Error( 141 | "client post expected to timeout", 142 | "error: was nil", 143 | ) 144 | } 145 | 146 | //client with no timout 147 | client = NewSocketClient(socket2) 148 | err = client.DoJSON(&Request{"POST", pathString, &question{Ping: "junk"}, &foo, nil}) 149 | verifyFoo(err, foo, t) 150 | } 151 | 152 | func TestHTTP(t *testing.T) { 153 | // server 154 | go http.ListenAndServe(":8080", &testHandler{t: t}) 155 | 156 | client := NewHTTPClient("http://127.0.0.1:8080") 157 | 158 | //client 159 | var foo answer 160 | err := client.DoJSON(&Request{"POST", pathString, &question{Ping: "junk"}, &foo, nil}) 161 | verifyFoo(err, foo, t) 162 | 163 | var bad badnews 164 | err = client.DoJSON( 165 | &Request{ 166 | Action: "POST", 167 | Path: "/error", 168 | Payload: &question{Ping: "junk"}, 169 | Response: &foo, 170 | ResponseError: &bad, 171 | }) 172 | verifyBadNews(err, bad, t) 173 | } 174 | 175 | func TestHTTPTimeout(t *testing.T) { 176 | // server 177 | go http.ListenAndServe(":8082", &testTimeoutHandler{t: t}) 178 | 179 | //client with timeout 180 | client := NewHTTPClientWithTimeout("http://127.0.0.1:8082", time.Millisecond) 181 | var foo answer 182 | err := client.DoJSON(&Request{"POST", pathString, &question{Ping: "junk"}, &foo, nil}) 183 | if err == nil { 184 | t.Error( 185 | "client post expected to timeout", 186 | "error: was nil", 187 | ) 188 | } 189 | 190 | client = NewHTTPClient("http://127.0.0.1:8080") 191 | err = client.DoJSON(&Request{"POST", pathString, &question{Ping: "junk"}, &foo, nil}) 192 | verifyFoo(err, foo, t) 193 | } 194 | 195 | func verifyFoo(err error, foo answer, t *testing.T) { 196 | if err != nil { 197 | t.Error( 198 | "client post expected to not have error", 199 | "got error:", err, 200 | ) 201 | } 202 | if foo.Pong != "test" { 203 | t.Error( 204 | "For", "foo.Pong", 205 | "expected", "test", 206 | "got", foo.Pong) 207 | } 208 | } 209 | 210 | func verifyBadNews(err error, bad badnews, t *testing.T) { 211 | if err == nil { 212 | t.Error( 213 | "expected to get an error from POST to /error", 214 | ) 215 | } 216 | if bad.Info != "sending an error" { 217 | t.Error( 218 | "Bad", "bad.Info", 219 | "expected", "sending an error", 220 | "got", bad.Info) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /common/docker/dockerlt/dockerlt.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2018 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package dockerlt 18 | 19 | import ( 20 | "github.com/hpe-storage/dory/common/connectivity" 21 | "github.com/hpe-storage/dory/common/util" 22 | "time" 23 | ) 24 | 25 | const ( 26 | defaultSocketPath = "/var/run/docker.sock" 27 | dockerClientSocketTimeout = time.Duration(300) * time.Second 28 | ) 29 | 30 | // DockerClient is a light weight docker client 31 | type DockerClient struct { 32 | client *connectivity.Client 33 | } 34 | 35 | type errorResponse struct { 36 | Message string `json:"message,omitempty"` 37 | } 38 | 39 | // NewDockerClient provides a light weight docker client connection 40 | func NewDockerClient(socketPath string) *DockerClient { 41 | if socketPath == "" { 42 | socketPath = defaultSocketPath 43 | } 44 | return &DockerClient{connectivity.NewSocketClientWithTimeout(socketPath, dockerClientSocketTimeout)} 45 | } 46 | 47 | // PluginsGet does a GET against /plugins 48 | func (dc *DockerClient) PluginsGet() ([]Plugin, error) { 49 | plugins := make([]Plugin, 0) 50 | apiError := &errorResponse{} 51 | 52 | err := dc.client.DoJSON(&connectivity.Request{ 53 | Action: "GET", 54 | Path: "/plugins", 55 | Payload: nil, 56 | Response: &plugins, 57 | ResponseError: apiError}) 58 | 59 | if err != nil { 60 | util.LogInfo.Printf("unable to list docker plugins - %s (%s)", err.Error(), apiError.Message) 61 | return nil, err 62 | } 63 | 64 | util.LogDebug.Printf("returning %#v", plugins) 65 | return plugins, nil 66 | } 67 | -------------------------------------------------------------------------------- /common/docker/dockerlt/plugin.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2018 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package dockerlt 18 | 19 | // Plugin describes a Docker v2 plugin 20 | type Plugin struct { 21 | ID string `json:"Id,omitempty"` 22 | Name string `json:"Name,omitempty"` 23 | Enabled bool `json:"Enabled,omitempty"` 24 | Config PluginConfig `json:"Config,omitempty"` 25 | } 26 | 27 | // PluginConfig describes the config for the plugin 28 | type PluginConfig struct { 29 | Interface PluginInterface `json:"Interface,omitempty"` 30 | } 31 | 32 | // PluginInterface describes the interface used by docker to communicate with this plugin 33 | type PluginInterface struct { 34 | Socket string `json:"Socket,omitempty"` 35 | } 36 | -------------------------------------------------------------------------------- /common/docker/dockervol/dockervol.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package dockervol 18 | 19 | import ( 20 | "fmt" 21 | "github.com/hpe-storage/dory/common/connectivity" 22 | "github.com/hpe-storage/dory/common/docker/dockerlt" 23 | "github.com/hpe-storage/dory/common/util" 24 | "strings" 25 | "time" 26 | ) 27 | 28 | const ( 29 | //ActivateURI is /Plugin.Activate 30 | ActivateURI = "/Plugin.Activate" 31 | //CreateURI is /VolumeDriver.Create 32 | CreateURI = "/VolumeDriver.Create" 33 | //UpdateURI = "/VolumeDriver.Update" 34 | UpdateURI = "/VolumeDriver.Update" 35 | //ListURI is /VolumeDriver.List 36 | ListURI = "/VolumeDriver.List" 37 | //CapabilitiesURI is /VolumeDriver.Capabilities 38 | CapabilitiesURI = "/VolumeDriver.Capabilities" 39 | //RemoveURI is /VolumeDriver.Remove 40 | RemoveURI = "/VolumeDriver.Remove" 41 | //MountURI is /VolumeDriver.Mount 42 | MountURI = "/VolumeDriver.Mount" 43 | //UnmountURI is /VolumeDriver.Unmount 44 | UnmountURI = "/VolumeDriver.Unmount" 45 | //GetURI is /VolumeDriver.Get 46 | GetURI = "/VolumeDriver.Get" 47 | //NotFound describes the beginning of the not found error message 48 | NotFound = "Unable to find" 49 | 50 | defaultSocketPath = "/run/docker/plugins/nimble.sock" 51 | maxTries = 3 52 | dvpSocketTimeout = time.Duration(300) * time.Second 53 | ) 54 | 55 | //Options for volumedriver 56 | type Options struct { 57 | SocketPath string 58 | StripK8sFromOptions bool 59 | LogFilePath string 60 | Debug bool 61 | CreateVolumes bool 62 | ListOfStorageResourceOptions []string 63 | FactorForConversion int 64 | SupportsCapabilities bool 65 | } 66 | 67 | //DockerVolumePlugin is the client to a specific docker volume plugin 68 | type DockerVolumePlugin struct { 69 | stripK8sOpts bool 70 | client *connectivity.Client 71 | ListOfStorageResourceOptions []string 72 | FactorForConversion int 73 | } 74 | 75 | //Errorer describes the ability get the embedded error 76 | type Errorer interface { 77 | getErr() string 78 | } 79 | 80 | //Request is the basic request to use when talking to the driver 81 | type Request struct { 82 | Name string `json:"Name,omitempty"` 83 | Opts map[string]interface{} `json:"Opts,omitempty"` 84 | } 85 | 86 | //MountRequest is used to mount and unmount volumes 87 | type MountRequest struct { 88 | Name string `json:"Name,omitempty"` 89 | ID string `json:"ID,omitempty"` 90 | } 91 | 92 | //MountResponse is returned from the volume driver 93 | type MountResponse struct { 94 | Mountpoint string `json:"Mountpoint,omitempty"` 95 | Err string `json:"Err,omitempty"` 96 | } 97 | 98 | func (g *MountResponse) getErr() string { 99 | return g.Err 100 | } 101 | 102 | //GetResponse is returned from the volume driver 103 | type GetResponse struct { 104 | Volume DockerVolume `json:"Volume,omitempty"` 105 | Err string `json:"Err,omitempty"` 106 | } 107 | 108 | func (g *GetResponse) getErr() string { 109 | return g.Err 110 | } 111 | 112 | //GetListResponse is returned from the volume driver list request 113 | type GetListResponse struct { 114 | Volumes []DockerVolume `json:"Volumes,omitempty"` 115 | Err string `json:"Err,omitempty"` 116 | } 117 | 118 | func (g *GetListResponse) getErr() string { 119 | return g.Err 120 | } 121 | 122 | //DockerVolume represents the details about a docker volume 123 | type DockerVolume struct { 124 | Name string `json:"Name,omitempty"` 125 | Mountpoint string `json:"Mountpoint,omitempty"` 126 | Status map[string]interface{} `json:"Status,omitempty"` 127 | } 128 | 129 | //CapResponse describes the capabilities of the plugin 130 | type CapResponse struct { 131 | Capabilities PluginCapabilities `json:"Capabilities,omitempty"` 132 | } 133 | 134 | //PluginCapabilities includes the scope of the plugin 135 | type PluginCapabilities struct { 136 | Scope string `json:"Scope,omitempty"` 137 | } 138 | 139 | // NewDockerVolumePlugin creates a DockerVolumePlugin which can be used to communicate with 140 | // a Docker Volume Plugin. options.socketPath can be the full path to the socket file or 141 | // the name of a Docker V2 plugin. In the case of the V2 plugin, the name of th plugin 142 | // is used to look up the full path to the socketfile. 143 | func NewDockerVolumePlugin(options *Options) (*DockerVolumePlugin, error) { 144 | var err error 145 | if !strings.HasPrefix(options.SocketPath, "/") { 146 | // this is a v2 plugin, so we need to find its socket file 147 | options.SocketPath, err = getV2PluginSocket(options.SocketPath, "") 148 | } 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | if options.SocketPath == "" { 154 | options.SocketPath = defaultSocketPath 155 | } 156 | dvp := &DockerVolumePlugin{ 157 | stripK8sOpts: options.StripK8sFromOptions, 158 | client: connectivity.NewSocketClientWithTimeout(options.SocketPath, dvpSocketTimeout), 159 | ListOfStorageResourceOptions: options.ListOfStorageResourceOptions, 160 | FactorForConversion: options.FactorForConversion, 161 | } 162 | 163 | if options.SupportsCapabilities { 164 | // test connectivity 165 | _, err = dvp.Capabilities() 166 | if err != nil { 167 | return dvp, err 168 | } 169 | } 170 | 171 | return dvp, nil 172 | 173 | } 174 | 175 | type empty struct{} 176 | 177 | //Capabilities returns the capabilities supported by the plugin 178 | func (dvp *DockerVolumePlugin) Capabilities() (*CapResponse, error) { 179 | var req = &empty{} 180 | var res = &CapResponse{} 181 | 182 | err := dvp.driverRun(&connectivity.Request{ 183 | Action: "POST", 184 | Path: CapabilitiesURI, 185 | Payload: req, 186 | Response: res, 187 | ResponseError: res}) 188 | if err != nil { 189 | util.LogInfo.Printf("unable to get Capabilities - %s\n", err.Error()) 190 | return nil, err 191 | } 192 | 193 | util.LogDebug.Printf("returning %#v", res) 194 | return res, nil 195 | } 196 | 197 | //Get a docker volume by docker name returning the response from the driver 198 | func (dvp *DockerVolumePlugin) Get(name string) (*GetResponse, error) { 199 | var req = &Request{Name: name} 200 | var res = &GetResponse{} 201 | 202 | err := dvp.driverRun(&connectivity.Request{ 203 | Action: "POST", 204 | Path: GetURI, 205 | Payload: req, 206 | Response: res, 207 | ResponseError: res}) 208 | if err != nil { 209 | util.LogInfo.Printf("unable to get docker volume using %s - %s response - %v\n", name, err.Error(), res) 210 | return nil, err 211 | } 212 | 213 | if err = driverErrorCheck(res); err != nil { 214 | util.LogInfo.Printf("unable to get docker volume using %s - %s\n", name, err.Error()) 215 | return nil, err 216 | } 217 | util.LogDebug.Printf("returning %#v", res) 218 | return res, nil 219 | } 220 | 221 | //List the docker volumes returning the response from the driver 222 | func (dvp *DockerVolumePlugin) List() (*GetListResponse, error) { 223 | var req = &Request{} 224 | var res = &GetListResponse{} 225 | 226 | err := dvp.driverRun(&connectivity.Request{ 227 | Action: "POST", 228 | Path: ListURI, 229 | Payload: req, 230 | Response: res, 231 | ResponseError: res}) 232 | if err != nil { 233 | util.LogInfo.Printf("unable to list docker volumes - %s response - %v\n", err.Error(), res) 234 | return nil, err 235 | } 236 | 237 | if err = driverErrorCheck(res); err != nil { 238 | util.LogInfo.Printf("unable to list docker volumes - %s\n", err.Error()) 239 | return nil, err 240 | } 241 | util.LogDebug.Printf("returning %#v", res) 242 | return res, nil 243 | } 244 | 245 | // createOrUpdate handler 246 | func (dvp *DockerVolumePlugin) createOrUpdate(name string, options map[string]interface{}, isUpdate bool) (string, error) { 247 | if name == "" { 248 | return "", fmt.Errorf("name is required") 249 | } 250 | for key := range options { 251 | if key == "name" || (dvp.stripK8sOpts && strings.HasPrefix(key, "kubernetes.io")) { 252 | delete(options, key) 253 | } 254 | } 255 | var req = &Request{Name: name, Opts: options} 256 | var res = &GetResponse{} 257 | var err error 258 | if isUpdate { 259 | err = dvp.driverRun(&connectivity.Request{ 260 | Action: "PUT", 261 | Path: UpdateURI, 262 | Payload: req, 263 | Response: res, 264 | ResponseError: res}) 265 | } else { 266 | err = dvp.driverRun(&connectivity.Request{ 267 | Action: "POST", 268 | Path: CreateURI, 269 | Payload: req, 270 | Response: res, 271 | ResponseError: res}) 272 | } 273 | if err != nil { 274 | util.LogError.Printf("unable to create/update docker volume using %v & %v - %s response - %v\n", name, options, err.Error(), res) 275 | return "", err 276 | } 277 | if err = driverErrorCheck(res); err != nil { 278 | return "", err 279 | } 280 | return res.Volume.Name, nil 281 | } 282 | 283 | // Update the docker volumes 284 | // nolint Create and Update have same signature. For maintaining backward compatibility we need these two definitions 285 | func (dvp *DockerVolumePlugin) Update(name string, options map[string]interface{}) (string, error) { 286 | name, err := dvp.createOrUpdate(name, options, true) 287 | if err != nil { 288 | util.LogError.Printf("unable to update docker volume using %v & %v - %s\n", name, options, err.Error()) 289 | return "", err 290 | } 291 | return name, nil 292 | } 293 | 294 | //Create a docker volume returning the docker volume name 295 | // nolint Create and Update have same signature. For maintaining backward compatibility we need these two definitions 296 | func (dvp *DockerVolumePlugin) Create(name string, options map[string]interface{}) (string, error) { 297 | name, err := dvp.createOrUpdate(name, options, false) 298 | if err != nil { 299 | util.LogError.Printf("unable to create docker volume using %v & %v - %s\n", name, options, err.Error()) 300 | return "", err 301 | } 302 | return name, nil 303 | } 304 | 305 | //Mount attaches and mounts a nimble volume returning the path 306 | func (dvp *DockerVolumePlugin) Mount(name, mountID string) (string, error) { 307 | util.LogDebug.Printf("Mount called with %s %s", name, mountID) 308 | try := 0 309 | for { 310 | util.LogDebug.Printf("dvp.mounter() called with %s %s %s try:%d", name, mountID, MountURI, try+1) 311 | m, err := dvp.mounter(name, mountID, MountURI) 312 | if err != nil { 313 | if try < maxTries { 314 | try++ 315 | time.Sleep(time.Duration(try) * time.Second) 316 | continue 317 | } 318 | return "", err 319 | } 320 | return m, nil 321 | } 322 | } 323 | 324 | //Unmount and detaches volume for maxTries 325 | func (dvp *DockerVolumePlugin) Unmount(name, mountID string) error { 326 | util.LogDebug.Printf("Unmount called with %s %s", name, mountID) 327 | try := 0 328 | for { 329 | util.LogDebug.Printf("dvp.mounter() called with %s %s %s try:%d", name, mountID, UnmountURI, try+1) 330 | _, err := dvp.mounter(name, mountID, UnmountURI) 331 | if err != nil { 332 | if try < maxTries { 333 | try++ 334 | time.Sleep(time.Duration(try) * time.Second) 335 | continue 336 | } 337 | return err 338 | } 339 | return nil 340 | } 341 | } 342 | 343 | //Delete calls the delete function of the plugin 344 | func (dvp *DockerVolumePlugin) Delete(name string, managerName string) error { 345 | if name == "" { 346 | return fmt.Errorf("name is required") 347 | } 348 | var req *Request 349 | if managerName != "" { 350 | req = &Request{Name: name, Opts: map[string]interface{}{"manager": managerName}} 351 | } else { 352 | req = &Request{Name: name} 353 | } 354 | 355 | var res = &GetResponse{} 356 | 357 | err := dvp.driverRun(&connectivity.Request{ 358 | Action: "POST", 359 | Path: RemoveURI, 360 | Payload: req, 361 | Response: res, 362 | ResponseError: res}) 363 | if err != nil { 364 | util.LogError.Printf("%s failed %v - %s response - %v\n", RemoveURI, name, err.Error(), res) 365 | return err 366 | } 367 | 368 | if err = driverErrorCheck(res); err != nil { 369 | util.LogError.Printf("%s failed %v - %s\n", RemoveURI, name, err.Error()) 370 | return err 371 | } 372 | 373 | return nil 374 | } 375 | 376 | func (dvp *DockerVolumePlugin) mounter(name, mountID string, path string) (string, error) { 377 | if name == "" { 378 | return "", fmt.Errorf("name is required") 379 | } 380 | var req = &MountRequest{Name: name, ID: mountID} 381 | var res = &MountResponse{} 382 | 383 | err := dvp.driverRun(&connectivity.Request{ 384 | Action: "POST", 385 | Path: path, 386 | Payload: req, 387 | Response: res, 388 | ResponseError: res}) 389 | if err != nil { 390 | util.LogError.Printf("%s failed %v & %v - %s response - %v\n", path, name, mountID, err.Error(), res) 391 | return "", err 392 | } 393 | 394 | if err = driverErrorCheck(res); err != nil { 395 | util.LogError.Printf("%s failed %v & %v - %s\n", path, name, mountID, err.Error()) 396 | return "", err 397 | } 398 | 399 | return res.Mountpoint, nil 400 | } 401 | 402 | func (dvp *DockerVolumePlugin) driverRun(r *connectivity.Request) error { 403 | return dvp.client.DoJSON(r) 404 | } 405 | 406 | func driverErrorCheck(e Errorer) error { 407 | if e.getErr() != "" { 408 | return fmt.Errorf(e.getErr()) 409 | } 410 | return nil 411 | } 412 | 413 | // name is the name of the docker volume plugin. dockerSocket is the full path to the docker socket. The default is used if an empty string is passed. 414 | func getV2PluginSocket(name, dockerSocket string) (string, error) { 415 | c := dockerlt.NewDockerClient(dockerSocket) 416 | plugins, err := c.PluginsGet() 417 | 418 | if err != nil { 419 | return "", fmt.Errorf("failed to get V2 plugins from docker. error=%s", err.Error()) 420 | } 421 | 422 | for _, plugin := range plugins { 423 | if strings.Compare(name, plugin.Name) == 0 || strings.Compare(fmt.Sprintf("%s:latest", name), plugin.Name) == 0 { 424 | if !plugin.Enabled { 425 | return fmt.Sprintf("/run/docker/plugins/%s/%s", plugin.ID, plugin.Config.Interface.Socket), fmt.Errorf("found Docker V2 Plugin named %s, but it is disabled", name) 426 | } 427 | return fmt.Sprintf("/run/docker/plugins/%s/%s", plugin.ID, plugin.Config.Interface.Socket), nil 428 | } 429 | } 430 | 431 | return "", fmt.Errorf("unable to find V2 plugin named %s", name) 432 | } 433 | -------------------------------------------------------------------------------- /common/jconfig/broken.json: -------------------------------------------------------------------------------- 1 | { 2 | "broken": 23a 3 | } -------------------------------------------------------------------------------- /common/jconfig/jconfig.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package jconfig 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | "reflect" 24 | "strconv" 25 | ) 26 | 27 | // Config contains a map loaded from t a json file 28 | type Config struct { 29 | config map[string]interface{} 30 | } 31 | 32 | //NewConfig loads the JSON in the file referred to in the path 33 | func NewConfig(path string) (*Config, error) { 34 | c := &Config{} 35 | file, err := os.Open(path) 36 | if err != nil { 37 | return nil, err 38 | } 39 | if file != nil { 40 | defer file.Close() 41 | if err := json.NewDecoder(file).Decode(&c.config); err != nil { 42 | return nil, err 43 | } 44 | } 45 | return c, nil 46 | } 47 | 48 | //GetString returns the string value loaded from the JSON (backward compatibility) 49 | func (c *Config) GetString(key string) (s string) { 50 | s, _ = c.GetStringWithError(key) 51 | return 52 | } 53 | 54 | //GetStringWithError returns the string value loaded from the JSON 55 | func (c *Config) GetStringWithError(key string) (s string, err error) { 56 | if _, found := c.config[key]; found { 57 | switch value := c.config[key].(type) { 58 | case string: 59 | return value, nil 60 | default: 61 | return fmt.Sprintf("%v", c.config[key]), nil 62 | } 63 | } 64 | return s, fmt.Errorf("key:%v not found", key) 65 | } 66 | 67 | //GetStringSlice returns the string value loaded from the JSON (backward compatibility) 68 | func (c *Config) GetStringSlice(key string) (strings []string) { 69 | strings, _ = c.GetStringSliceWithError(key) 70 | return 71 | } 72 | 73 | //GetMapSlice returns map of strings and interface with error 74 | func (c *Config) GetMapSlice(key string) (maps []map[string]interface{}, err error) { 75 | if _, found := c.config[key]; found { 76 | switch values := c.config[key].(type) { 77 | case []interface{}: 78 | for _, value := range values { 79 | v := reflect.ValueOf(value) 80 | if v.Kind() == reflect.Map { 81 | for _, key := range v.MapKeys() { 82 | val := v.MapIndex(key).Interface() 83 | maps = append(maps, map[string]interface{}{fmt.Sprintf("%v", key): val}) 84 | } 85 | } 86 | } 87 | return maps, nil 88 | } 89 | } 90 | return nil, fmt.Errorf("key:%v not found", key) 91 | } 92 | 93 | //GetStringSliceWithError returns the string value loaded from the JSON 94 | func (c *Config) GetStringSliceWithError(key string) (strings []string, err error) { 95 | if _, found := c.config[key]; found { 96 | switch value := c.config[key].(type) { 97 | case []interface{}: 98 | for _, d := range value { 99 | strings = append(strings, fmt.Sprintf("%v", d)) 100 | } 101 | return strings, nil 102 | default: 103 | return strings, fmt.Errorf("key:%v is not a slice. value:%v kind:%s type:%s", key, c.config[key], reflect.TypeOf(c.config[key]).Kind(), reflect.TypeOf(c.config[key])) 104 | } 105 | } 106 | return strings, fmt.Errorf("key:%v not found", key) 107 | } 108 | 109 | //GetInt64 returns the value in the JSON cast to int64 (backward compatibility) 110 | func (c *Config) GetInt64(key string) (i int64) { 111 | i, _ = c.GetInt64SliceWithError(key) 112 | return 113 | } 114 | 115 | //GetInt64SliceWithError returns the value in the JSON cast to int64 116 | func (c *Config) GetInt64SliceWithError(key string) (i int64, err error) { 117 | if _, found := c.config[key]; found { 118 | switch value := c.config[key].(type) { 119 | //json marshall stores numbers as floats 120 | case float64: 121 | return int64(value), nil 122 | //we can always try to parse a string 123 | case string: 124 | return strconv.ParseInt(value, 10, 64) 125 | default: 126 | return 0, fmt.Errorf("key:%v is not a number. value:%v kind:%s type:%s", key, c.config[key], reflect.TypeOf(c.config[key]).Kind(), reflect.TypeOf(c.config[key])) 127 | } 128 | } 129 | return 0, fmt.Errorf("key:%v not found", key) 130 | } 131 | 132 | //GetBool returns the value in the JSON cast to bool 133 | func (c *Config) GetBool(key string) (b bool, err error) { 134 | if _, found := c.config[key]; found { 135 | switch value := c.config[key].(type) { 136 | case bool: 137 | return bool(value), nil 138 | //we can always try to parse a string 139 | case string: 140 | return strconv.ParseBool(value) 141 | default: 142 | return false, fmt.Errorf("key:%v is not a bool. value:%v kind:%s type:%s", key, c.config[key], reflect.TypeOf(c.config[key]).Kind(), reflect.TypeOf(c.config[key])) 143 | } 144 | } 145 | return false, fmt.Errorf("key:%v not found", key) 146 | } 147 | -------------------------------------------------------------------------------- /common/jconfig/jconfig_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package jconfig 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | var ( 24 | mapsSlice []map[string]interface{} 25 | ) 26 | 27 | var basicTests = []struct { 28 | name string 29 | testKey string 30 | resultString string 31 | resultInt int64 32 | resultStringSlice []string 33 | resultBool bool 34 | resultBoolError bool 35 | resultMapSlice []map[string]interface{} 36 | }{ 37 | {"get someString", "someString", "some string", 0, nil, false, true, nil}, 38 | {"get stringNumber", "stringNumber", "23", 23, nil, false, true, nil}, 39 | {"get actualNumber", "actualNumber", "1.073741824e+10", 10737418240, nil, false, true, nil}, 40 | {"get badIntNumber", "badIntNumber", "2.3", 2, nil, false, true, nil}, 41 | {"get notFound", "no key named this", "", 0, nil, false, true, nil}, 42 | {"get smallNumber", "smallNumber", "40", 40, nil, false, true, nil}, 43 | {"get someStrings", "someStrings", "[first 2nd c]", 0, []string{"first", "2nd", "c"}, false, true, nil}, 44 | {"get boolean", "boolean", "true", 0, nil, true, false, nil}, 45 | {"get stringBool", "stringBool", "True", 0, nil, true, false, nil}, 46 | {"get someMaps", "someMaps", "[map[first:1] map[second:2]]", 0, nil, false, false, append(mapsSlice, map[string]interface{}{"first": 1}, map[string]interface{}{"second": 2})}, 47 | } 48 | 49 | func TestBasic(t *testing.T) { 50 | c, err := NewConfig("./test.json") 51 | if err != nil { 52 | t.Error( 53 | "For file load of ./test.json", 54 | "expected", "no error", 55 | "got error:", err, 56 | ) 57 | } 58 | 59 | for _, tc := range basicTests { 60 | t.Run(tc.name, func(t *testing.T) { 61 | s := c.GetString(tc.testKey) 62 | if s != tc.resultString { 63 | t.Fatalf("%s: GetString(%v) should return %v; got %v", tc.name, tc.testKey, tc.resultString, s) 64 | } 65 | i := c.GetInt64(tc.testKey) 66 | if i != tc.resultInt { 67 | t.Fatalf("%s: GetInt64(%v) should return %v; got %v", tc.name, tc.testKey, tc.resultInt, i) 68 | } 69 | ss := c.GetStringSlice(tc.testKey) 70 | if ss != nil && tc.resultStringSlice != nil { 71 | for x := range tc.resultStringSlice { 72 | if ss[x] != tc.resultStringSlice[x] { 73 | t.Fatalf("%s: GetStringSlice(%v) should return %v; got %v", tc.name, tc.testKey, tc.resultStringSlice, ss) 74 | } 75 | } 76 | } 77 | b, _ := c.GetBool(tc.testKey) 78 | if b != tc.resultBool { 79 | t.Fatalf("%s: GetBool(%v) should return %v; got %v", tc.name, tc.testKey, tc.resultBool, b) 80 | } 81 | ms, _ := c.GetMapSlice(tc.testKey) 82 | testMapSlice(ms, tc.resultMapSlice, t) 83 | }) 84 | } 85 | } 86 | 87 | func testMapSlice(ms []map[string]interface{}, resultSlice []map[string]interface{}, t *testing.T) { 88 | if ms != nil && resultSlice != nil { 89 | for x := range resultSlice { 90 | if ms[x] == nil { 91 | t.Errorf("GetMapSlice should return %v; got %v", resultSlice, ms) 92 | } 93 | } 94 | } 95 | } 96 | 97 | func TestBroken(t *testing.T) { 98 | _, err := NewConfig("./broken.json") 99 | if err == nil { 100 | t.Errorf("%s: FileLoadConfig(./broken.json) should get error.", "TestBroken") 101 | } 102 | } 103 | 104 | func TestFNF(t *testing.T) { 105 | _, err := NewConfig("./missing.json") 106 | if err == nil { 107 | t.Errorf("%s: FileLoadConfig(./missing.json) should get error.", "TestFNF") 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /common/jconfig/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "someString": "some string", 3 | "stringNumber": "23", 4 | "smallNumber":40, 5 | "actualNumber":10737418240, 6 | "badIntNumber":2.3, 7 | "someStrings": ["first", "2nd", "c"], 8 | "boolean": true, 9 | "stringBool": "True", 10 | "someMaps": [{"first":1}, {"second": 2}] 11 | } -------------------------------------------------------------------------------- /common/k8s/flexvol/flexvol.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package flexvol 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "github.com/hpe-storage/dory/common/docker/dockervol" 23 | "github.com/hpe-storage/dory/common/linux" 24 | "github.com/hpe-storage/dory/common/util" 25 | "io/ioutil" 26 | "os" 27 | "path/filepath" 28 | "regexp" 29 | "strings" 30 | "time" 31 | ) 32 | 33 | const ( 34 | // InitCommand - Initializes the driver. 35 | InitCommand = "init" 36 | // AttachCommand - Attach the volume specified by the given spec. 37 | AttachCommand = "attach" 38 | //DetachCommand - Detach the volume from the kubelet. 39 | DetachCommand = "detach" 40 | //MountCommand - Mount device mounts the device to a global path which individual pods can then bind mount. 41 | MountCommand = "mount" 42 | //UnmountCommand - Unmounts the filesystem for the device. 43 | UnmountCommand = "unmount" 44 | //GetVolumeNameCommand - Get the name of the volume. 45 | GetVolumeNameCommand = "getvolumename" 46 | //SuccessStatus indicates success 47 | SuccessStatus = "Success" 48 | //FailureStatus indicates failure 49 | FailureStatus = "Failure" 50 | //NotSupportedStatus indicates not supported 51 | NotSupportedStatus = "Not supported" 52 | //FailureJSON is a pre-marshalled response used in the case of a marshalling error 53 | FailureJSON = "{\"status\":\"Failure\",\"message\":\"Unknown error.\"}" 54 | //mountPathRegex describes the uuid and flexvolume name in the path 55 | //examples: 56 | // /var/lib/origin/openshift.local.volumes/pods/88917cdb-514d-11e7-93fb-5254005e615a/volumes/hpe~nimble/test2 57 | // /var/lib/kubelet/pods/fb36bec9-51f7-11e7-8eb8-005056968cbc/volumes/hpe~nimble/test 58 | mountPathRegex = "/var/lib/.*/pods/(?P[\\w\\d-]*)/volumes/" 59 | //docker volume status key 60 | devicePathKey = "devicePath" 61 | maxTries = 3 62 | notMounted = "not mounted" 63 | noFileOrDirErr = "no such file or directory" 64 | ) 65 | 66 | var ( 67 | //createVolumes indicate whether the driver should create missing volumes 68 | createVolumes = true 69 | 70 | execPath string 71 | 72 | dvp *dockervol.DockerVolumePlugin 73 | ) 74 | 75 | // Response containers the required information for each invocation 76 | type Response struct { 77 | //"status": "", 78 | Status string `json:"status"` 79 | //"message": "", 80 | Message string `json:"message,omitempty"` 81 | //"device": "" 82 | Device string `json:"device,omitempty"` 83 | //"volumeName:" "undocumented" 84 | VolumeName string `json:"volumeName,omitempty"` 85 | //"attached": 86 | Attached bool `json:"attached,omitempty"` 87 | //Capabilities reported on Driver init 88 | DriverCapabilities map[string]bool `json:"capabilities,omitempty"` 89 | } 90 | 91 | //AttachRequest is used to create a volume if one with this name doesn't exist 92 | type AttachRequest struct { 93 | Name string 94 | PvOrVolumeName string `json:"kubernetes.io/pvOrVolumeName,omitempty"` 95 | FsType string `json:"kubernetes.io/fsType,omitempty"` 96 | ReadWrite string `json:"kubernetes.io/readwrite,omitempty"` 97 | } 98 | 99 | func (ar *AttachRequest) getBestName() string { 100 | if ar.Name != "" { 101 | return ar.Name 102 | } 103 | return ar.PvOrVolumeName 104 | } 105 | 106 | // Config controls the docker behavior 107 | func Config(ePath string, options *dockervol.Options) (err error) { 108 | dvp, err = dockervol.NewDockerVolumePlugin(options) 109 | createVolumes = options.CreateVolumes 110 | execPath = ePath 111 | return err 112 | } 113 | 114 | // BuildJSONResponse marshals a message into the FlexVolume JSON Response. 115 | // If error is not nil, the default Failure message is returned. 116 | func BuildJSONResponse(response *Response) string { 117 | if len(response.Status) < 1 { 118 | response.Status = NotSupportedStatus 119 | } 120 | 121 | jmess, err := json.Marshal(response) 122 | if err != nil { 123 | return FailureJSON 124 | } 125 | return string(jmess) 126 | } 127 | 128 | // ErrorResponse creates a Response with Status and Message set. 129 | func ErrorResponse(err error) *Response { 130 | response := &Response{ 131 | Status: FailureStatus, 132 | } 133 | response.Message = err.Error() 134 | return response 135 | } 136 | 137 | //Get a volume (create if necessary) This was added to k8s 1.6 138 | func Get(jsonRequest string) (string, error) { 139 | util.LogInfo.Printf("get called with (%s)\n", jsonRequest) 140 | req := &AttachRequest{} 141 | err := json.Unmarshal([]byte(jsonRequest), req) 142 | if err != nil { 143 | return "", err 144 | } 145 | name, err := getOrCreate(req.getBestName(), jsonRequest) 146 | if err != nil { 147 | return "", err 148 | } 149 | response := &Response{ 150 | Status: SuccessStatus, 151 | VolumeName: name, 152 | } 153 | return BuildJSONResponse(response), nil 154 | } 155 | 156 | //Attach doesn't attach a volume. It simply creates a volume if necessary. It then returns "Not Supported". 157 | //This worked well in 1.5 in that it broke the create and mount into 2 timeout windows, but 158 | //this has changed in 1.6. 159 | func Attach(jsonRequest string) (string, error) { 160 | util.LogDebug.Printf("attach called with %s\n", jsonRequest) 161 | req := &AttachRequest{} 162 | err := json.Unmarshal([]byte(jsonRequest), req) 163 | if err != nil { 164 | return "", err 165 | } 166 | 167 | _, err = getOrCreate(req.getBestName(), jsonRequest) 168 | if err != nil { 169 | return "", err 170 | } 171 | 172 | return BuildJSONResponse(&Response{Status: NotSupportedStatus, Message: "Not supported."}), nil 173 | } 174 | 175 | func getOrCreate(name, jsonRequest string) (string, error) { 176 | util.LogDebug.Printf("getOrCreate called with %s and %s\n", name, jsonRequest) 177 | volume, err := getVolume(name) 178 | if err != nil || volume.Volume.Name != name { 179 | if !createVolumes { 180 | return "", fmt.Errorf("configured to NOT create volumes") 181 | } 182 | 183 | util.LogInfo.Printf("volume %s was not found(err=%v), creating a new volume using %v", name, err, jsonRequest) 184 | var options map[string]interface{} 185 | err := json.Unmarshal([]byte(jsonRequest), &options) 186 | if err != nil { 187 | util.LogError.Printf("unable to unmarshal options for %v - %s", jsonRequest, err.Error()) 188 | return "", err 189 | } 190 | newName, err := dvp.Create(name, options) 191 | util.LogDebug.Printf("getOrCreate returning %v for %s", newName, name) 192 | if err != nil { 193 | return "", err 194 | } 195 | return newName, nil 196 | } 197 | 198 | return volume.Volume.Name, nil 199 | } 200 | 201 | // wrapper for dvp.Get() with retries incorporated 202 | func getVolume(name string) (volume *dockervol.GetResponse, err error) { 203 | util.LogDebug.Printf("getVolume called with %s", name) 204 | try := 0 205 | for { 206 | util.LogDebug.Printf("dvp.Get() called with %s try:%d", name, try+1) 207 | volume, err = dvp.Get(name) 208 | util.LogDebug.Printf("volume returned from dvp.Get() is %#v", volume) 209 | if volume != nil { 210 | return volume, nil 211 | } 212 | if err != nil { 213 | if try < maxTries { 214 | try++ 215 | time.Sleep(time.Duration(try) * time.Second) 216 | continue 217 | } 218 | return nil, err 219 | } 220 | return volume, nil 221 | } 222 | } 223 | 224 | //Mount a volume 225 | func Mount(args []string) (string, error) { 226 | util.LogDebug.Printf("mount called with %v\n", args) 227 | err := ensureArg("mount", args, 2) 228 | if err != nil { 229 | return "", err 230 | } 231 | 232 | req := &AttachRequest{} 233 | //json seems to be in the second or third argument 234 | jsonRequest, err := findJSON(args, req) 235 | if err != nil { 236 | return "", err 237 | } 238 | 239 | dockerVolName := req.getBestName() 240 | _, err = getOrCreate(dockerVolName, jsonRequest) 241 | if err != nil { 242 | return "", err 243 | } 244 | 245 | mountID, err := getMountID(args[0]) 246 | if err != nil { 247 | return "", err 248 | } 249 | 250 | path, err := dvp.Mount(dockerVolName, mountID) 251 | if err != nil { 252 | return "", err 253 | } 254 | 255 | //Mkdir 256 | err = os.MkdirAll(args[0], 0755) 257 | if err != nil { 258 | return "", err 259 | } 260 | 261 | err = doMount(args[0], path, dockerVolName, mountID) 262 | if err != nil { 263 | return "", err 264 | } 265 | 266 | // Set selinux context if configured 267 | // References: 268 | // https://github.com/kubernetes/kubernetes/issues/20813 269 | // https://github.com/openshift/origin/issues/741 270 | // https://github.com/projectatomic/atomic-site/blob/master/source/blog/2015-06-15-using-volumes-with-docker-can-cause-problems-with-selinux.html.md 271 | err = linux.Chcon("svirt_sandbox_file_t", path) 272 | if err != nil { 273 | return "", err 274 | } 275 | 276 | return BuildJSONResponse(&Response{Status: SuccessStatus}), nil 277 | } 278 | 279 | // Unmount a volume 280 | //nolint :gocyclo 281 | func Unmount(args []string) (string, error) { 282 | util.LogDebug.Printf("Unmount called with %v", args) 283 | mountID, err := getMountID(args[0]) 284 | if err != nil { 285 | return "", err 286 | } 287 | 288 | devPath, err := linux.GetDeviceFromMountPoint(args[0]) 289 | if err != nil { 290 | return "", err 291 | } 292 | 293 | util.LogDebug.Printf("Umount of \"%s\" from %s", args[0], devPath) 294 | err = linux.BindUnmount(args[0]) 295 | if err != nil && !strings.Contains(err.Error(), notMounted) { 296 | return "", err 297 | } 298 | 299 | dockerPath, metadata, err := retryGetDockerPathAndMetadata(args[0], devPath) 300 | if err != nil && !strings.Contains(strings.ToLower(err.Error()), noFileOrDirErr) { 301 | return "", err 302 | } 303 | 304 | dockerVolumeName, err := retryGetVolumeNameFromMountPath(args[0], dockerPath) 305 | if err != nil { 306 | return "", err 307 | } 308 | 309 | util.LogDebug.Printf("docker unmount of %s %s", dockerVolumeName, mountID) 310 | err = dvp.Unmount(dockerVolumeName, mountID) 311 | if err != nil && !strings.Contains(err.Error(), notMounted) { 312 | return "", err 313 | } 314 | 315 | if metadata != "" { 316 | dockerVolumeName, err = getVolumeNameFromMountPath(args[0], dockerPath) 317 | if err != nil { 318 | // an error means that we didn't find the volume mounted 319 | // this means we can clean up the breadcrumbs 320 | util.LogDebug.Printf("Unmount: removing metadata=%s", metadata) 321 | os.Remove(metadata) 322 | } 323 | util.LogDebug.Printf("Unmount: dockerVolumeName=%s still has an active mount at %s.", dockerVolumeName, dockerPath) 324 | } 325 | 326 | return BuildJSONResponse(&Response{Status: SuccessStatus}), nil 327 | } 328 | 329 | // retry getVolumeNameFromMountPath for maxTries 330 | func retryGetVolumeNameFromMountPath(k8sPath, dockerPath string) (string, error) { 331 | util.LogDebug.Printf("retryGetVolumeNameFromMountPath called with %s %s", k8sPath, dockerPath) 332 | try := 0 333 | for { 334 | util.LogDebug.Printf("getVolumeNameFromMountPath called with %s %s try:%d", k8sPath, dockerPath, try+1) 335 | dockerVolumeName, err := getVolumeNameFromMountPath(k8sPath, dockerPath) 336 | if err != nil { 337 | if try < maxTries { 338 | try++ 339 | time.Sleep(time.Duration(try) * time.Second) 340 | continue 341 | } 342 | return "", err 343 | } 344 | util.LogDebug.Printf("dockerVolumeName %s found at k8sPath :%s", dockerVolumeName, k8sPath) 345 | return dockerVolumeName, nil 346 | } 347 | } 348 | 349 | func getMountID(path string) (string, error) { 350 | util.LogDebug.Printf("getMountID called with %v\n", path) 351 | r := regexp.MustCompile(mountPathRegex) 352 | groups := r.FindStringSubmatch(path) 353 | if len(groups) < 2 { 354 | return "", fmt.Errorf("unable to split %s", path) 355 | } 356 | util.LogDebug.Printf("getMountID returning \"%s\"", groups[1]) 357 | return groups[1], nil 358 | 359 | } 360 | 361 | //nolint : gocyclo 362 | func getVolumeNameFromMountPath(k8sPath, dockerPath string) (string, error) { 363 | util.LogDebug.Printf("getVolumeNameFromMountPath called with %s and %s", k8sPath, dockerPath) 364 | // sometimes the dockerPath is empty in case of failover/failback scenarios for OSP 3.11 and greater make sure we return the volume if it exists mounted 365 | if dockerPath == "" && k8sPath != "" { 366 | //if docker path is empty but k8sPath exist, try to use that to the unmount try to use k8s path for volume name 367 | volNames := strings.Split(k8sPath, "/") 368 | if len(volNames) == 0 || volNames[len(volNames)-1] == "" { 369 | return "", fmt.Errorf("no volume found from k8s path %s", k8sPath) 370 | } 371 | return volNames[len(volNames)-1], nil 372 | } 373 | name := filepath.Base(dockerPath) 374 | dockerVolume, err := getVolume(name) 375 | util.LogDebug.Printf("retrieved dockerVolume %#v with name %s", dockerVolume, name) 376 | if err != nil || dockerVolume.Volume.Name != name { 377 | // The docker plugin might not use the docker volume name in the path. 378 | // Therefore we need to look through the know volumes to find out who 379 | // is mounted at that path. 380 | volumes, err2 := dvp.List() 381 | if err2 != nil { 382 | util.LogError.Printf("Unable to get list of volumes. - %s, get error was %s", err2, err) 383 | return "", err 384 | } 385 | for _, vol := range volumes.Volumes { 386 | if vol.Mountpoint == dockerPath { 387 | return vol.Name, nil 388 | } 389 | } 390 | return "", fmt.Errorf("unable to find docker volume for %s. No docker volume claimed to be mounted at %s", k8sPath, dockerPath) 391 | } 392 | if dockerVolume.Volume.Mountpoint == "" { 393 | // it could be mounted by other host, so don't treat it as an error 394 | util.LogDebug.Printf("found a docker volume but its MountPoint was \"\", checking from /proc/mounts") 395 | devPath, _ := linux.GetDeviceFromMountPoint(dockerPath) 396 | util.LogDebug.Printf("devPath %s was found for volume %s since MountPoint was \"\" in dockerVolume status", devPath, dockerVolume.Volume.Name) 397 | if devPath != "" { 398 | util.LogDebug.Printf("devPath %s for docker volume %s", devPath, dockerVolume.Volume.Name) 399 | return dockerVolume.Volume.Name, nil 400 | } 401 | return "", fmt.Errorf("found a docker volume but its MountPoint was \"\"") 402 | } 403 | return dockerVolume.Volume.Name, nil 404 | } 405 | 406 | func retryGetDockerPathAndMetadata(flexvolPath, devPath string) (string, string, error) { 407 | util.LogDebug.Printf("retryGetDockerPathAndMetadata called with flexvolPath(%s) devPath(%s)", flexvolPath, devPath) 408 | maxTries := 3 409 | try := 0 410 | for { 411 | dockerPath, metadata, err := getDockerPathAndMetadata(flexvolPath, devPath) 412 | if err != nil { 413 | util.LogError.Printf("getDockerPathAndMetadata failed for flexvolPath %s, devPath %s : %s", flexvolPath, devPath, err.Error()) 414 | if try < maxTries { 415 | try++ 416 | util.LogDebug.Printf("try=%d", try) 417 | time.Sleep(time.Duration(try) * time.Second) 418 | continue 419 | } 420 | return dockerPath, metadata, err 421 | } 422 | if err != nil { 423 | return dockerPath, metadata, err 424 | } 425 | return dockerPath, metadata, nil 426 | } 427 | } 428 | 429 | func findJSON(args []string, req *AttachRequest) (string, error) { 430 | var err error 431 | for i := 1; i < len(args); i++ { 432 | util.LogDebug.Printf("findJSON(%d) about to unmarshal %v", i, args[i]) 433 | err = json.Unmarshal([]byte(args[i]), req) 434 | if err == nil { 435 | return args[i], nil 436 | } 437 | } 438 | return "", err 439 | } 440 | 441 | // return the dockerPath and a path to the metadata file if present 442 | func getDockerPathAndMetadata(flexvolPath, devPath string) (string, string, error) { 443 | dockerPath, err := linux.GetMountPointFromDevice(devPath) 444 | if err != nil { 445 | return "", "", err 446 | } 447 | util.LogDebug.Printf("getDockerPathAndMetadata: devPath=%s was mounted on dockerPath=%s", devPath, dockerPath) 448 | 449 | metadata := "" 450 | if dockerPath == "" { 451 | // if we didn't get a docker path its because we're running 452 | // in a different namespace (likely rkt) 453 | util.LogInfo.Printf("getDockerPathAndMetadata: didn't find a docker path for devPath=%s and flexvolPath=%s", devPath, flexvolPath) 454 | 455 | metadata, err = getMountMetadataPath(flexvolPath) 456 | if err != nil { 457 | util.LogError.Printf("getDockerPathAndMetadata: unable to read metadata=%s for devPath=%s and flexvolPath=%s", metadata, devPath, flexvolPath) 458 | return "", "", err 459 | } 460 | 461 | var fileData []byte 462 | fileData, err = ioutil.ReadFile(metadata) 463 | if err != nil { 464 | util.LogError.Printf("getDockerPathAndMetadata: unable to read file content from metadata=%s for devPath=%s and flexvolPath=%s", metadata, dockerPath, flexvolPath) 465 | return "", "", err 466 | } 467 | dockerPath = string(fileData) 468 | util.LogDebug.Printf("getDockerPathAndMetadata: found dockerPath=%s for devPath=%s and flexvolPath=%s", dockerPath, devPath, flexvolPath) 469 | } 470 | util.LogDebug.Printf("getDockerPathAndMetadata: devPath=%s was mounted on dockerPath=%s", devPath, dockerPath) 471 | return dockerPath, metadata, nil 472 | } 473 | 474 | func doMount(flexvolPath, dockerPath, dockerName, mountID string) error { 475 | devPath, err := linux.GetDeviceFromMountPoint(dockerPath) 476 | if err != nil { 477 | return err 478 | } 479 | 480 | if devPath == "" { 481 | //we're probably running in a different namespace 482 | //so we need to pull the device path from the 483 | //docker volume driver 484 | util.LogInfo.Printf("doMount: devPath was empty for flexvolPath=% volume=%s", flexvolPath, dockerName) 485 | 486 | //get the volume info 487 | var volRes *dockervol.GetResponse 488 | volRes, err = dvp.Get(dockerName) 489 | if err != nil { 490 | return err 491 | } 492 | 493 | devPath, found := volRes.Volume.Status[devicePathKey].(string) 494 | if !found || devPath == "" { 495 | util.LogError.Printf("Unable to get device for flexvolPath=%s from docker volume=%+v (path=%s)", flexvolPath, volRes, dockerPath) 496 | return fmt.Errorf("Unable to get device for flexvolPath=%s from docker volume=%s", flexvolPath, dockerPath) 497 | } 498 | util.LogDebug.Printf("doMount: found devPath=%s for volume=%s", devPath, dockerName) 499 | 500 | //mount devicePath onto flexvolPath 501 | _, err = linux.MountDeviceWithFileSystem(devPath, flexvolPath) 502 | if err != nil { 503 | return err 504 | } 505 | util.LogDebug.Printf("doMount: mounted devPath=%s at flexvolPath=%s", devPath, flexvolPath) 506 | 507 | //create a hidden file in the flexvolume path that maps flexvolume mount to the docker volume (breadcrumb) 508 | var metadata string 509 | metadata, err = getMountMetadataPath(flexvolPath) 510 | if err != nil { 511 | return err 512 | } 513 | err = ioutil.WriteFile(metadata, []byte(dockerPath), 0600) 514 | if err != nil { 515 | return err 516 | } 517 | util.LogDebug.Printf("doMount: stored dockerPath=%s at metadata=%s", dockerPath, metadata) 518 | 519 | } else { 520 | //bind mount the docker path to the flexvol path 521 | err = linux.BindMount(dockerPath, flexvolPath, false) 522 | util.LogDebug.Printf("doMount: bind mounted dockerPath=%s at flexvolPath=%s", dockerPath, flexvolPath) 523 | } 524 | 525 | return err 526 | } 527 | 528 | func getMountMetadataPath(flexvolPath string) (string, error) { 529 | _, flexvolFilename := filepath.Split(flexvolPath) 530 | if flexvolFilename == "" { 531 | return "", fmt.Errorf("unable to get filename from %s", flexvolPath) 532 | } 533 | execPathDir, _ := filepath.Split(execPath) 534 | if flexvolFilename == "" { 535 | return "", fmt.Errorf("unable to get dir from %s", execPath) 536 | } 537 | return fmt.Sprintf("%s.%s", execPathDir, flexvolFilename), nil 538 | } 539 | -------------------------------------------------------------------------------- /common/k8s/flexvol/handler.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package flexvol 18 | 19 | import ( 20 | "fmt" 21 | "github.com/hpe-storage/dory/common/util" 22 | ) 23 | 24 | // Handle the conversion of flexvol commands and args to docker volume 25 | func Handle(driverCommand string, enable16 bool, args []string) string { 26 | if driverCommand == InitCommand { 27 | util.LogDebug.Print("enable1.6=", enable16) 28 | if enable16 { 29 | return BuildJSONResponse(&Response{Status: SuccessStatus}) 30 | } 31 | capabilities := map[string]bool{"attach": false} 32 | return BuildJSONResponse(&Response{Status: SuccessStatus, DriverCapabilities: capabilities}) 33 | } 34 | 35 | err := ensureArg(driverCommand, args, 1) 36 | if err != nil { 37 | return BuildJSONResponse(ErrorResponse(err)) 38 | } 39 | 40 | switch driverCommand { 41 | case AttachCommand: 42 | return attachVolume(args[0]) 43 | case MountCommand: 44 | return mountVolume(args) 45 | case UnmountCommand: 46 | return unmountVolume(args) 47 | default: 48 | util.LogError.Printf("Unsupported command (%s) was called with %s\n", driverCommand, args) 49 | } 50 | return BuildJSONResponse(&Response{Status: NotSupportedStatus, Message: "Not supported."}) 51 | } 52 | 53 | func attachVolume(json string) string { 54 | mess, err := Attach(json) 55 | if err != nil { 56 | return BuildJSONResponse(ErrorResponse(err)) 57 | } 58 | return mess 59 | } 60 | 61 | func mountVolume(args []string) string { 62 | mess, err := Mount(args) 63 | if err != nil { 64 | return BuildJSONResponse(ErrorResponse(err)) 65 | } 66 | return mess 67 | } 68 | 69 | func unmountVolume(args []string) string { 70 | mess, err := Unmount(args) 71 | if err != nil { 72 | return BuildJSONResponse(ErrorResponse(err)) 73 | } 74 | return mess 75 | } 76 | 77 | func ensureArg(driverCommand string, args []string, number int) error { 78 | if len(args) < number { 79 | return fmt.Errorf("Not enough arguments for %s", driverCommand) 80 | } 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /common/k8s/flexvol/handler_test.go: -------------------------------------------------------------------------------- 1 | package flexvol 2 | 3 | import "testing" 4 | 5 | func TestInitCapabilities(t *testing.T) { 6 | expectedInitResponse := "{\"status\":\"Success\",\"capabilities\":{\"attach\":false}}" 7 | result := Handle("init", false, []string{"foo", "bar"}) 8 | if result != expectedInitResponse { 9 | t.Errorf("init response mismatch. Expected " + expectedInitResponse + " got " + result) 10 | } 11 | } 12 | 13 | func TestInitCapabilitiesWith16(t *testing.T) { 14 | expectedInitResponse := "{\"status\":\"Success\"}" 15 | result := Handle("init", true, []string{"foo", "bar"}) 16 | if result != expectedInitResponse { 17 | t.Errorf("init response mismatch. Expected " + expectedInitResponse + " got " + result) 18 | } 19 | } 20 | 21 | func TestInvalidCommand(t *testing.T) { 22 | expectedResponse := "{\"status\":\"Not supported\",\"message\":\"Not supported.\"}" 23 | result := Handle("blahblah", false, []string{"foo", "bar"}) 24 | if result != expectedResponse { 25 | t.Errorf("unsupported command expected response " + expectedResponse + " got " + result) 26 | } 27 | } 28 | 29 | func TestEnsureArgs(t *testing.T) { 30 | size := 1 31 | err := ensureArg("init", []string{"foo", "bar"}, size) 32 | if err != nil { 33 | t.Errorf("args size is less than" + string(size)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /common/k8s/provisioner/claim.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package provisioner 18 | 19 | import ( 20 | "fmt" 21 | "github.com/hpe-storage/dory/common/util" 22 | api_v1 "k8s.io/api/core/v1" 23 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/runtime" 25 | "k8s.io/apimachinery/pkg/watch" 26 | "k8s.io/client-go/tools/cache" 27 | "strings" 28 | ) 29 | 30 | func (p *Provisioner) listAllClaims(options meta_v1.ListOptions) (runtime.Object, error) { 31 | return p.kubeClient.Core().PersistentVolumeClaims(meta_v1.NamespaceAll).List(options) 32 | } 33 | 34 | func (p *Provisioner) watchAllClaims(options meta_v1.ListOptions) (watch.Interface, error) { 35 | return p.kubeClient.Core().PersistentVolumeClaims(meta_v1.NamespaceAll).Watch(options) 36 | } 37 | 38 | //NewClaimController provides a controller that watches for PersistentVolumeClaims and takes action on them 39 | func (p *Provisioner) newClaimController() (cache.Store, cache.Controller) { 40 | claimListWatch := &cache.ListWatch{ 41 | ListFunc: p.listAllClaims, 42 | WatchFunc: p.watchAllClaims, 43 | } 44 | 45 | return cache.NewInformer( 46 | claimListWatch, 47 | &api_v1.PersistentVolumeClaim{}, 48 | resyncPeriod, 49 | cache.ResourceEventHandlerFuncs{ 50 | AddFunc: p.addedClaim, 51 | UpdateFunc: p.updatedClaim, 52 | }, 53 | ) 54 | } 55 | 56 | func (p *Provisioner) addedClaim(t interface{}) { 57 | claim, err := getPersistentVolumeClaim(t) 58 | if err != nil { 59 | util.LogError.Printf("Failed to get persistent volume claim from %v, %s", t, err.Error()) 60 | return 61 | } 62 | go p.processAddedClaim(claim) 63 | } 64 | 65 | func (p *Provisioner) processAddedClaim(claim *api_v1.PersistentVolumeClaim) { 66 | // is this a state we can do anything about 67 | if claim.Status.Phase != api_v1.ClaimPending { 68 | util.LogInfo.Printf("pvc %s was not in pending phase. current phase=%s - skipping", claim.Name, claim.Status.Phase) 69 | return 70 | } 71 | 72 | // is this a class we support 73 | className := getClaimClassName(claim) 74 | class, err := p.getClass(className) 75 | if err != nil { 76 | util.LogError.Printf("error getting class named %s for pvc %s. err=%v", className, claim.Name, err) 77 | return 78 | } 79 | if !strings.HasPrefix(class.Provisioner, p.namePrefix) { 80 | util.LogInfo.Printf("class named %s in pvc %s did not refer to a supported provisioner (name must begin with %s). current provisioner=%s - skipping", className, claim.Name, p.namePrefix, class.Provisioner) 81 | return 82 | } 83 | 84 | util.LogInfo.Printf("processAddedClaim: provisioner:%s pvc:%s class:%s", class.Provisioner, claim.Name, className) 85 | p.addMessageChan(fmt.Sprintf("%s", claim.UID), nil) 86 | p.provisionVolume(claim, class) 87 | } 88 | 89 | func (p *Provisioner) updatedClaim(oldT interface{}, newT interface{}) { 90 | claim, err := getPersistentVolumeClaim(newT) 91 | if err != nil { 92 | util.LogError.Printf("Oops - %s\n", err.Error()) 93 | return 94 | } 95 | util.LogDebug.Printf("updatedClaim: pvc %s current phase=%s", claim.Name, claim.Status.Phase) 96 | go p.sendUpdate(claim) 97 | } 98 | 99 | func getClaimClassName(claim *api_v1.PersistentVolumeClaim) (name string) { 100 | name, beta := claim.Annotations[api_v1.BetaStorageClassAnnotation] 101 | 102 | //if no longer in beta 103 | if !beta && claim.Spec.StorageClassName != nil { 104 | name = *claim.Spec.StorageClassName 105 | } 106 | return name 107 | } 108 | 109 | func getClaimMatchLabels(claim *api_v1.PersistentVolumeClaim) map[string]string { 110 | if claim.Spec.Selector == nil || claim.Spec.Selector.MatchLabels == nil { 111 | return map[string]string{} 112 | } 113 | return claim.Spec.Selector.MatchLabels 114 | } 115 | 116 | func getPersistentVolumeClaim(t interface{}) (*api_v1.PersistentVolumeClaim, error) { 117 | switch t := t.(type) { 118 | default: 119 | return nil, fmt.Errorf("unexpected type %T for %v", t, t) 120 | case *api_v1.PersistentVolumeClaim: 121 | return t, nil 122 | case api_v1.PersistentVolumeClaim: 123 | return &t, nil 124 | } 125 | } 126 | 127 | func (p *Provisioner) getClaimFromPVCName(nameSpace, claimName string) (*api_v1.PersistentVolumeClaim, error) { 128 | util.LogDebug.Printf("getClaimFromPVCNames called with %s/%s", nameSpace, claimName) 129 | if p.claimsStore == nil { 130 | return nil, fmt.Errorf("requested pvc %s/%s was not found because claimStore was nil", nameSpace, claimName) 131 | } 132 | if len(p.claimsStore.List()) < 1 { 133 | return nil, fmt.Errorf("requested pvc %s/%s was not found", nameSpace, claimName) 134 | } 135 | claimInterface, found, err := p.claimsStore.GetByKey(nameSpace + "/" + claimName) 136 | if err != nil { 137 | util.LogError.Printf("Error to retrieve pvc %s/%s : %s", nameSpace, claimName, err.Error()) 138 | return nil, fmt.Errorf("Error to retrieve pvc %s/%s : %s", nameSpace, claimName, err.Error()) 139 | } 140 | if !found { 141 | util.LogError.Printf("requested pvc %s/%s was not found", nameSpace, claimName) 142 | return nil, fmt.Errorf("requested pvc %s/%s was not found", nameSpace, claimName) 143 | } 144 | var claim *api_v1.PersistentVolumeClaim 145 | claim, err = getPersistentVolumeClaim(claimInterface) 146 | if err != nil { 147 | util.LogError.Printf("requested pvc %s/%s was not found : %s", nameSpace, claimName, err.Error()) 148 | return nil, fmt.Errorf("requested pvc %s/%s was not found : %s", nameSpace, claimName, err.Error()) 149 | } 150 | util.LogDebug.Printf("claim found namespace :%s name: %s", claim.Namespace, claim.Name) 151 | 152 | return claim, nil 153 | } 154 | 155 | func (p *Provisioner) getClaimOverrideOptions(claim *api_v1.PersistentVolumeClaim, overrides []string, optionsMap map[string]interface{}) (map[string]interface{}, error) { 156 | util.LogDebug.Printf("handling getClaimOverrideOptions for %s", p.namePrefix) 157 | provisionerName := p.namePrefix 158 | for _, override := range overrides { 159 | util.LogDebug.Printf("handling override :%v", override) 160 | for key, annotation := range claim.Annotations { 161 | util.LogDebug.Printf("handling annotation key :%v val :%v", key, annotation) 162 | if strings.HasPrefix(strings.ToLower(key), provisionerName+strings.ToLower(override)) { 163 | util.LogDebug.Printf("annotation key :%v val :%v matched override :%v", key, annotation, override) 164 | if valOpt, ok := optionsMap[override]; ok { 165 | util.LogInfo.Printf("key %v exist with val %v, overriding with pvc annotation %v", override, valOpt, annotation) 166 | } 167 | util.LogDebug.Printf("adding key %v val :%v to docker opts", override, annotation) 168 | optionsMap[override] = annotation 169 | } 170 | } 171 | } 172 | 173 | // check to see if there is cloneOfPVC annotation present 174 | volumeName, err := p.getPVFromPVCAnnotation(claim) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | // update the options map with the pv name if there is one 180 | if volumeName != "" { 181 | optionsMap[cloneOf] = volumeName 182 | } 183 | 184 | // make sure cloneOfPVC is removed from the options (all cases) 185 | if _, found := optionsMap[cloneOfPVC]; found { 186 | delete(optionsMap, cloneOfPVC) 187 | } 188 | return optionsMap, nil 189 | } 190 | 191 | func (p *Provisioner) getClaimNameSpace(claim *api_v1.PersistentVolumeClaim) string { 192 | nameSpace := "default" 193 | if claim != nil && claim.Namespace != "" { 194 | nameSpace = claim.Namespace 195 | } 196 | util.LogDebug.Printf("namespace of the claim %s is : %s", claim.Name, nameSpace) 197 | return nameSpace 198 | } 199 | 200 | func (p *Provisioner) getPVFromPVCAnnotation(claim *api_v1.PersistentVolumeClaim) (string, error) { 201 | // check to see if we have the cloneOfPVC annotation 202 | pvcToClone, foundClonePVC := claim.Annotations[fmt.Sprintf("%s%s", p.namePrefix, cloneOfPVC)] 203 | if !foundClonePVC { 204 | return "", nil 205 | } 206 | 207 | namespace := p.getClaimNameSpace(claim) 208 | util.LogDebug.Printf("%s%s: %s was found in claim annotations", p.namePrefix, cloneOfPVC, pvcToClone) 209 | 210 | pvName, err := p.getVolumeNameFromClaimName(namespace, pvcToClone) 211 | util.LogDebug.Printf("pvc %s/%s maps to pv %s", namespace, pvcToClone, pvName) 212 | if err != nil { 213 | return "", fmt.Errorf("unable to retrieve pvc %s/%s : %s", namespace, pvcToClone, err.Error()) 214 | } 215 | if pvName == "" { 216 | return "", fmt.Errorf("unable to retrieve pvc %s/%s : not found", namespace, pvcToClone) 217 | } 218 | return pvName, nil 219 | } 220 | -------------------------------------------------------------------------------- /common/k8s/provisioner/claim_test.go: -------------------------------------------------------------------------------- 1 | package provisioner 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var annotationTests = []struct { 8 | name string 9 | annotation string 10 | pvcName string 11 | err bool 12 | pvName string 13 | }{ 14 | {"test/no annotation", "", "", false, ""}, 15 | {"test/wrong annotation", "foo.com/" + cloneOfPVC, "cloneMe", false, ""}, 16 | {"test/annotation", "dory/" + cloneOfPVC, "cloneMe", true, ""}, 17 | } 18 | 19 | func TestPVCCloneOf(t *testing.T) { 20 | p := getTestProvisioner() 21 | 22 | for _, tc := range annotationTests { 23 | t.Run(tc.name, func(t *testing.T) { 24 | claim := getTestPVC() 25 | if tc.annotation != "" { 26 | claim.Annotations[tc.annotation] = tc.pvcName 27 | } 28 | 29 | pvName, err := p.getPVFromPVCAnnotation(claim) 30 | if (err != nil) != tc.err { 31 | t.Error( 32 | "For:", "getPVFromPVCAnnotation", 33 | "expected:", "No error", 34 | "got:", err, 35 | ) 36 | } 37 | if pvName != tc.pvName { 38 | t.Error( 39 | "For:", "pvName", 40 | "expected:", "", 41 | "got:", pvName, 42 | ) 43 | } 44 | 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /common/k8s/provisioner/class.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package provisioner 18 | 19 | import ( 20 | "fmt" 21 | "github.com/hpe-storage/dory/common/util" 22 | storage_v1 "k8s.io/api/storage/v1" 23 | storage_v1beta1 "k8s.io/api/storage/v1beta1" 24 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | "k8s.io/apimachinery/pkg/watch" 27 | "k8s.io/client-go/kubernetes" 28 | "k8s.io/client-go/tools/cache" 29 | "strings" 30 | ) 31 | 32 | func (p *Provisioner) listAllClasses(options meta_v1.ListOptions) (runtime.Object, error) { 33 | return p.kubeClient.StorageV1().StorageClasses().List(options) 34 | } 35 | func (p *Provisioner) watchAllClasses(options meta_v1.ListOptions) (watch.Interface, error) { 36 | return p.kubeClient.StorageV1().StorageClasses().Watch(options) 37 | } 38 | 39 | func (p *Provisioner) listBetaAllClasses(options meta_v1.ListOptions) (runtime.Object, error) { 40 | return p.kubeClient.StorageV1beta1().StorageClasses().List(options) 41 | } 42 | func (p *Provisioner) watchBetaAllClasses(options meta_v1.ListOptions) (watch.Interface, error) { 43 | return p.kubeClient.StorageV1beta1().StorageClasses().Watch(options) 44 | } 45 | 46 | //NewClassReflector provides a controller that watches for PersistentVolumeClasss and takes action on them 47 | func (p *Provisioner) newClassReflector(kubeClient *kubernetes.Clientset) (cache.Store, *cache.Reflector) { 48 | classStore := cache.NewStore(cache.DeletionHandlingMetaNamespaceKeyFunc) 49 | var classReflector *cache.Reflector 50 | // In 1.6 and above classes are out of beta 51 | classListWatch := &cache.ListWatch{ 52 | ListFunc: p.listAllClasses, 53 | WatchFunc: p.watchAllClasses, 54 | } 55 | classReflector = cache.NewReflector(classListWatch, &storage_v1.StorageClass{}, classStore, 0) 56 | 57 | // if we're dealing with 1.5, classes are still in beta 58 | if p.serverVersion.Major == "1" && p.serverVersion.Minor == "5" { 59 | classListWatch = &cache.ListWatch{ 60 | ListFunc: p.listBetaAllClasses, 61 | WatchFunc: p.watchBetaAllClasses, 62 | } 63 | classReflector = cache.NewReflector(classListWatch, &storage_v1beta1.StorageClass{}, classStore, 0) 64 | } 65 | 66 | return classStore, classReflector 67 | } 68 | 69 | func (p *Provisioner) getClass(className string) (*storage_v1.StorageClass, error) { 70 | classObj, found, err := p.classStore.GetByKey(className) 71 | if err != nil { 72 | util.LogError.Printf("error getting class named %s. err=%v", className, err) 73 | return nil, err 74 | } 75 | if !found { 76 | util.LogError.Printf("unable to find a class named %s", className) 77 | return nil, fmt.Errorf("unable to find a class named %s", className) 78 | } 79 | return getStorageClass(classObj) 80 | } 81 | 82 | func (p *Provisioner) getClassOverrides(optionsMap map[string]string) []string { 83 | var overrides []string 84 | if val, ok := optionsMap[allowOverrides]; ok { 85 | util.LogDebug.Printf("allowOverrides %s", val) 86 | for k, v := range strings.Split(val, ",") { 87 | // remove leading and trailing spaces from value before Trim (needed to support multiline overrides e.g ", ") 88 | v = strings.TrimSpace(v) 89 | if len(v) > 0 && v != "" { 90 | util.LogDebug.Printf("processing iter :%v value :%v", k, v) 91 | overrides = append(overrides, v) 92 | } 93 | } 94 | } 95 | util.LogDebug.Printf("resulting overrides :%#v dockerOpts :%#v", overrides, optionsMap) 96 | return overrides 97 | } 98 | 99 | func getStorageClass(t interface{}) (*storage_v1.StorageClass, error) { 100 | switch t := t.(type) { 101 | default: 102 | return nil, fmt.Errorf("unexpected type %T for %v", t, t) 103 | case *storage_v1.StorageClass: 104 | return t, nil 105 | case *storage_v1beta1.StorageClass: 106 | return &storage_v1.StorageClass{ 107 | TypeMeta: t.TypeMeta, 108 | ObjectMeta: t.ObjectMeta, 109 | Provisioner: t.Provisioner, 110 | Parameters: t.Parameters, 111 | }, nil 112 | case storage_v1beta1.StorageClass: 113 | return &storage_v1.StorageClass{ 114 | TypeMeta: t.TypeMeta, 115 | ObjectMeta: t.ObjectMeta, 116 | Provisioner: t.Provisioner, 117 | Parameters: t.Parameters, 118 | }, nil 119 | case storage_v1.StorageClass: 120 | return &t, nil 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /common/k8s/provisioner/monitor.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package provisioner 18 | 19 | import ( 20 | "fmt" 21 | "github.com/hpe-storage/dory/common/chain" 22 | "github.com/hpe-storage/dory/common/util" 23 | api_v1 "k8s.io/api/core/v1" 24 | "reflect" 25 | "time" 26 | ) 27 | 28 | type monitorBind struct { 29 | origClaim *api_v1.PersistentVolumeClaim 30 | pChain *chain.Chain 31 | p *Provisioner 32 | vol *api_v1.PersistentVolume 33 | } 34 | 35 | func (m *monitorBind) Name() string { 36 | return reflect.TypeOf(m).Name() 37 | } 38 | 39 | func (m *monitorBind) Run() (name interface{}, err error) { 40 | messChan := m.p.getMessageChan(fmt.Sprintf("%s", m.origClaim.UID)) 41 | 42 | m.vol, _ = getPersistentVolume(m.pChain.GetRunnerOutput("createPersistentVolume")) 43 | if m.vol == nil { 44 | return nil, fmt.Errorf("unable to get volume for pvc %s waiting for bind status (UID=%s)", m.origClaim.Name, m.origClaim.GetUID()) 45 | } 46 | 47 | // add the pv id to the map referencing this channel 48 | m.p.addMessageChan(fmt.Sprintf("%s", m.vol.UID), messChan) 49 | 50 | for name == nil && err == nil { 51 | name, err = m.route(messChan) 52 | } 53 | return name, err 54 | } 55 | 56 | func (m *monitorBind) Rollback() (err error) { 57 | return nil 58 | } 59 | 60 | func (m *monitorBind) route(channel chan *updateMessage) (interface{}, error) { 61 | select { 62 | case message := <-channel: 63 | if message.pvc != nil { 64 | name, err := m.processClaimMessage(message) 65 | return name, err 66 | } 67 | if message.pv != nil { 68 | name, err := m.processVolMessage(message) 69 | return name, err 70 | } 71 | case <-time.After(maxWaitForBind): 72 | return m.processTimeout() 73 | } 74 | return nil, nil 75 | } 76 | 77 | func (m *monitorBind) processClaimMessage(message *updateMessage) (name interface{}, err error) { 78 | claim := message.pvc 79 | util.LogDebug.Printf("pvc %s updated (UID=%s). Status is now %s", claim.Name, claim.GetUID(), claim.Status.Phase) 80 | if claim.Status.Phase == api_v1.ClaimBound { 81 | if m.vol.Name != claim.Spec.VolumeName { 82 | info := fmt.Sprintf("pvc %s was satisfied by %s, the pv provisioned was %s", claim.Name, claim.Spec.VolumeName, m.vol.Name) 83 | m.p.eventRecorder.Event(claim, api_v1.EventTypeWarning, "MonitorBind", info) 84 | // roll back here or our volume will be left hanging 85 | return nil, fmt.Errorf("%s", info) 86 | } 87 | util.LogDebug.Printf("pvc %s was satisfied by pv %s", claim.Name, claim.Spec.VolumeName) 88 | return claim.Spec.VolumeName, nil 89 | } else if claim.Status.Phase == api_v1.ClaimLost { 90 | info := fmt.Sprintf("pvc %s was lost, reverting volume create (UID=%s)", claim.Name, claim.UID) 91 | util.LogError.Print(info) 92 | m.p.eventRecorder.Event(m.origClaim, api_v1.EventTypeWarning, "MonitorBind", info) 93 | // roll back here since the claim was lost 94 | return nil, fmt.Errorf("pvc %s was lost, reverting volume create (UID=%s)", claim.Name, claim.UID) 95 | } 96 | return nil, nil 97 | } 98 | 99 | func (m *monitorBind) processVolMessage(message *updateMessage) (name interface{}, err error) { 100 | volume := message.pv 101 | util.LogDebug.Printf("pv %s updated (UID=%s). Status is now %s", volume.Name, volume.UID, volume.Status.Phase) 102 | switch volume.Status.Phase { 103 | case api_v1.VolumeBound: 104 | if m.origClaim.UID != volume.Spec.ClaimRef.UID { 105 | info := fmt.Sprintf("pv %s satisfied pvc %s (%s), expecting %s", volume.Name, volume.Spec.ClaimRef.Name, volume.Spec.ClaimRef.UID, m.origClaim.Name) 106 | m.p.eventRecorder.Event(volume, api_v1.EventTypeWarning, "MonitorBind", info) 107 | util.LogError.Printf(info) 108 | //don't roll back here since the volume we created is bound to something 109 | return volume.Name, nil 110 | } 111 | util.LogInfo.Printf("pv %s satisfied pvc %s (%s)", volume.Name, volume.Spec.ClaimRef.Name, volume.Spec.ClaimRef.UID) 112 | return volume.Name, nil 113 | 114 | case api_v1.VolumeReleased: 115 | info := fmt.Sprintf("pv %s has been released, claimref was %s (waiting for %s)", volume.Name, volume.Spec.ClaimRef.UID, m.origClaim.UID) 116 | util.LogInfo.Printf(info) 117 | // don't roll back here since the volume will be deleted by the normal workflow 118 | return volume.Name, nil 119 | } 120 | return nil, nil 121 | } 122 | 123 | func (m *monitorBind) processTimeout() (interface{}, error) { 124 | info := fmt.Sprintf("pvc %s timed out waiting for bind status, reverting volume create (UID=%s)", m.origClaim.Name, m.origClaim.UID) 125 | util.LogError.Print(info) 126 | m.p.eventRecorder.Event(m.origClaim, api_v1.EventTypeWarning, "MonitorBind", info) 127 | return nil, fmt.Errorf("pvc %s (%s) not bound after timeout", m.origClaim.Name, m.origClaim.GetUID()) 128 | } 129 | -------------------------------------------------------------------------------- /common/k8s/provisioner/provisioner_test.go: -------------------------------------------------------------------------------- 1 | package provisioner 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hpe-storage/dory/common/docker/dockervol" 6 | api_v1 "k8s.io/api/core/v1" 7 | storage_v1 "k8s.io/api/storage/v1" 8 | resource_v1 "k8s.io/apimachinery/pkg/api/resource" 9 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/client-go/kubernetes" 11 | rest "k8s.io/client-go/rest" 12 | "testing" 13 | ) 14 | 15 | type testDockerOptions struct { 16 | listOfStorageResourceOptions []string 17 | factorForConversion int 18 | claimSize int 19 | } 20 | 21 | func getStorageClassParams() map[string]string { 22 | m := make(map[string]string) 23 | m["size"] = "123" 24 | m["description"] = "dynamic" 25 | return m 26 | } 27 | 28 | func getTestDockerOptions() map[string]testDockerOptions { 29 | m := make(map[string]testDockerOptions) 30 | m["invalidClaim"] = testDockerOptions{ 31 | []string{"size", "sizeInGiB"}, 32 | 0, 33 | -1, 34 | } 35 | m["validClaim"] = testDockerOptions{ 36 | []string{"size", "sizeInGiB"}, 37 | 1073741824, 38 | 16, 39 | } 40 | 41 | m["invalidStorageResources"] = testDockerOptions{ 42 | []string{"invalidSize", "invalidSizeinGiB"}, 43 | 12345, 44 | 16, 45 | } 46 | return m 47 | 48 | } 49 | 50 | func TestDockerOptions(t *testing.T) { 51 | p := getTestProvisioner() 52 | invalidOption := getTestDockerOptions()["invalidClaim"] 53 | outputOption, _ := p.getDockerOptions(getStorageClassParams(), getTestStorageClass(), invalidOption.claimSize, invalidOption.listOfStorageResourceOptions, "default") 54 | if outputOption["size"] != getStorageClassParams()["size"] { 55 | t.Error("size should not be set for invalid claimsize") 56 | } 57 | 58 | invalidOption = getTestDockerOptions()["invalidStorageResources"] 59 | outputOption, _ = p.getDockerOptions(getStorageClassParams(), getTestStorageClass(), invalidOption.claimSize, invalidOption.listOfStorageResourceOptions, "default") 60 | if outputOption["size"] == getStorageClassParams()["size"] { 61 | t.Error("size should set for for invalid listOfStorageResourceOptions but valid claim size") 62 | } 63 | 64 | validOption := getTestDockerOptions()["validClaim"] 65 | outputOption, _ = p.getDockerOptions(getStorageClassParams(), getTestStorageClass(), validOption.claimSize, validOption.listOfStorageResourceOptions, "default") 66 | if outputOption["size"] == getStorageClassParams()["size"] { 67 | t.Error("size should be set for for valid claimsize") 68 | } 69 | 70 | } 71 | 72 | func getTestPVC() *api_v1.PersistentVolumeClaim { 73 | testClaim := new(api_v1.PersistentVolumeClaim) 74 | testClaim.Annotations = make(map[string]string) 75 | testValue := resource_v1.NewQuantity(123456789012345, "BinarySI") 76 | requests := make(api_v1.ResourceList) 77 | requests["storage"] = *testValue 78 | testClaim.Spec.Resources.Requests = requests 79 | testClaim.ObjectMeta.Name = "pvc-test" 80 | testClaim.Namespace = "default" 81 | testClaim.SelfLink = "/api/v1/namespaces/default/persistentvolumeclaims/pvc-test" 82 | testClaim.UID = "29dd7cc4-c319-11e7-83a2-005056860ac5" 83 | selector := new(meta_v1.LabelSelector) 84 | selector.MatchLabels = map[string]string{"foo": "bar"} 85 | testClaim.Spec.Selector = selector 86 | storageClass := "foo" 87 | testClaim.Spec.StorageClassName = &storageClass 88 | return testClaim 89 | } 90 | func TestNonZeroGetClaimSize(t *testing.T) { 91 | claim := getTestPVC() 92 | dockerClient := &dockervol.DockerVolumePlugin{ 93 | ListOfStorageResourceOptions: []string{"size", "sizeInGiB"}, 94 | FactorForConversion: 1073741824} 95 | 96 | val := getClaimSizeForFactor(claim, dockerClient, 0) 97 | if val == 0 { 98 | t.Error("Claim size should be non zero value") 99 | } 100 | } 101 | func TestZeroClaimSize(t *testing.T) { 102 | claim := getTestPVC() 103 | dockerClient := &dockervol.DockerVolumePlugin{ 104 | FactorForConversion: 0, 105 | ListOfStorageResourceOptions: []string{"size", "sizeInGiB"}} 106 | val := getClaimSizeForFactor(claim, dockerClient, 0) 107 | if val != 0 { 108 | t.Error("Claim size should be zero for invalid FactorForConversion") 109 | } 110 | 111 | dockerClient = &dockervol.DockerVolumePlugin{ 112 | FactorForConversion: 1073741824, 113 | ListOfStorageResourceOptions: nil} 114 | val = getClaimSizeForFactor(claim, dockerClient, 0) 115 | if val != 0 { 116 | t.Error("Claim size should be zero for invalid Storage Resource") 117 | } 118 | } 119 | 120 | func getTestStorageClass() *storage_v1.StorageClass { 121 | testClass := new(storage_v1.StorageClass) 122 | testClass.ObjectMeta.Name = "pvc" 123 | testClass.Namespace = "/apis/storage.k8s.io/v1/storageclasses/foo" 124 | testClass.UID = "76fe311a-b511-11e7-8025-005056860ac5" 125 | testClass.ResourceVersion = "78963" 126 | testClass.Generation = 0 127 | testClass.Provisioner = "dory" 128 | testClass.Parameters = make(map[string]string) 129 | return testClass 130 | } 131 | 132 | func getTestProvisioner() *Provisioner { 133 | config := new(rest.Config) 134 | kubeClient, _ := kubernetes.NewForConfig(config) 135 | p := NewProvisioner(kubeClient, "dory", true, true) 136 | return p 137 | } 138 | 139 | func TestGetPersistentVolume(t *testing.T) { 140 | p := getTestProvisioner() 141 | pv, _ := p.newPersistentVolume("pv-test", getStorageClassParams(), getTestPVC(), getTestStorageClass()) 142 | vol, _ := getPersistentVolume(pv) 143 | if vol == nil { 144 | t.Error("unable to retrieve volume from pv interface") 145 | } 146 | } 147 | 148 | func TestGetPersistentVolumeClaim(t *testing.T) { 149 | claim, _ := getPersistentVolumeClaim(getTestPVC()) 150 | if claim == nil { 151 | t.Error("unable to retrieve claim from pvc interface") 152 | } 153 | } 154 | 155 | func TestGetClaimMatchLabels(t *testing.T) { 156 | matchLabels := getClaimMatchLabels(getTestPVC()) 157 | if matchLabels["foo"] != "bar" { 158 | t.Error("unable to retrieve match labels from pvc") 159 | } 160 | } 161 | 162 | func TestGetClaimClassName(t *testing.T) { 163 | name := getClaimClassName(getTestPVC()) 164 | if name == "" { 165 | t.Error("claim name should not be empty for claim", getTestPVC()) 166 | } 167 | } 168 | 169 | func TestGetStorageClass(t *testing.T) { 170 | class, _ := getStorageClass(getTestStorageClass()) 171 | if class == nil { 172 | t.Error("unable to retrieve storage class") 173 | } 174 | } 175 | 176 | func TestGetBestVolName(t *testing.T) { 177 | p := getTestProvisioner() 178 | volName := p.getBestVolName(getTestPVC(), getTestStorageClass()) 179 | if volName == "" { 180 | t.Error("volname should not be empty") 181 | } 182 | } 183 | 184 | func TestEmptyVolumeCreate(t *testing.T) { 185 | options := &dockervol.Options{StripK8sFromOptions: true} 186 | dvp, _ := dockervol.NewDockerVolumePlugin(options) 187 | optionsMap := make(map[string]interface{}) 188 | optionsMap["description"] = "empty volume" 189 | _, err := dvp.Create("", optionsMap) 190 | if err == nil { 191 | t.Error("expected error on empty volume name") 192 | } 193 | } 194 | 195 | func TestOverrides(t *testing.T) { 196 | 197 | p := getTestProvisioner() 198 | params := getStorageClassParams() 199 | params["allowOverrides"] = "description" 200 | overrides := p.getClassOverrides(params) 201 | 202 | if overrides[0] != "description" { 203 | t.Error("expected overrides to have only description") 204 | } 205 | 206 | fmt.Print(overrides) 207 | 208 | } 209 | -------------------------------------------------------------------------------- /common/k8s/provisioner/volume.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package provisioner 18 | 19 | import ( 20 | "fmt" 21 | "github.com/hpe-storage/dory/common/util" 22 | api_v1 "k8s.io/api/core/v1" 23 | storage_v1 "k8s.io/api/storage/v1" 24 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | "k8s.io/apimachinery/pkg/watch" 27 | "k8s.io/client-go/kubernetes/scheme" 28 | "k8s.io/client-go/tools/cache" 29 | "k8s.io/client-go/tools/reference" 30 | "strings" 31 | ) 32 | 33 | func (p *Provisioner) listAllVolumes(options meta_v1.ListOptions) (runtime.Object, error) { 34 | return p.kubeClient.Core().PersistentVolumes().List(options) 35 | } 36 | 37 | func (p *Provisioner) watchAllVolumes(options meta_v1.ListOptions) (watch.Interface, error) { 38 | return p.kubeClient.Core().PersistentVolumes().Watch(options) 39 | } 40 | 41 | //NewVolumeController provides a controller that watches for PersistentVolumes and takes action on them 42 | func (p *Provisioner) newVolumeController() cache.Controller { 43 | volListWatch := &cache.ListWatch{ 44 | ListFunc: p.listAllVolumes, 45 | WatchFunc: p.watchAllVolumes, 46 | } 47 | 48 | _, volInformer := cache.NewInformer( 49 | volListWatch, 50 | &api_v1.PersistentVolume{}, 51 | resyncPeriod, 52 | cache.ResourceEventHandlerFuncs{ 53 | AddFunc: p.addedVolume, 54 | UpdateFunc: p.updatedVolume, 55 | DeleteFunc: p.deletedVolume, 56 | }, 57 | ) 58 | return volInformer 59 | } 60 | 61 | func (p *Provisioner) addedVolume(t interface{}) { 62 | vol, err := getPersistentVolume(t) 63 | if err != nil { 64 | util.LogError.Printf("unable to process pv add - %v, %s", t, err.Error()) 65 | } 66 | go p.processVolEvent("added", vol, true) 67 | } 68 | 69 | func (p *Provisioner) updatedVolume(oldT interface{}, newT interface{}) { 70 | vol, err := getPersistentVolume(newT) 71 | if err != nil { 72 | util.LogError.Printf("unable to process pv update - %v, %s", newT, err.Error()) 73 | } 74 | 75 | go p.processVolEvent("updatedVol", vol, true) 76 | } 77 | 78 | func (p *Provisioner) deletedVolume(t interface{}) { 79 | vol, err := getPersistentVolume(t) 80 | if err != nil { 81 | util.LogError.Printf("unable to process pv delete - %v, %s", t, err.Error()) 82 | } 83 | go p.processVolEvent("deletedVol", vol, false) 84 | } 85 | 86 | // We map updated and deleted events here incase we were not running when the pv state changed to Released. If rmPV is true, we try to remove the pv object from the cluster. If its false, we don't. 87 | func (p *Provisioner) processVolEvent(event string, vol *api_v1.PersistentVolume, rmPV bool) { 88 | //notify the monitor 89 | go p.sendUpdate(vol) 90 | 91 | if vol.Status.Phase != api_v1.VolumeReleased || vol.Spec.PersistentVolumeReclaimPolicy != api_v1.PersistentVolumeReclaimDelete { 92 | util.LogInfo.Printf("%s event: pv:%s phase:%v (reclaim policy:%v) - skipping", event, vol.Name, vol.Status.Phase, vol.Spec.PersistentVolumeReclaimPolicy) 93 | return 94 | } 95 | if _, found := vol.Annotations[k8sProvisionedBy]; !found { 96 | util.LogInfo.Printf("%s event: pv:%s phase:%v (reclaim policy:%v) - missing annotation skipping", event, vol.Name, vol.Status.Phase, vol.Spec.PersistentVolumeReclaimPolicy) 97 | return 98 | } 99 | 100 | if !strings.HasPrefix(vol.Annotations[k8sProvisionedBy], p.namePrefix) { 101 | util.LogInfo.Printf("%s event: pv:%s phase:%v (reclaim policy:%v) provisioner:%v - unknown provisioner skipping", event, vol.Name, vol.Status.Phase, vol.Spec.PersistentVolumeReclaimPolicy, vol.Annotations[k8sProvisionedBy]) 102 | return 103 | } 104 | 105 | util.LogDebug.Printf("%s event: cleaning up pv:%s phase:%v", event, vol.Name, vol.Status.Phase) 106 | p.deleteVolume(vol, rmPV) 107 | } 108 | 109 | func getPersistentVolume(t interface{}) (*api_v1.PersistentVolume, error) { 110 | switch t := t.(type) { 111 | default: 112 | return nil, fmt.Errorf("unexpected type %T for %v", t, t) 113 | case *api_v1.PersistentVolume: 114 | return t, nil 115 | case api_v1.PersistentVolume: 116 | return &t, nil 117 | } 118 | } 119 | func (p *Provisioner) getVolumeNameFromClaimName(nameSpace, claimName string) (string, error) { 120 | // get the pv corresponding to this pvc and substitute with pv (docker volume name) 121 | util.LogDebug.Printf("handling %s with pvcName %s", cloneOfPVC, claimName) 122 | claim, err := p.getClaimFromPVCName(nameSpace, claimName) 123 | if err != nil { 124 | return "", err 125 | } 126 | if claim == nil || claim.Spec.VolumeName == "" { 127 | return "", fmt.Errorf("no volume found for claim %s", claimName) 128 | } 129 | return claim.Spec.VolumeName, nil 130 | } 131 | 132 | func (p *Provisioner) getDockerOptions(params map[string]string, class *storage_v1.StorageClass, claimSizeinGiB int, listOfOptions []string, nameSpace string) (map[string]interface{}, error) { 133 | dockOpts := make(map[string]interface{}, len(params)) 134 | foundSizeKey := false 135 | for key, value := range params { 136 | if key == cloneOfPVC { 137 | pvName, err := p.getVolumeNameFromClaimName(nameSpace, value) 138 | if err != nil { 139 | util.LogError.Printf("Error to retrieve pvc %s/%s : %s return existing options", nameSpace, value, err.Error()) 140 | p.eventRecorder.Event(class, api_v1.EventTypeWarning, "ProvisionStorage", 141 | fmt.Sprintf("Error to retrieve pvc %s/%s : %s", nameSpace, value, err.Error())) 142 | return nil, err 143 | } 144 | util.LogDebug.Printf("setting key : cloneOf value : %v", pvName) 145 | dockOpts["cloneOf"] = pvName 146 | continue 147 | } 148 | dockOpts[key] = value 149 | util.LogDebug.Printf("storageclass option key:%v value:%v", key, value) 150 | if claimSizeinGiB > 0 && contains(listOfOptions, key) { 151 | foundSizeKey = true 152 | for _, option := range listOfOptions { 153 | if key == option { 154 | util.LogInfo.Printf("storageclass option matched storage resource option:%s ,overriding the value to %d", key, claimSizeinGiB) 155 | dockOpts[key] = claimSizeinGiB 156 | break 157 | } 158 | } 159 | } 160 | } 161 | if claimSizeinGiB > 0 && !foundSizeKey { 162 | util.LogDebug.Print("storage class does not contain size key, overriding to claim size") 163 | dockOpts["size"] = claimSizeinGiB 164 | } 165 | return dockOpts, nil 166 | } 167 | 168 | func contains(s []string, e string) bool { 169 | for _, a := range s { 170 | if a == e { 171 | return true 172 | } 173 | } 174 | return false 175 | } 176 | 177 | func (p *Provisioner) newPersistentVolume(pvName string, params map[string]string, claim *api_v1.PersistentVolumeClaim, class *storage_v1.StorageClass) (*api_v1.PersistentVolume, error) { 178 | claimRef, err := reference.GetReference(scheme.Scheme, claim) 179 | if err != nil { 180 | util.LogError.Printf("unable to get reference for claim %v. %s", claim, err) 181 | return nil, err 182 | } 183 | 184 | claimName := getClaimClassName(claim) 185 | if class.Parameters == nil { 186 | class.Parameters = make(map[string]string) 187 | } 188 | class.Parameters["name"] = pvName 189 | 190 | if class.ReclaimPolicy == nil { 191 | class.ReclaimPolicy = new(api_v1.PersistentVolumeReclaimPolicy) 192 | *class.ReclaimPolicy = api_v1.PersistentVolumeReclaimDelete 193 | } 194 | 195 | pv := &api_v1.PersistentVolume{ 196 | ObjectMeta: meta_v1.ObjectMeta{ 197 | Name: pvName, 198 | Namespace: claim.Namespace, 199 | Labels: getClaimMatchLabels(claim), 200 | Annotations: map[string]string{ 201 | "volume.beta.kubernetes.io/storage-class": claimName, 202 | k8sProvisionedBy: class.Provisioner, 203 | p.dockerVolNameAnnotation: pvName, 204 | }, 205 | }, 206 | Spec: api_v1.PersistentVolumeSpec{ 207 | PersistentVolumeReclaimPolicy: *class.ReclaimPolicy, 208 | AccessModes: claim.Spec.AccessModes, 209 | ClaimRef: claimRef, 210 | StorageClassName: claimName, 211 | Capacity: api_v1.ResourceList{ 212 | api_v1.ResourceName(api_v1.ResourceStorage): claim.Spec.Resources.Requests[api_v1.ResourceName(api_v1.ResourceStorage)], 213 | }, 214 | PersistentVolumeSource: api_v1.PersistentVolumeSource{ 215 | FlexVolume: &api_v1.FlexVolumeSource{ 216 | Driver: class.Provisioner, 217 | Options: params, 218 | }, 219 | }, 220 | }, 221 | } 222 | return pv, nil 223 | } 224 | -------------------------------------------------------------------------------- /common/linux/bmount.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package linux 18 | 19 | import ( 20 | "github.com/hpe-storage/dory/common/util" 21 | "strings" 22 | ) 23 | 24 | const ( 25 | procMounts = "/proc/mounts" 26 | mountCommand = "mount" 27 | umountCommand = "umount" 28 | ) 29 | 30 | func unmount(mountPoint string) error { 31 | // try to unmount 32 | args := []string{mountPoint} 33 | _, _, err := util.ExecCommandOutput(umountCommand, args) 34 | if err != nil { 35 | return err 36 | } 37 | return nil 38 | } 39 | 40 | //BindMount mounts a path to a mountPoint. The rbind flag controls recursive binding 41 | func BindMount(path, mountPoint string, rbind bool) error { 42 | util.LogDebug.Printf("BindMount called with %s %s %v", path, mountPoint, rbind) 43 | flag := "--bind" 44 | if rbind { 45 | flag = "--rbind" 46 | } 47 | 48 | args := []string{flag, path, mountPoint} 49 | out, rc, err := util.ExecCommandOutput(mountCommand, args) 50 | if err != nil { 51 | util.LogError.Printf("BindMount failed with %d. It was called with %s %s %v. Output=%v.", rc, path, mountPoint, rbind, out) 52 | return err 53 | } 54 | 55 | return nil 56 | } 57 | 58 | //BindUnmount unmounts a bind mount. 59 | func BindUnmount(mountPoint string) error { 60 | util.LogDebug.Printf("BindUnmount called with %s", mountPoint) 61 | err := unmount(mountPoint) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | 69 | //GetDeviceFromMountPoint returns the device path from /proc/mounts 70 | // for the mountpoint provided. For example /dev/mapper/mpathd might be 71 | // returned for /mnt. 72 | func GetDeviceFromMountPoint(mountPoint string) (string, error) { 73 | util.LogDebug.Print("getDeviceFromMountPoint called with ", mountPoint) 74 | return getMountsEntry(mountPoint, false) 75 | } 76 | 77 | //GetMountPointFromDevice returns the FIRST mountpoint listed in 78 | // /proc/mounts matching the device. Note that /proc/mounts lists 79 | // device paths using the device mapper format. For example: /dev/mapper/mpathd 80 | func GetMountPointFromDevice(devPath string) (string, error) { 81 | util.LogDebug.Print("getMountPointFromDevice called with ", devPath) 82 | return getMountsEntry(devPath, true) 83 | } 84 | 85 | func getMountsEntry(path string, dev bool) (string, error) { 86 | util.LogDebug.Printf("getMountsEntry called with path:%v isDev:%v", path, dev) 87 | mountLines, err := util.FileGetStrings(procMounts) 88 | if err != nil { 89 | return "", err 90 | } 91 | 92 | var searchIndex, returnIndex int 93 | if dev { 94 | returnIndex = 1 95 | } else { 96 | searchIndex = 1 97 | } 98 | 99 | for _, line := range mountLines { 100 | entry := strings.Fields(line) 101 | util.LogDebug.Print("mounts entry :", entry) 102 | if len(entry) > 2 { 103 | if entry[searchIndex] == path { 104 | util.LogDebug.Printf("%s was found with %s", path, entry[returnIndex]) 105 | return entry[returnIndex], nil 106 | } 107 | } 108 | } 109 | return "", nil 110 | } 111 | -------------------------------------------------------------------------------- /common/linux/bmount_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package linux 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | var ( 24 | devPath = "/dev/sda1" 25 | mountPoint = "/boot" 26 | ) 27 | 28 | func TestGetDeviceFromMountPoint(t *testing.T) { 29 | dev, err := GetDeviceFromMountPoint(mountPoint) 30 | 31 | if err != nil { 32 | t.Error( 33 | "Unexpected error", err, 34 | ) 35 | } 36 | if dev != devPath { 37 | t.Error( 38 | "For", "dev", 39 | "expected", devPath, 40 | "got", dev, 41 | ) 42 | } 43 | } 44 | func TestGetMountPointFromDevice(t *testing.T) { 45 | m, err := GetMountPointFromDevice(devPath) 46 | 47 | if err != nil { 48 | t.Error( 49 | "Unexpected error", err, 50 | ) 51 | } 52 | if m != mountPoint { 53 | t.Error( 54 | "For", "mountpoint", 55 | "expected", mountPoint, 56 | "got", m, 57 | ) 58 | } 59 | } 60 | 61 | func TestGetSilly(t *testing.T) { 62 | m, err := GetMountPointFromDevice("") 63 | 64 | if err != nil { 65 | t.Error( 66 | "Unexpected error", err, 67 | ) 68 | } 69 | if m != "" { 70 | t.Error( 71 | "For", "mountpoint", 72 | "expected", "", 73 | "got", m, 74 | ) 75 | } 76 | dev, err := GetDeviceFromMountPoint("") 77 | 78 | if err != nil { 79 | t.Error( 80 | "Unexpected error", err, 81 | ) 82 | } 83 | if dev != "" { 84 | t.Error( 85 | "For", "dev", 86 | "expected", "", 87 | "got", dev, 88 | ) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /common/linux/mount.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package linux 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "github.com/hpe-storage/dory/common/model" 23 | "github.com/hpe-storage/dory/common/util" 24 | "strconv" 25 | ) 26 | 27 | const ( 28 | mountUUIDErr = 32 29 | ) 30 | 31 | // MountDeviceWithFileSystem : Mount device with filesystem at the mountPoint 32 | func MountDeviceWithFileSystem(devPath string, mountPoint string) (*model.Mount, error) { 33 | util.LogDebug.Printf("MountDeviceWithFileSystem called with %s %s", devPath, mountPoint) 34 | if devPath == "" || mountPoint == "" { 35 | return nil, errors.New("Neither arg can be nul devPath :" + devPath + " mountPoint :" + mountPoint) 36 | } 37 | 38 | // check if the mountpoint exists 39 | err := checkIfMountExists(devPath, mountPoint) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | // check if mountpoint already has a device 45 | mountedDevice, err := GetDeviceFromMountPoint(mountPoint) 46 | if mountedDevice != "" || err != nil { 47 | return nil, errors.New(devPath + " is already mounted at " + mountPoint) 48 | } 49 | 50 | // if not already mounted try to mount the device 51 | args := []string{devPath, mountPoint} 52 | _, rc, err := util.ExecCommandOutput(mountCommand, args) 53 | if err != nil { 54 | if rc == mountUUIDErr { 55 | util.LogDebug.Print("rc=" + strconv.Itoa(mountUUIDErr) + " trying again with no uuid option") 56 | _, _, err = util.ExecCommandOutput(mountCommand, []string{"-o", "nouuid", devPath, mountPoint}) 57 | } 58 | } 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | // verify the mount worked 64 | err = verifyMount(devPath, mountPoint) 65 | device := &model.Device{ 66 | AltFullPathName: mountedDevice, 67 | } 68 | mount := &model.Mount{ 69 | Mountpoint: mountPoint, 70 | Device: device} 71 | return mount, err 72 | } 73 | 74 | func checkIfMountExists(devPath, mountPoint string) error { 75 | is, _, err := util.FileExists(devPath) 76 | if err != nil || is == false { 77 | return fmt.Errorf("Device Path %s doesn't exist", devPath) 78 | } 79 | is, _, err = util.FileExists(mountPoint) 80 | if err != nil || is == false { 81 | return fmt.Errorf("Mountpoint %s doesn't exist", mountPoint) 82 | } 83 | return err 84 | } 85 | 86 | func verifyMount(devPath, mountPoint string) error { 87 | mountedDevice, err := GetDeviceFromMountPoint(mountPoint) 88 | if err != nil { 89 | return err 90 | } 91 | if mountedDevice != "" { 92 | util.LogDebug.Printf("%s is mounted at %s", devPath, mountPoint) 93 | } 94 | return err 95 | } 96 | -------------------------------------------------------------------------------- /common/linux/selinux.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package linux 18 | 19 | import ( 20 | "github.com/hpe-storage/dory/common/util" 21 | ) 22 | 23 | const ( 24 | selinuxenabled = "selinuxenabled" 25 | chcon = "chcon" 26 | ) 27 | 28 | //SelinuxEnabled runs selinuxenabled if found and returns the result. If its not found, false is returned. 29 | // From man - It exits with status 0 if SELinux is enabled and 1 if it is not enabled. 30 | func SelinuxEnabled() bool { 31 | _, rc, err := util.ExecCommandOutput(selinuxenabled, nil) 32 | util.LogDebug.Printf("selinuxenabled returned %d and err=%v", rc, err) 33 | if rc == 0 && err == nil { 34 | return true 35 | } 36 | util.LogDebug.Print("selinux is NOT enabled") 37 | return false 38 | } 39 | 40 | //Chcon - chcon -t svirt_sandbox_file_t 41 | func Chcon(context, path string) error { 42 | if SelinuxEnabled() { 43 | util.LogDebug.Printf("Chcon about to change context of %s to %s", path, context) 44 | args := []string{"-t", context, path} 45 | _, _, err := util.ExecCommandOutput(chcon, args) 46 | if err != nil { 47 | return err 48 | } 49 | } 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /common/model/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package model 18 | 19 | //Device describes a storage device 20 | type Device struct { 21 | Pathname string `json:"path_name,omitempty"` 22 | SerialNumber string `json:"serial_number,omitempty"` 23 | Major string `json:"major,omitempty"` 24 | Minor string `json:"minor,omitempty"` 25 | AltFullPathName string `json:"alt_full_path_name,omitempty"` 26 | MpathName string `json:"mpath_device_name,omitempty"` 27 | Size int64 `json:"size,omitempty"` // size in MiB 28 | Slaves []string `json:"slaves,omitempty"` 29 | Hcils []string `json:"-"` // export it if needed 30 | } 31 | 32 | //Mount describes a filesystem mount 33 | type Mount struct { 34 | ID uint64 `json:"id,omitempty"` 35 | Mountpoint string `json:"mount_point,omitempty"` 36 | Device *Device `json:"device,omitempty"` 37 | } 38 | -------------------------------------------------------------------------------- /common/util/cmd.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "os/exec" 23 | "regexp" 24 | "strings" 25 | "syscall" 26 | ) 27 | 28 | // ExecCommandOutput returns stdout and stderr in a single string, the return code, and error. 29 | // If the return code is not zero, error will not be nil. 30 | // Stdout and Stderr are dumped to the log at the debug level. 31 | // Return code of 999 indicates an error starting the command. 32 | func ExecCommandOutput(cmd string, args []string) (string, int, error) { 33 | LogDebug.Print("ExecCommandOutput called with ", cmd, args) 34 | c := exec.Command(cmd, args...) 35 | var b bytes.Buffer 36 | c.Stdout = &b 37 | c.Stderr = &b 38 | 39 | if err := c.Start(); err != nil { 40 | return "", 999, err 41 | } 42 | 43 | //TODO we could set a timeout here if needed 44 | 45 | err := c.Wait() 46 | out := string(b.Bytes()) 47 | 48 | for _, line := range strings.Split(out, "\n") { 49 | LogDebug.Print("out :", line) 50 | } 51 | 52 | if err != nil { 53 | //check the rc of the exec 54 | if badnews, ok := err.(*exec.ExitError); ok { 55 | if status, ok := badnews.Sys().(syscall.WaitStatus); ok { 56 | return out, status.ExitStatus(), fmt.Errorf("rc=%d", status.ExitStatus()) 57 | } 58 | } else { 59 | return out, 888, fmt.Errorf("unknown error") 60 | } 61 | } 62 | 63 | return out, 0, nil 64 | } 65 | 66 | // FindStringSubmatchMap : find and build the map of named groups 67 | func FindStringSubmatchMap(s string, r *regexp.Regexp) map[string]string { 68 | captures := make(map[string]string) 69 | match := r.FindStringSubmatch(s) 70 | if match == nil { 71 | return captures 72 | } 73 | for i, name := range r.SubexpNames() { 74 | if i != 0 { 75 | captures[name] = match[i] 76 | } 77 | } 78 | return captures 79 | } 80 | -------------------------------------------------------------------------------- /common/util/cmd_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "strings" 21 | "testing" 22 | ) 23 | 24 | func TestEchoExecCommandOutput(t *testing.T) { 25 | out, rc, err := ExecCommandOutput("echo", []string{"Hello"}) 26 | if err != nil { 27 | t.Error( 28 | "Unexpected error", err, 29 | ) 30 | } 31 | if rc > 0 { 32 | t.Error( 33 | "Unexpected rc", rc, 34 | ) 35 | } 36 | if strings.HasSuffix(out, "Hello") { 37 | t.Error( 38 | "For", "return code of false", 39 | "expected", "1", 40 | "got", rc, 41 | ) 42 | } 43 | 44 | } 45 | 46 | func TestFalseExecCommandOutput(t *testing.T) { 47 | out, rc, err := ExecCommandOutput("false", []string{"foo"}) 48 | if err == nil { 49 | t.Error( 50 | "Expected error to not be nil", err, 51 | ) 52 | } 53 | if rc != 1 { 54 | t.Error( 55 | "For", "return code of false", 56 | "expected", "1", 57 | "got", rc, 58 | ) 59 | } 60 | if out != "" { 61 | t.Error( 62 | "For", "out of false", 63 | "expected", "", 64 | "got", out, 65 | ) 66 | } 67 | } 68 | 69 | func TestFailExecCommandOutput(t *testing.T) { 70 | out, _, err := ExecCommandOutput("cp", []string{"x"}) 71 | if err == nil { 72 | t.Error( 73 | "Expected error to be nil", err, 74 | ) 75 | } 76 | if out == "" { 77 | t.Error( 78 | "For", "out of 'cp x'", 79 | "expected", "some text", 80 | "got", out, 81 | ) 82 | } 83 | 84 | _, rc, err := ExecCommandOutput("nosuchcommand", []string{"x"}) 85 | if err == nil { 86 | t.Error( 87 | "Expected error to not be nil", err, 88 | ) 89 | } 90 | if rc != 999 { 91 | t.Error( 92 | "For", "rc", 93 | "expected", 999, 94 | "got", rc, 95 | ) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /common/util/file.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "bufio" 21 | "encoding/gob" 22 | "errors" 23 | "os" 24 | "regexp" 25 | "runtime" 26 | "strings" 27 | ) 28 | 29 | // FileReadFirstLine read first line from a file 30 | //TODO: make it OS independent 31 | func FileReadFirstLine(path string) (line string, er error) { 32 | LogDebug.Print("In FileReadFirstLine") 33 | file, err := os.Open(path) 34 | a := "" 35 | if err != nil { 36 | LogError.Print(err) 37 | return "", err 38 | } 39 | defer file.Close() 40 | 41 | scanner := bufio.NewScanner(file) 42 | for scanner.Scan() { 43 | a = scanner.Text() 44 | break 45 | } 46 | if err = scanner.Err(); err != nil { 47 | LogError.Print(err) 48 | return "", err 49 | } 50 | LogDebug.Print(a) 51 | return a, err 52 | } 53 | 54 | //FileExists does a stat on the path and returns true if it exists 55 | //In addition, dir returns true if the path is a directory 56 | func FileExists(path string) (exists bool, dir bool, err error) { 57 | info, err := os.Stat(path) 58 | if err != nil { 59 | if os.IsNotExist(err) { 60 | return false, false, nil 61 | } 62 | return false, false, err 63 | } 64 | return true, info.IsDir(), nil 65 | } 66 | 67 | // FileGetStringsWithPattern : get the filecontents as array of string matching pattern pattern 68 | func FileGetStringsWithPattern(path string, pattern string) (filelines []string, err error) { 69 | LogDebug.Print("FileGetStringsWithPattern caleld with path: ", path, " Pattern: ", pattern) 70 | file, err := os.Open(path) 71 | if err != nil { 72 | LogError.Print(err) 73 | return nil, err 74 | } 75 | defer file.Close() 76 | 77 | scanner := bufio.NewScanner(file) 78 | var lines []string 79 | 80 | for scanner.Scan() { 81 | lines = append(lines, scanner.Text()) 82 | } 83 | if err = scanner.Err(); err != nil { 84 | LogError.Print(err) 85 | return nil, err 86 | } 87 | 88 | var matchingLines []string 89 | if pattern == "" { 90 | return lines, nil 91 | } 92 | r := regexp.MustCompile(pattern) 93 | for _, l := range lines { 94 | if r.MatchString(l) { 95 | matchString := r.FindAllStringSubmatch(l, -1) 96 | LogDebug.Print("matchingline :", matchString[0][1]) 97 | matchingLines = append(matchingLines, matchString[0][1]) 98 | } 99 | } 100 | 101 | return matchingLines, err 102 | } 103 | 104 | // FileGetStrings : get the file contents as array of string 105 | func FileGetStrings(path string) (line []string, err error) { 106 | 107 | return FileGetStringsWithPattern(path, "") 108 | } 109 | 110 | //FileWriteString : write line to the path 111 | func FileWriteString(path, line string) (err error) { 112 | LogDebug.Printf("in FileWriteString called with path: %s and string: %s ", path, line) 113 | var file *os.File 114 | is, _, err := FileExists(path) 115 | if !is { 116 | LogDebug.Print("File doesn't exist, Creating : " + path) 117 | file, err = os.Create(path) 118 | defer file.Close() 119 | if err != nil { 120 | LogDebug.Print("err", err) 121 | return err 122 | } 123 | } 124 | err = os.Chmod(path, 644) 125 | if err != nil { 126 | return err 127 | } 128 | file, err = os.OpenFile(path, os.O_RDWR, 644) 129 | if err != nil { 130 | LogError.Print(err) 131 | return err 132 | } 133 | defer file.Close() 134 | 135 | writer := bufio.NewWriter(file) 136 | n, err := writer.WriteString(line) 137 | writer.Flush() 138 | if err != nil { 139 | LogDebug.Print("Unable to write to file " + path + " Err:" + err.Error()) 140 | err = errors.New("Unable to write to file " + path + " Err:" + err.Error()) 141 | return err 142 | } 143 | if n <= 0 { 144 | LogDebug.Printf("File write didnt go through as bytes written is %d: ", n) 145 | } 146 | LogDebug.Printf("%d bytes written", n) 147 | 148 | return err 149 | } 150 | 151 | // FileWriteStrings writes all lines to file specified by path. Newline is appended to each line 152 | func FileWriteStrings(path string, lines []string) (err error) { 153 | LogDebug.Printf("in FileWriteString called with path: %s", path) 154 | var file *os.File 155 | is, _, err := FileExists(path) 156 | if !is { 157 | LogDebug.Print("File doesn't exist, Creating : " + path) 158 | file, err = os.Create(path) 159 | defer file.Close() 160 | if err != nil { 161 | LogDebug.Print("err", err) 162 | return err 163 | } 164 | } 165 | err = os.Chmod(path, 644) 166 | if err != nil { 167 | return err 168 | } 169 | file, err = os.OpenFile(path, os.O_RDWR, 644) 170 | if err != nil { 171 | LogError.Print(err) 172 | return err 173 | } 174 | defer file.Close() 175 | 176 | w := bufio.NewWriter(file) 177 | defer func() { 178 | err = w.Flush() 179 | }() 180 | 181 | for _, line := range lines { 182 | // trim if newline is already appended to string, as we add later 183 | w.WriteString(strings.Trim(line, "\n")) 184 | // always add a new line so that caller doesn't need to append everytime 185 | w.WriteString("\n") 186 | } 187 | return err 188 | } 189 | 190 | //FileDelete : delete the file 191 | func FileDelete(path string) error { 192 | LogDebug.Print("File delete called") 193 | is, _, _ := FileExists(path) 194 | if !is { 195 | return errors.New("File doesnt exist " + path) 196 | } 197 | err := os.RemoveAll(path) 198 | if err != nil { 199 | return errors.New("Unable to delete file " + path + " " + err.Error()) 200 | } 201 | return nil 202 | } 203 | 204 | // FileSaveGob : save the Gob file 205 | func FileSaveGob(path string, object interface{}) error { 206 | LogDebug.Print("FileSaveGob called with ", path) 207 | file, err := os.Create(path) 208 | if err == nil { 209 | encoder := gob.NewEncoder(file) 210 | encoder.Encode(object) 211 | } 212 | file.Close() 213 | return err 214 | } 215 | 216 | // FileloadGob : Load and Decode Gob file 217 | func FileloadGob(path string, object interface{}) error { 218 | LogDebug.Print("FileloadGob called with", path) 219 | file, err := os.Open(path) 220 | if err == nil { 221 | decoder := gob.NewDecoder(file) 222 | err = decoder.Decode(object) 223 | } 224 | file.Close() 225 | return err 226 | } 227 | 228 | // FileCheck : checks for error 229 | func FileCheck(e error) { 230 | LogDebug.Print("FileCheck called") 231 | if e != nil { 232 | LogError.Print("err :", e.Error()) 233 | _, file, line, _ := runtime.Caller(1) 234 | LogError.Print("Line", line, "File", file, "Error:", e) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /common/util/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "fmt" 21 | "io/ioutil" 22 | "log" 23 | "net/http" 24 | "os" 25 | "path/filepath" 26 | "time" 27 | 28 | "gopkg.in/natefinch/lumberjack.v2" 29 | ) 30 | 31 | var ( 32 | //LogDebug logger 33 | LogDebug *log.Logger 34 | //LogInfo logger 35 | LogInfo *log.Logger 36 | //LogError logger 37 | LogError *log.Logger 38 | logOut *lumberjack.Logger 39 | // DebugLogLevel debug log level 40 | DebugLogLevel = "debug" 41 | // InfoLogLevel info log level 42 | InfoLogLevel = "info" 43 | ) 44 | 45 | func init() { 46 | LogDebug = log.New(ioutil.Discard, "Debug: ", log.Ldate|log.Ltime|log.Lshortfile) 47 | LogInfo = log.New(ioutil.Discard, "Info : ", log.Ldate|log.Ltime|log.Lshortfile) 48 | LogError = log.New(ioutil.Discard, "Error: ", log.Ldate|log.Ltime|log.Lshortfile) 49 | } 50 | 51 | //OpenLogFile creates a file based logger 52 | func OpenLogFile(filePath string, maxSizeMB int, maxFiles int, maxAgeDays int, debug bool) error { 53 | if logOut != nil { 54 | return fmt.Errorf("a log file is already open. %s", logOut.Filename) 55 | } 56 | 57 | pathCheck(filePath) 58 | logOut = &lumberjack.Logger{ 59 | Filename: filePath, 60 | MaxSize: maxSizeMB, 61 | MaxBackups: maxFiles, 62 | MaxAge: maxAgeDays, 63 | } 64 | 65 | if debug { 66 | LogDebug = log.New(logOut, "Debug: ", log.Ldate|log.Ltime|log.Lshortfile) 67 | } else { 68 | LogDebug = log.New(ioutil.Discard, "Debug: ", log.Ldate|log.Ltime|log.Lshortfile) 69 | } 70 | LogInfo = log.New(logOut, "Info : ", log.Ldate|log.Ltime|log.Lshortfile) 71 | LogError = log.New(logOut, "Error: ", log.Ldate|log.Ltime|log.Lshortfile) 72 | return nil 73 | } 74 | 75 | //OpenLog causes logging to happen to stdout 76 | func OpenLog(debug bool) error { 77 | stdLog := log.New(os.Stdout, "", log.Ltime|log.Lshortfile) 78 | 79 | if debug { 80 | LogDebug = stdLog 81 | } else { 82 | LogDebug = log.New(ioutil.Discard, "", log.Ltime|log.Lshortfile) 83 | } 84 | LogInfo = stdLog 85 | LogError = stdLog 86 | return nil 87 | } 88 | 89 | // create dirs if needed 90 | func pathCheck(filePath string) { 91 | dir := filepath.Dir(filePath) 92 | if len(dir) > 1 { 93 | err := os.MkdirAll(dir, 0755) 94 | if err != nil { 95 | fmt.Fprintln(os.Stderr, fmt.Sprintf("Unable to create %s (%s)", dir, err)) 96 | } 97 | } 98 | } 99 | 100 | //CloseLogFile closes the current logFile 101 | func CloseLogFile() { 102 | if logOut == nil { 103 | return 104 | } 105 | logOut.Close() 106 | logOut = nil 107 | } 108 | 109 | // HTTPLogger : wrapper for http logging 110 | func HTTPLogger(inner http.Handler, name string) http.Handler { 111 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 112 | start := time.Now() 113 | inner.ServeHTTP(w, r) 114 | 115 | LogInfo.Printf( 116 | "%s\t%s\t%s\t%s", 117 | r.Method, 118 | r.RequestURI, 119 | name, 120 | time.Since(start), 121 | ) 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /common/util/logger_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | (c) Copyright 2017 Hewlett Packard Enterprise Development LP 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "os" 21 | "path" 22 | "testing" 23 | ) 24 | 25 | type testPath struct { 26 | path string 27 | dirResult string 28 | } 29 | 30 | func getPaths() []testPath { 31 | tmpDir := os.TempDir() 32 | return []testPath{ 33 | { 34 | path.Join(".", "testfile.test"), 35 | path.Join("."), 36 | }, 37 | { 38 | path.Join(tmpDir, "testfile.test"), 39 | path.Join(tmpDir, "."), 40 | }, 41 | { 42 | path.Join(tmpDir, "testdir.test", "testfile.test"), 43 | path.Join(tmpDir, "testdir.test"), 44 | }, 45 | { 46 | path.Join(tmpDir, "testdir1.test", "testdir2.test", "testfile.test"), 47 | path.Join(tmpDir, "testdir1.test", "testdir2.test"), 48 | }, 49 | } 50 | } 51 | 52 | func cleanUp() { 53 | tmpDir := os.TempDir() 54 | for _, pathInfo := range getPaths() { 55 | os.Remove(pathInfo.path) 56 | } 57 | os.Remove(path.Join(tmpDir, "testdir.test")) 58 | os.Remove(path.Join(tmpDir, "testdir1.test", "testdir2.test")) 59 | } 60 | 61 | func TestNotOpen(t *testing.T) { 62 | // log something at each level 63 | logAtAllLevels() 64 | } 65 | 66 | func TestOpenLogClose(t *testing.T) { 67 | defer cleanUp() 68 | 69 | for _, pathInfo := range getPaths() { 70 | //Open a logfile 71 | openLog(t, pathInfo.path, true) 72 | 73 | // make sure we created any needed directories 74 | is, dir, err := FileExists(pathInfo.dirResult) 75 | if err != nil { 76 | t.Error( 77 | "For", pathInfo.path, 78 | "expected", "no error", 79 | "got error:", err, 80 | ) 81 | } 82 | if !is || !dir { 83 | t.Error( 84 | "For", pathInfo.path, 85 | "expected", pathInfo.dirResult, 86 | "got exists", is, 87 | "got dir", dir, 88 | ) 89 | } 90 | 91 | // log something at each level 92 | logAtAllLevels() 93 | 94 | // make sure we can't call open again 95 | err = OpenLogFile(pathInfo.path, 1, 2, 3, true) 96 | if err == nil { 97 | t.Error( 98 | "For", pathInfo.path, 99 | "expected", "error (a log file is already open.)", 100 | "got", "no error", 101 | ) 102 | } 103 | 104 | // close the log file 105 | CloseLogFile() 106 | 107 | // open with debug off 108 | openLog(t, pathInfo.path, false) 109 | // log something at each level 110 | logAtAllLevels() 111 | // close the log file 112 | CloseLogFile() 113 | } 114 | } 115 | 116 | func openLog(t *testing.T, path string, debug bool) { 117 | if err := OpenLogFile(path, 1, 2, 3, debug); err != nil { 118 | t.Error( 119 | "For", path, 120 | "expected", "no error", 121 | "got error:", err, 122 | ) 123 | } 124 | } 125 | 126 | func logAtAllLevels() { 127 | LogDebug.Printf("%s: testing", "Paul Bunyan") 128 | LogInfo.Printf("%s: testing", "Blue") 129 | LogError.Printf("%s: testing", "Axe") 130 | } 131 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | Currently available documentation: 3 | * [Dory](dory/README.md) 4 | * [Doryd](doryd/README.md) 5 | * [Plugins](plugins/README.md) 6 | 7 | Scattered blog posts on Dory: 8 | * [Tech Preview: Bringing Nimble Storage to Kubernetes and OpenShift](https://community.hpe.com/t5/HPE-Nimble-Storage-Tech-Blog/Tech-Preview-Bringing-Nimble-Storage-to-Kubernetes-and-OpenShift/ba-p/6986748) 9 | * [Dory: A FlexVolume Driver that speaks Whale!](https://community.hpe.com/t5/HPE-Nimble-Storage-Tech-Blog/Dory-A-FlexVolume-Driver-that-speaks-Whale/ba-p/6986638) 10 | -------------------------------------------------------------------------------- /docs/dory/README.md: -------------------------------------------------------------------------------- 1 | # Dory 2 | 3 | Dory is a Kubernetes [FlexVolume Plugin driver](https://kubernetes.io/docs/concepts/storage/volumes/#out-of-tree-volume-plugins) that leverages the [Docker Volume API](https://docs.docker.com/engine/extend/plugins_volume/) and allows Kubernetes to leverage Docker Volume plugins, both [legacy](https://docs.docker.com/engine/extend/legacy_plugins/#volume-plugins) and [managed](https://store.docker.com/search?category=volume&q=&type=plugin). There is also an out-of-tree dynamic provisioner for Dory, called [Doryd](../doryd/README.md). 4 | 5 | ## Design 6 | 7 | ### Overview 8 | 9 | In order to provide persistent storage for a Pod, Kubernetes makes a call to Dory via the Flexvolume interface. Dory is an executable placed in a specific directory (see [Installation](#installation)). Dory then communicates with the configured Docker Volume Plugin via a unix socket. Dory then translates the response from the Docker Volume Plugin to the Flexvolume interface. It is important to note that Dory never communicates with the container engine, it communicates directly with the Docker Volume Plugin. This theoretically allows Dory to work with any container engine supported by Kubernetes. 10 | 11 | ### Create 12 | 13 | Dory is configured by default to create a volume if one with that name doesn't exist. It does this by first using the Docker Volume Plugin 'get' function. If this doesn't return a volume (and Dory is configured to create volumes), Dory will call the Docker Volume Plugin 'create' function using the options specified in the Persistent Volume definition. This is handled during the Attach workflow in Kubernetes 1.5 and in the Mount workflow in 1.6 and higher. 14 | 15 | ### Mount 16 | 17 | The diagram below depicts the process communication on the right and the resulting objects on the left. When the Mount workflow is executed Dory first uses the Docker Volume Plugin 'get' function to see if the volume is available (see [Create](#create)). It then executes the Docker Volume Plugin 'mount' function to mount the filesystem. The Pod uuid is used as the Docker Volume Plugin 'mount id'. This results in the green cylinder labeled '/vol/HrPostgres' in the diagram. Dory then bind mounts the path returned by the Docker Volume Plugin to the location that Kubernetes has requested. This results in the dark blue cylinder in the diagram. If SELinux is configured on the kubelet Dory will set the proper context for this mount. 18 | 19 | ![Mount](../../assets/mount.png) 20 | 21 | ### Unmount 22 | 23 | The unmount workflow unmounts the bind mount and then uses the Docker Volume Plugin 'unmount' function to unmount and detach the filesystem from the kubelet. 24 | 25 | ## Building release 1.0 26 | 27 | Dory is written in Go and requires golang on your machine. The current stable branch is release-1.0. The following example installs the necessary tools and builds Dory on a RHEL 7.4 system: 28 | ``` 29 | sudo subscription-manager repos --enable=rhel-7-server-optional-rpms 30 | sudo yum install -y golang make 31 | git clone https://github.com/hpe-storage/dory.git 32 | git checkout release-1.0 33 | make gettools 34 | make dory 35 | ``` 36 | You should end up with a `dory` executable in the `./bin` directory and be ready for [installation](#installation). 37 | 38 | **Hint:** Go is available through the [EPEL](https://fedoraproject.org/wiki/EPEL) repository for .rpm based distributions and a `golang` package is part of the official Ubuntu repositories. 39 | 40 | ## Building latest 41 | 42 | Dory is written in Go and requires golang on your machine. The following example installs the necessary tools and builds Dory on a RHEL 7.4 system: 43 | ``` 44 | sudo subscription-manager repos --enable=rhel-7-server-optional-rpms 45 | sudo yum install -y golang make 46 | git clone https://github.com/hpe-storage/dory.git $GOPATH/src/github.com/hpe-storage/dory 47 | cd $GOPATH/src/github.com/hpe-storage/dory 48 | make vendor 49 | make dory 50 | ``` 51 | You should end up with a `dory` executable in the `src/github.com/hpe-storage/dory` directory and be ready for [installation](#installation). 52 | 53 | **Hint:** Go is available through the [EPEL](https://fedoraproject.org/wiki/EPEL) repository for .rpm based distributions and a `golang` package is part of the official Ubuntu repositories. 54 | 55 | ## Usage 56 | 57 | ### Installation 58 | 59 | Create a directory on each kubelet with using the following convention: `/usr/libexec/kubernetes/kubelet-plugins/volume/exec/dory~plugin` where `plugin` is replaced with the name of the Docker Volume Plugin. Then copy the dory binary to this folder naming the file to the name of the Docker Volume Plugin. For example, in order to use [HPE's Nimble Storage Docker Volume Plugin](https://www.nimblestorage.com/docker/), the following directory should be created: `/usr/libexec/kubernetes/kubelet-plugins/volume/exec/dory~nimble`. The Dory binary should be copied to `/usr/libexec/kubernetes/kubelet-plugins/volume/exec/dory~nimble/nimble`. 60 | 61 | ### Configuration 62 | 63 | Dory looks for a configuration file with the same name as the executable with a `.json` extension. Following the example above, the configuration file would be `/usr/libexec/kubernetes/kubelet-plugins/volume/exec/dory~nimble/nimble.json`. 64 | 65 | #### Docker Volume Plugin Socket Path 66 | 67 | The critical attribute in this file is called `"dockerVolumePluginSocketPath"`. 68 | 69 | In the case of a **version 1** [Docker Volume Plugin](https://docs.docker.com/engine/extend/legacy_plugins/), this is used to tell Dory where the socket file is located for the Docker Volume Plugin. Again, following the example above, the file would contain the following; 70 | ``` 71 | { 72 | "dockerVolumePluginSocketPath": "/run/docker/plugins/nimble.sock" 73 | } 74 | ``` 75 | 76 | The attribute can also be used to describe a **version 2** [Docker Volume Plugin](https://store.docker.com/search?type=plugin). In this case, the name of the plugin used in place of the path to the socket file. The following example shows a plugin that has been aliased to 'nimble'. Note that version 2 support was added after release-1. 77 | ``` 78 | { 79 | "dockerVolumePluginSocketPath": "nimble:latest" 80 | } 81 | ``` 82 | 83 | Prior to Kubernetes 1.8 FlexVolume drivers are not dynamically discovered and require a kubelet restart. 84 | 85 | #### Logging 86 | 87 | There are two attributes which control how Dory logs. The `"logFilePath"` attribute provides the full path to the log file. The `"logDebug"` indicates whether to log at a debug granularity. The following are the default values; 88 | ``` 89 | { 90 | ... 91 | "logFilePath": "/var/log/dory.log", 92 | "logDebug": false 93 | } 94 | ``` 95 | 96 | #### Behavior 97 | 98 | There are two attributes which control Dory's behavior. The `"createVolumes"` attribute indicates whether Dory should create a volume when it can't find one. The `"stripK8sFromOptions"` attribute indicates whether the options in the Kubernetes.io namespace should be passed on to the Docker Volume Driver. The following are the default values; 99 | ``` 100 | { 101 | ... 102 | "stripK8sFromOptions": true, 103 | "createVolumes": true 104 | } 105 | ``` 106 | 107 | #### Example 108 | 109 | The following is an example of the default values; 110 | ``` 111 | { 112 | "dockerVolumePluginSocketPath": "/run/docker/plugins/nimble.sock", 113 | "logFilePath": "/var/log/dory.log", 114 | "logDebug": false, 115 | "stripK8sFromOptions": true, 116 | "createVolumes": true 117 | } 118 | ``` 119 | 120 | ### What's in a name? 121 | 122 | There are several names that you should be aware of when using Dory. The first is the Docker Volume name. This is used by Dory to identify the Docker Volume that should be exposed to Kubernetes. The second name to be aware of is that of the Persistent Volume. This name is used by Kubernetes to identify the Persistent Volume object (for example, in the output of `kubectl get pv`). The final name to be aware of is that of the Persistent Volume Claim. This name is used to tie the claim to a Pod or Pod template. 123 | 124 | Each of these names can be different. Some administrators follow a naming pattern in order to easily identify these relationships. For example, when using a Docker Volume named "sqldata" they might create a Persistent Volume named "sqldata-pv" and a Persistent Volume Claim named "sqldata-pvc". The Pod definition would then reference "sqldata-pvc". 125 | 126 | Giving the Docker Volume, Persistent Volume and Persistent Volume Claim all different names is not required though. Because these are each different objects, they can all share the same name. This makes it even easier to identify the relationship between these objects. 127 | 128 | **Note:** In Kubernetes 1.5 and 1.6 Flexvolume didn't provide the Persistent Volume name to its driver, so the Docker Volume name must be provided an an option in the Persistent Volume definition. As of 1.7 the Persistent Volume name is passed to the driver. 129 | 130 | ### Sometimes size does matter 131 | 132 | Flexvolume currently doesn't communicate the size of the volume to its driver. If the Docker Volume Plugin you're using requires a size, you'll need to specify this in the options section of the Persistent Volumes. 133 | 134 | #### Example 135 | 136 | This example uses the Docker Volume Driver from [HPE Nimble Storage](https://www.nimblestorage.com/docker/) to create a Persistent Volume to back a MySQL database instance. First, the administrator creates a Persistent Volume named "sqldata-pv". In order to be able to reference this Persistent Volume explicitly in the claim, the 'volumeName.dory' label is added. The options section may contain any create options the Docker Volume Driver supports. The administrator then creates a Persistent Volume Claim which uses a matchLabels selector to find the Persistent Volume. Finally, the administrator creates a Replication Controller that references the claim by name. When the Replication Controller is created, the Persistent Volume is matched with the Persistent Volume Claim and the Docker Volume is then created and then mounted. 137 | 138 | ![Example](../../assets/example.png) 139 | 140 | ## Future 141 | 142 | Docker Volume plugins are beginning to surface for Windows Containers. Kubernetes is getting more mature for Windows. We hope to extend Dory to Windows in the future. 143 | 144 | ## Dynamic Provisioning 145 | 146 | Dory has been extended with a dynamic StorageClass provisioner named [Doryd](../doryd/README.md). Doryd still depends on Dory for attach/detach and mount/unmount. 147 | 148 | ## Licensing 149 | 150 | Dory is licensed under the Apache License, Version 2.0. Please see [LICENSE](../../LICENSE) for the full license text. 151 | -------------------------------------------------------------------------------- /docs/doryd/README.md: -------------------------------------------------------------------------------- 1 | # Doryd 2 | Doryd is an out-of-tree dynamic provisioner for Docker Volume plugins that uses the StorageClass interface available in Kubernetes. Doryd needs to have access to the cluster to listen for Persistent Volume Claims against the Storage Classes that Dory governs. Doryd also depends on [Dory](../dory/README.md), the FlexVolume driver for Docker Volume plugins. 3 | 4 | # Building (optional) 5 | There are two ways provided to build Doryd. A host machine build and a fully containerized build. The containerized build require Docker 17.05 or newer on both client and daemon. 6 | 7 | ## Host build 8 | Doryd is written in Go and requires golang on your machine. The following example installs the necessary tools and builds Doryd on a RHEL 7.4 system: 9 | ``` 10 | sudo subscription-manager repos --enable=rhel-7-server-optional-rpms 11 | sudo yum install -y golang make 12 | git clone https://github.com/hpe-storage/dory.git $GOPATH/src/github.com/hpe-storage/dory 13 | cd $GOPATH/src/github.com/hpe-storage/dory 14 | make vendor 15 | make doryd 16 | ``` 17 | 18 | You should end up with a `doryd` executable in the `$GOPATH/src/github.com/hpe-storage/dory` directory. 19 | 20 | Optionally, you may build a doryd container: 21 | ``` 22 | make doryd_docker 23 | ``` 24 | 25 | **Hint:** Go is available through the [EPEL](https://fedoraproject.org/wiki/EPEL) repository for .rpm based distributions and a `golang` package is part of the official Ubuntu repositories. 26 | 27 | ## Containerized build 28 | Building Doryd in a container uses a multi-stage build and only require Docker 17.05 or newer. 29 | ``` 30 | docker build -t doryd:latest https://raw.githubusercontent.com/hpe-storage/dory/master/build/docker/doryd/Dockerfile.staged 31 | ``` 32 | 33 | # Running 34 | Doryd is available on Docker Hub and an [example DaemonSet specification](../../examples/ds-doryd.yaml) is available. 35 | 36 | ## Prerequisities 37 | The `doryd` binary needs access to the cluster via a kubeconfig file. The location may vary between distributions. The stock DaemonSet spec will assume the container default of `/etc/kubernetes/admin.conf`. This file needs to exist on all nodes prior to deploying the DaemonSet. 38 | 39 | The default provisioner name is prefixed with `dev.hpe.com` and will listen for Persistent Volume Claims that asks for Storage Classes with `provisioner: dev.hpe.com/DockerVolumeDriverName` and will map against a [Dory FlexVolume driver](../dory/README.md) with the same `provisioner`. Hence it's important that the FlexVolume driver name matches up with what you name your provisioner. 40 | 41 | A custom `doryd` command line could look like this: 42 | ``` 43 | doryd /root/.kube/cluster.conf nimblestorage.com 44 | ``` 45 | 46 | There should then be a Dory FlexVolume driver named `nimblestorage.com/yourdrivername` and Storage Classes should use `provisioner: nimblestorage.com/yourdrivername`. 47 | 48 | ## kubectl 49 | Deploying the default DaemonSet out-of-the-box can be accomplished with: 50 | ``` 51 | kubectl apply -f https://raw.githubusercontent.com/hpe-storage/dory/master/examples/ds-doryd.yaml 52 | ``` 53 | 54 | Add arguments to the container image if using a custom path to kubeconfig or need a different prefix. 55 | 56 | # Using 57 | The [Kubernetes documentation](https://kubernetes.io/docs/concepts/storage/volumes/) is good source for learning about Storage Classes, Proivsioners, Persistent Volume Claims and Persistent Volumes. The following basic examples assumes familiarity with those basic concepts. A full tutorial for Dory and Doryd is available on [developer.hpe.com](https://developer.hpe.com/platform/nimble-storage/home) 58 | 59 | Since Dory and Doryd solely relies on underlying Docker Volume plugin capabilities to provision storage, please consult the documentation for the corresponding plugin documentation. 60 | 61 | The following example will use the `dev.hpe.com/nimble` FlexVolume driver to provision volumes capped at 5000 IOPS using a database optimized Performance Profile with a custom Protection Template that replicate data offsite. 62 | 63 | ``` 64 | kubectl create -f- < 5 | 6 | Docker Volume Plugin 7 | --driver 8 | Version 9 | Status 10 | 11 | 12 | HPE Nimble Storage Docker Volume Plugin 13 | nimble 14 | 2.1.1 15 | Works 16 | 17 | 18 | Notes: Tested with K8s 1.5, 1.6, 1.7 and 1.8. OpenShift 3.5 and 3.6. Only tested with the "fat" version included in the Nimble Linux Toolkit available on InfoSight. The Docker Store version should work as well. 19 | 20 | 21 | HPE 3PAR Volume Plug-in for Docker 22 | hpe 23 | 1.0.0 24 | Works 25 | 26 | 27 | Notes: Tested with OpenShift 3.5. 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/dep-doryd-1.8.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: ClusterRole 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: doryd 6 | rules: 7 | - apiGroups: ["storage.k8s.io"] 8 | resources: ["storageclasses"] 9 | verbs: ["get", "list", "watch"] 10 | - apiGroups: [""] 11 | resources: ["persistentvolumeclaims"] 12 | verbs: ["get", "list", "watch"] 13 | - apiGroups: [""] 14 | resources: ["persistentvolumes"] 15 | verbs: ["get", "list", "watch", "create", "delete"] 16 | - apiGroups: [""] 17 | resources: ["events"] 18 | verbs: ["create"] 19 | --- 20 | kind: ClusterRoleBinding 21 | apiVersion: rbac.authorization.k8s.io/v1 22 | metadata: 23 | name: doryd 24 | subjects: 25 | - kind: ServiceAccount 26 | name: doryd 27 | namespace: kube-system 28 | roleRef: 29 | kind: ClusterRole 30 | name: doryd 31 | apiGroup: rbac.authorization.k8s.io 32 | --- 33 | apiVersion: v1 34 | kind: ServiceAccount 35 | metadata: 36 | name: doryd 37 | namespace: kube-system 38 | --- 39 | apiVersion: extensions/v1beta1 40 | kind: Deployment 41 | metadata: 42 | name: kube-storage-controller-doryd 43 | namespace: kube-system 44 | labels: 45 | app: doryd 46 | spec: 47 | strategy: 48 | type: RollingUpdate 49 | template: 50 | metadata: 51 | labels: 52 | daemon: kube-storage-controller-daemon 53 | name: kube-storage-controller 54 | spec: 55 | restartPolicy: Always 56 | serviceAccountName: doryd 57 | containers: 58 | - 59 | image: nimblestorage/kube-storage-controller:edge 60 | imagePullPolicy: Always 61 | name: kube-storage-controller 62 | volumeMounts: 63 | - name: k8s 64 | mountPath: /etc/kubernetes 65 | - name: flexvolumedriver 66 | mountPath: /usr/libexec/kubernetes/kubelet-plugins/volume/exec 67 | - name: dockersocket 68 | mountPath: /run/docker/plugins/ 69 | volumes: 70 | - name: k8s 71 | hostPath: 72 | path: /etc/kubernetes/ 73 | - name: flexvolumedriver 74 | hostPath: 75 | path: /usr/libexec/kubernetes/kubelet-plugins/volume/exec 76 | - name: dockersocket 77 | hostPath: 78 | path: /run/docker/plugins/ -------------------------------------------------------------------------------- /examples/dep-doryd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Deployment 4 | metadata: 5 | name: doryd 6 | spec: 7 | strategy: 8 | type: RollingUpdate 9 | template: 10 | metadata: 11 | namespace: kube-system 12 | labels: 13 | daemon: dory-daemon 14 | name: doryd 15 | spec: 16 | restartPolicy: Always 17 | affinity: 18 | nodeAffinity: 19 | requiredDuringSchedulingIgnoredDuringExecution: 20 | nodeSelectorTerms: 21 | - matchExpressions: 22 | - key: "node-role.kubernetes.io/master" 23 | operator: In 24 | values: [""] 25 | tolerations: 26 | - 27 | operator: Exists 28 | effect: NoSchedule 29 | containers: 30 | - 31 | image: nimblestorage/kube-storage-controller:edge 32 | imagePullPolicy: Always 33 | name: dory 34 | volumeMounts: 35 | - name: k8s 36 | mountPath: /etc/kubernetes 37 | - name: flexvolumedriver 38 | mountPath: /usr/libexec/kubernetes/kubelet-plugins/volume/exec 39 | - name: dockersocket 40 | mountPath: /run/docker/plugins/ 41 | volumes: 42 | - name: k8s 43 | hostPath: 44 | path: /etc/kubernetes/ 45 | - name: flexvolumedriver 46 | hostPath: 47 | path: /usr/libexec/kubernetes/kubelet-plugins/volume/exec 48 | - name: dockersocket 49 | hostPath: 50 | path: /run/docker/plugins/ 51 | -------------------------------------------------------------------------------- /examples/ds-doryd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: DaemonSet 4 | metadata: 5 | name: doryd 6 | spec: 7 | updateStrategy: 8 | type: RollingUpdate 9 | template: 10 | metadata: 11 | namespace: kube-system 12 | labels: 13 | daemon: dory-daemon 14 | name: doryd 15 | spec: 16 | restartPolicy: Always 17 | tolerations: 18 | - 19 | effect: NoSchedule 20 | operator: Exists 21 | containers: 22 | - 23 | image: nimblestorage/kube-storage-controller:edge 24 | imagePullPolicy: Always 25 | name: dory 26 | volumeMounts: 27 | - name: k8s 28 | mountPath: /etc/kubernetes 29 | - name: flexvolumedriver 30 | mountPath: /usr/libexec/kubernetes/kubelet-plugins/volume/exec 31 | - name: dockersocket 32 | mountPath: /run/docker/plugins/ 33 | volumes: 34 | - name: k8s 35 | hostPath: 36 | path: /etc/kubernetes/ 37 | - name: flexvolumedriver 38 | hostPath: 39 | path: /usr/libexec/kubernetes/kubelet-plugins/volume/exec 40 | - name: dockersocket 41 | hostPath: 42 | path: /run/docker/plugins/ 43 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: f2dc6db3b52750e32162ce23b7ac7590c75108dfdcbf7c55c819c85b4d74378d 2 | updated: 2018-06-18T17:53:12.437847707-04:00 3 | imports: 4 | - name: github.com/davecgh/go-spew 5 | version: 782f4967f2dc4564575ca782fe2d04090b5faca8 6 | subpackages: 7 | - spew 8 | - name: github.com/emicklei/go-restful 9 | version: ff4f55a206334ef123e4f79bbf348980da81ca46 10 | subpackages: 11 | - log 12 | - name: github.com/emicklei/go-restful-swagger12 13 | version: dcef7f55730566d41eae5db10e7d6981829720f6 14 | - name: github.com/ghodss/yaml 15 | version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee 16 | - name: github.com/go-openapi/jsonpointer 17 | version: 46af16f9f7b149af66e5d1bd010e3574dc06de98 18 | - name: github.com/go-openapi/jsonreference 19 | version: 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272 20 | - name: github.com/go-openapi/spec 21 | version: 6aced65f8501fe1217321abf0749d354824ba2ff 22 | - name: github.com/go-openapi/swag 23 | version: 1d0bd113de87027671077d3c71eb3ac5d7dbba72 24 | - name: github.com/gogo/protobuf 25 | version: c0656edd0d9eab7c66d1eb0c568f9039345796f7 26 | subpackages: 27 | - proto 28 | - sortkeys 29 | - name: github.com/golang/glog 30 | version: 44145f04b68cf362d9c4df2182967c2275eaefed 31 | - name: github.com/golang/groupcache 32 | version: 02826c3e79038b59d737d3b1c0a1d937f71a4433 33 | subpackages: 34 | - lru 35 | - name: github.com/golang/protobuf 36 | version: 4bd1920723d7b7c925de087aa32e2187708897f7 37 | subpackages: 38 | - proto 39 | - ptypes 40 | - ptypes/any 41 | - ptypes/duration 42 | - ptypes/timestamp 43 | - name: github.com/google/btree 44 | version: 7d79101e329e5a3adf994758c578dab82b90c017 45 | - name: github.com/google/gofuzz 46 | version: 44d81051d367757e1c7c6a5a86423ece9afcf63c 47 | - name: github.com/googleapis/gnostic 48 | version: 0c5108395e2debce0d731cf0287ddf7242066aba 49 | subpackages: 50 | - OpenAPIv2 51 | - compiler 52 | - extensions 53 | - name: github.com/gregjones/httpcache 54 | version: 787624de3eb7bd915c329cba748687a3b22666a6 55 | subpackages: 56 | - diskcache 57 | - name: github.com/hashicorp/golang-lru 58 | version: a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4 59 | subpackages: 60 | - simplelru 61 | - name: github.com/howeyc/gopass 62 | version: bf9dde6d0d2c004a008c27aaee91170c786f6db8 63 | - name: github.com/imdario/mergo 64 | version: 6633656539c1639d9d78127b7d47c622b5d7b6dc 65 | - name: github.com/json-iterator/go 66 | version: 36b14963da70d11297d313183d7e6388c8510e1e 67 | - name: github.com/juju/ratelimit 68 | version: 5b9ff866471762aa2ab2dced63c9fb6f53921342 69 | - name: github.com/mailru/easyjson 70 | version: d5b7844b561a7bc640052f1b935f7b800330d7e0 71 | subpackages: 72 | - buffer 73 | - jlexer 74 | - jwriter 75 | - name: github.com/peterbourgon/diskv 76 | version: 5f041e8faa004a95c88a202771f4cc3e991971e6 77 | - name: github.com/PuerkitoBio/purell 78 | version: 8a290539e2e8629dbc4e6bad948158f790ec31f4 79 | - name: github.com/PuerkitoBio/urlesc 80 | version: 5bd2802263f21d8788851d5305584c82a5c75d7e 81 | - name: github.com/satori/go.uuid 82 | version: f58768cc1a7a7e77a3bd49e98cdd21419399b6a3 83 | - name: github.com/spf13/pflag 84 | version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7 85 | - name: golang.org/x/crypto 86 | version: 81e90905daefcd6fd217b62423c0908922eadb30 87 | subpackages: 88 | - ssh/terminal 89 | - name: golang.org/x/net 90 | version: 1c05540f6879653db88113bc4a2b70aec4bd491f 91 | subpackages: 92 | - context 93 | - context/ctxhttp 94 | - http2 95 | - http2/hpack 96 | - idna 97 | - lex/httplex 98 | - name: golang.org/x/sys 99 | version: 7ddbeae9ae08c6a06a59597f0c9edbc5ff2444ce 100 | subpackages: 101 | - unix 102 | - windows 103 | - name: golang.org/x/text 104 | version: b19bf474d317b857955b12035d2c5acb57ce8b01 105 | subpackages: 106 | - cases 107 | - internal 108 | - internal/tag 109 | - language 110 | - runes 111 | - secure/bidirule 112 | - secure/precis 113 | - transform 114 | - unicode/bidi 115 | - unicode/norm 116 | - width 117 | - name: gopkg.in/inf.v0 118 | version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 119 | - name: gopkg.in/natefinch/lumberjack.v2 120 | version: a96e63847dc3c67d17befa69c303767e2f84e54f 121 | - name: gopkg.in/yaml.v2 122 | version: 53feefa2559fb8dfa8d81baad31be332c97d6c77 123 | - name: k8s.io/api 124 | version: 6c6dac0277229b9e9578c5ca3f74a4345d35cdc2 125 | subpackages: 126 | - admissionregistration/v1alpha1 127 | - apps/v1beta1 128 | - apps/v1beta2 129 | - authentication/v1 130 | - authentication/v1beta1 131 | - authorization/v1 132 | - authorization/v1beta1 133 | - autoscaling/v1 134 | - autoscaling/v2beta1 135 | - batch/v1 136 | - batch/v1beta1 137 | - batch/v2alpha1 138 | - certificates/v1beta1 139 | - core/v1 140 | - extensions/v1beta1 141 | - networking/v1 142 | - policy/v1beta1 143 | - rbac/v1 144 | - rbac/v1alpha1 145 | - rbac/v1beta1 146 | - scheduling/v1alpha1 147 | - settings/v1alpha1 148 | - storage/v1 149 | - storage/v1beta1 150 | - name: k8s.io/apimachinery 151 | version: 019ae5ada31de202164b118aee88ee2d14075c31 152 | subpackages: 153 | - pkg/api/equality 154 | - pkg/api/errors 155 | - pkg/api/meta 156 | - pkg/api/resource 157 | - pkg/apis/meta/internalversion 158 | - pkg/apis/meta/v1 159 | - pkg/apis/meta/v1/unstructured 160 | - pkg/apis/meta/v1alpha1 161 | - pkg/conversion 162 | - pkg/conversion/queryparams 163 | - pkg/conversion/unstructured 164 | - pkg/fields 165 | - pkg/labels 166 | - pkg/runtime 167 | - pkg/runtime/schema 168 | - pkg/runtime/serializer 169 | - pkg/runtime/serializer/json 170 | - pkg/runtime/serializer/protobuf 171 | - pkg/runtime/serializer/recognizer 172 | - pkg/runtime/serializer/streaming 173 | - pkg/runtime/serializer/versioning 174 | - pkg/selection 175 | - pkg/types 176 | - pkg/util/cache 177 | - pkg/util/clock 178 | - pkg/util/diff 179 | - pkg/util/errors 180 | - pkg/util/framer 181 | - pkg/util/intstr 182 | - pkg/util/json 183 | - pkg/util/mergepatch 184 | - pkg/util/net 185 | - pkg/util/runtime 186 | - pkg/util/sets 187 | - pkg/util/strategicpatch 188 | - pkg/util/validation 189 | - pkg/util/validation/field 190 | - pkg/util/wait 191 | - pkg/util/yaml 192 | - pkg/version 193 | - pkg/watch 194 | - third_party/forked/golang/json 195 | - third_party/forked/golang/reflect 196 | - name: k8s.io/client-go 197 | version: 2ae454230481a7cb5544325e12ad7658ecccd19b 198 | subpackages: 199 | - discovery 200 | - kubernetes 201 | - kubernetes/scheme 202 | - kubernetes/typed/admissionregistration/v1alpha1 203 | - kubernetes/typed/apps/v1beta1 204 | - kubernetes/typed/apps/v1beta2 205 | - kubernetes/typed/authentication/v1 206 | - kubernetes/typed/authentication/v1beta1 207 | - kubernetes/typed/authorization/v1 208 | - kubernetes/typed/authorization/v1beta1 209 | - kubernetes/typed/autoscaling/v1 210 | - kubernetes/typed/autoscaling/v2beta1 211 | - kubernetes/typed/batch/v1 212 | - kubernetes/typed/batch/v1beta1 213 | - kubernetes/typed/batch/v2alpha1 214 | - kubernetes/typed/certificates/v1beta1 215 | - kubernetes/typed/core/v1 216 | - kubernetes/typed/extensions/v1beta1 217 | - kubernetes/typed/networking/v1 218 | - kubernetes/typed/policy/v1beta1 219 | - kubernetes/typed/rbac/v1 220 | - kubernetes/typed/rbac/v1alpha1 221 | - kubernetes/typed/rbac/v1beta1 222 | - kubernetes/typed/scheduling/v1alpha1 223 | - kubernetes/typed/settings/v1alpha1 224 | - kubernetes/typed/storage/v1 225 | - kubernetes/typed/storage/v1beta1 226 | - pkg/version 227 | - rest 228 | - rest/watch 229 | - tools/auth 230 | - tools/cache 231 | - tools/clientcmd 232 | - tools/clientcmd/api 233 | - tools/clientcmd/api/latest 234 | - tools/clientcmd/api/v1 235 | - tools/metrics 236 | - tools/pager 237 | - tools/record 238 | - tools/reference 239 | - transport 240 | - util/cert 241 | - util/flowcontrol 242 | - util/homedir 243 | - util/integer 244 | - name: k8s.io/kube-openapi 245 | version: 868f2f29720b192240e18284659231b440f9cda5 246 | subpackages: 247 | - pkg/common 248 | testImports: [] 249 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/hpe-storage/dory 2 | import: 3 | - package: github.com/satori/go.uuid 4 | version: ~1.2.0 5 | - package: gopkg.in/natefinch/lumberjack.v2 6 | version: ~2.1.0 7 | - package: k8s.io/api 8 | subpackages: 9 | - core/v1 10 | - storage/v1 11 | - storage/v1beta1 12 | - package: k8s.io/apimachinery 13 | subpackages: 14 | - pkg/api/resource 15 | - pkg/apis/meta/v1 16 | - pkg/runtime 17 | - pkg/version 18 | - pkg/watch 19 | - package: k8s.io/client-go 20 | version: ~5.0.0 21 | subpackages: 22 | - kubernetes 23 | - kubernetes/scheme 24 | - kubernetes/typed/core/v1 25 | - tools/reference 26 | - rest 27 | - tools/cache 28 | - tools/clientcmd 29 | - tools/record --------------------------------------------------------------------------------