├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── BUILD.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── OWNERS.md ├── README.md ├── SUPPORT.md ├── arches.txt ├── build ├── README.md ├── build.sh └── test.sh ├── cmd └── csi-packet-driver │ └── main.go ├── deploy ├── demo │ ├── demo-deployment.yaml │ ├── demo-pod.yaml │ ├── demo-pv.yaml │ └── demo-statefulset.yaml ├── kubernetes │ ├── controller.yaml │ ├── node.yaml │ ├── node_controller_sanity_test.yaml │ └── setup.yaml └── template │ └── secret.yaml ├── docs └── background.md ├── go.mod ├── go.sum ├── isns-build.sh ├── packet-scripts ├── create_and_attach.sh └── detach_and_delete.sh └── pkg ├── .DS_Store ├── driver ├── attacher.go ├── controller.go ├── controller_test.go ├── driver.go ├── driver_test.go ├── exec.go ├── identity.go ├── initializer.go ├── mounter.go ├── node.go ├── node_test.go └── server.go ├── packet ├── errors.go ├── provider.go ├── provider_test.go ├── utilities.go └── volume.go ├── test └── volume_mock.go └── version └── version.go /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened] 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | report: 11 | name: Report 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: ref 15 | run: echo ${{ github.ref }} 16 | - name: event_name 17 | run: echo ${{ github.event_name }} 18 | build: 19 | name: Build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: checkout 23 | uses: actions/checkout@v1 24 | - uses: actions/setup-go@v1 25 | with: 26 | go-version: '1.14.3' # The Go version to download (if necessary) and use. 27 | - name: ci 28 | run: make ci 29 | - name: Register binfmt_misc entries for qemu-user-static 30 | run: make register 31 | - name: set buildx cross-builder 32 | run: | 33 | docker buildx create --name cross-builder 34 | docker buildx use cross-builder 35 | docker buildx inspect --bootstrap 36 | - name: check buildx 37 | run: docker buildx ls 38 | - name: image 39 | run: make image-all 40 | - name: hub login 41 | if: (github.event_name == 'push' && endsWith(github.ref,'/master')) || (github.event_name == 'create' && startsWith(github.ref,'refs/tags/')) 42 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin 43 | - name: deploy # when merged into master, tag master and push - ideally, this would be a separate job, but you cannot share docker build cache between jobs 44 | if: github.event_name == 'push' && endsWith(github.ref,'/master') 45 | run: make cd CONFIRM=true BRANCH_NAME=master 46 | - name: release # when based on a tag, tag master and push - ideally, this would be a separate job, but you cannot share docker build cache between jobs 47 | if: github.event_name == 'create' && startsWith(github.ref,'refs/tags/') 48 | run: make release CONFIRM=true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _vendor* 2 | vendor/ 3 | bin/ 4 | dist/bin/ 5 | hacks/ 6 | .go/ 7 | scratch 8 | .container* 9 | .docker* 10 | cloud-sa.json 11 | .cache 12 | .vscode 13 | dist/ 14 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # CSI plugin for Equinix Metal Build and Design 2 | 3 | The CSI Equinix Metal plugin allows the creation and mounting of metal storage volumes as 4 | persistent volume claims in a kubernetes cluster. 5 | 6 | ## Deploy 7 | Read how to deploy the Kubernetes CSI plugin for Equnix Metal in the [README.md](./README.md)! 8 | 9 | ## Design 10 | The basic refernce for Kubernetes CSI is found at https://kubernetes-csi.github.io/docs/ 11 | 12 | A typical sequence of the tasks performed by the Controller and Node components is 13 | 14 | - **Create** *Controller.CreateVolume* 15 | - **Attach** *Controller.ControllerPublish* 16 | - **Mount, Format** *Node.NodeStageVolume* (called once only per volume) 17 | - **Bind Mount** *Node.NodePublishVolume* 18 | - **Bind Unmount** *Node.NodeUnpublishVolume* 19 | - **Unmount** *Node.NodeUnstageVolume* 20 | - **Detach** *Controller.ControllerUnpublish* 21 | - **Destroy** *Controller.DeleteVolum*e 22 | 23 | 24 | ## System configuration 25 | 26 | The plugin node component require particular configuration of the metal host with regard to the services that are running. 27 | It relies on iscsid being configured correctly with the initiator name, and up multipathd running with a configuration that includes `user_friendly_names yes` This setup is not perfomed by the plugin. 28 | 29 | ## Deployment 30 | 31 | The files found in `deploy/kubernetes/` define the deployment process, which follows the approach diagrammed in the [design proposal](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/container-storage-interface.md#recommended-mechanism-for-deploying-csi-drivers-on-kubernetes) 32 | 33 | The documentation for performing the deployment is in the [README.md](./README.md). 34 | 35 | ### Equinix Metal credentials 36 | 37 | Equinix Metal credentials are used by the controller to manage volumes. They are configured with a json-formatted secret which contains 38 | 39 | * an authetication token 40 | * a project id 41 | * a facility id 42 | 43 | The cluster is assumed to reside wholly in one facility, so the controller includes that facility id in all of the volume-related api calls. 44 | 45 | ### RBAC 46 | 47 | The file [deploy/kubernetes/setup.yaml](./deploy/kubernetes/setup.yaml) contains the `ServiceAccount, `Role` and `RoleBinding` definitions used by the various components. 48 | 49 | ### Deployment 50 | 51 | The controller is deployed as a `StatefulSet` to ensure that there is a single instance of the pod. The node is deployed as a `DaemonSet`, which will install an instance of the pod on every un-tainted node. In most cluster deployments, the master node will have a `node-role.kubernetes.io/master:NoSchedule` taint and so the csi-packet plugin will not operate there. 52 | 53 | ### Helper sidecar containers 54 | 55 | The CSI plugin framework is designed to be agnostic with respect to the Container Orchestrator system, and so there is no direct communication between the CSI plugin and the kubernetes api. Instead, the CSI project provides a number of sidecar containers which mediate beween the plugin and kubernetes. 56 | 57 | The controller deployment uses 58 | 59 | * [external-attacher](https://github.com/kubernetes-csi/external-attacher) 60 | * [external-provisioner](https://github.com/kubernetes-csi/external-provisioner) 61 | 62 | which communicate with the kubernetes api within the cluster, and communicate with the csi-packet plugin through a unix domain socket shared in the pod. 63 | 64 | The node deployment uses 65 | 66 | * [driver-registrar](https://github.com/kubernetes-csi/driver-registrar) 67 | 68 | which advertises the csi-packet driver to the cluster. There is also by unix domain socket communciation channel, but in this case it is a host-mounted directory in order to permit the kubelet process to interact with the plugin. 69 | 70 | TODO: incorporate the liveness prob 71 | 72 | * [liveness-probe](https://github.com/kubernetes-csi/livenessprobe) 73 | 74 | ### Mounted volumes and privilege 75 | 76 | The node processes must interact with services running on the host in order to connect, mount and format the Equinix Metal volumes. These interactions require a particular pod configuration. The driver invokes the *iscsiadm* and *multipath* client processes and they must communicate with the *iscisd* and *multipathd* systemd services. In consequence, the pod 77 | - uses `hostNetwork: true` 78 | - uses `privileged: true` 79 | - mounts `/etc` 80 | - mounts `/dev` 81 | - mounts `/var/lib/iscsi` 82 | - mounts `/sys/devices` 83 | - mounts `/run/udev/` 84 | - mounts `/var/lib/kubelet` 85 | - mounts `/csi` 86 | 87 | 88 | ## Further documentation 89 | 90 | See additional documents in the `docs/` directory 91 | 92 | ## Development 93 | The Makefile builds locally using your own installed `go`, or in a container via `docker run`. 94 | 95 | The following are standard commands: 96 | 97 | * `make build ARCH=$(ARCH)` - ensure vendor dependencies and build the single CSI binary as `dist/bin/packet-cloud-storage-interface-$(ARCH)` 98 | * `make build-all` - ensure vendor dependencies and build the single CSI binary for all supported architectures. We build a binary for each arch for which a `Dockerfile.` exists in this directory. 99 | * `make build ARCH=$(ARCH) DOCKERBUILD=true` - ensure vendor dependencies and build the CSI binary as above, while performing the build inside a docker container 100 | * `make build-all` - ensure vendor dependencies and build the CSI binary for all supported architectures. 101 | * `make image` - make an OCI image for your current ARCH. 102 | * `make image ARCH=$(ARCH)` - make an OCI image for the provided ARCH, as distinct from your current ARCH. Requires that you have [binfmt](https://en.wikipedia.org/wiki/Binfmt_misc) support. 103 | * `make image-all` - make an OCI image for all supported architectures 104 | * `make ci` - build, test and create an OCI image for all supported architectures. This is what the CI system runs. 105 | * `make cd` - deploy images for all supported architectures, as well as a multi-arch manifest.. 106 | * `make release` - deploy tagged release images for all supported architectures, as well as a multi-arch manifest.. 107 | 108 | All images are tagged `$(TAG)-$(ARCH)`, where: 109 | 110 | * `TAG` = the image tag, which always includes the current branch when merged into `master`, and the short git hash. For git tags that are applied in master via `make release`, it also is the git tag. Thus a normal merge releases two image tags - git hash and `master` - while adding a git tag to release formally creates a third one. 111 | * `ARCH` = the architecture of the image 112 | 113 | In addition, we use multi-arch manifests for "archless" releases, e.g. `:3456abcd` or `:v1.2.3` or `:master`. 114 | 115 | ## Dockerfiles 116 | 117 | This repository supports a single version of the [Dockerfile](./Dockerfile), supporting both building on your own architecture, e.g. `amd64` on `amd64`, and cross, e.g. `arm64` on `amd64`. In _all cases_, you must set the following `--build-arg` options: 118 | 119 | * `BINARCH` - target arch for the binary, compatible with `GOARCH` 120 | * `REPOARCH` - target arch for the image, compatible with the docker hub repositories, i.e. `amd64`, `arm64v8` 121 | 122 | If cross-compiling an image to an alternate architecture, you have two additional requirements: 123 | 124 | * [binfmt](https://en.wikipedia.org/wiki/Binfmt_misc) support 125 | * a Linux kernel version of 4.8 or higher, so that you don't have to copy qemu-static into the container. 126 | 127 | To simpify commands, you can do `make image` (which is what `make` is for in the first place): 128 | 129 | * `make image` - build for your local architecture 130 | * `make image ARCH=amd64` - build for `amd64` 131 | * `make image ARCH=arm64` - build for `arm64` 132 | 133 | etc. 134 | 135 | In all cases, `make image` will set the correct `--build-arg` arguments. 136 | 137 | The image always will be tagged `packethost/csi-packet:latest-`, e.g. `packethost/csi-packet:latest-amd64`. 138 | 139 | ## Supported Platforms 140 | 141 | Equinix Metal CSI is supported on Linux only. As of this writing, it supports the architectures listed in [arch.txt](./arch.txt). 142 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at support@equinixmetal.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Hello Contributors! 2 | Thx for your interest! We're so glad you're here. 3 | 4 | ### Code of Conduct 5 | Available via [https://github.com/packethost/csi-packet/blob/master/CODE_OF_CONDUCT.md](https://github.com/packethost/csi-packet/blob/master/CODE_OF_CONDUCT.md) 6 | 7 | ### Environment Details 8 | [https://github.com/packethost/csi-packet/blob/master/MANIFEST.md](https://github.com/packethost/csi-packet/blob/master/MANIFEST.md) 9 | 10 | ### How to Report a Bug 11 | Bugs are problems in code, in the functionality of an application or in its UI design; you can submit them via our [Support channels](https://github.com/packethost/csi-packet/blob/master/SUPPORT.md); however, please note that "[Elastic Block Storage is only available in Core Legacy Sites: AMS1, DFW2, EWR1, NRT1, SJC1. If you do not have access to these sites, you may reach out to our support team to request it.](https://metal.equinix.com/developers/docs/resilience-recovery/elastic-block-storage/#legacy-only-sites)" 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # global ARG ARCH to set target ARCH 2 | ARG BINARCH 3 | ARG REPOARCH 4 | 5 | ## build the go binary 6 | # build container runs on local arch 7 | FROM golang:1.12.6 as build 8 | ARG BINARCH 9 | 10 | ARG pkgpath=/go/src/github.com/packethost/csi-packet/ 11 | ENV GO111MODULE=on 12 | RUN mkdir -p $pkgpath 13 | WORKDIR $pkgpath 14 | # separate steps to avoid cache busting 15 | COPY go.mod go.sum $pkgpath 16 | RUN go mod download 17 | COPY . $pkgpath 18 | RUN make build install DESTDIR=/dist ARCH=${BINARCH} 19 | 20 | ## build iscsi 21 | FROM ${REPOARCH}/gcc:9.2.0 as iscsi-build 22 | 23 | RUN apt update && apt install -y libkmod-dev libsystemd-dev 24 | RUN mkdir /src 25 | 26 | WORKDIR /src 27 | RUN git clone https://github.com/open-iscsi/open-isns.git 28 | WORKDIR /src/open-isns 29 | COPY isns-build.sh /tmp 30 | RUN git checkout cfdbcff867ee580a71bc9c18c3a38a6057df0150 && /tmp/isns-build.sh && ./configure && make && make install install_hdrs install_lib 31 | 32 | WORKDIR /src 33 | RUN git clone https://github.com/open-iscsi/open-iscsi.git 34 | WORKDIR /src/open-iscsi 35 | # install to a fresh tree under /dist 36 | RUN mkdir /dist && git checkout 288add22d6b61cc68ede358faeec9affb15019cd && make && make install DESTDIR=/dist 37 | 38 | FROM ${REPOARCH}/ubuntu:18.04 39 | ARG BINARCH 40 | 41 | RUN apt-get update 42 | RUN apt-get install -y wget multipath-tools open-iscsi curl jq 43 | 44 | # now install latest open-iscsi, ensuring it is *after* the apt install is done 45 | # we need to use the tmpdir, because some archs install in /usr/lib, and others in /usr/lib64 46 | COPY --from=iscsi-build /dist /tmp/distiscsi 47 | WORKDIR /tmp/distiscsi 48 | RUN mv sbin/* /sbin 49 | RUN if [ -d usr/lib64 ]; then mkdir -p /usr/lib64; mv usr/lib64/* /usr/lib64; fi 50 | RUN if [ -d usr/lib ]; then mkdir -p /usr/lib; mv usr/lib/* /usr/lib; fi 51 | WORKDIR / 52 | RUN rm -rf /tmp/distiscsi 53 | 54 | # we need to do use the tmpdir, because the COPY command cannot run $(..) and save the output 55 | COPY --from=build /dist/packet-cloud-storage-interface-${BINARCH} /packet-cloud-storage-interface 56 | 57 | ENV LD_LIBRARY_PATH=/usr/lib64:$LD_LIBRARY_PATH 58 | 59 | ENTRYPOINT ["/packet-cloud-storage-interface"] 60 | -------------------------------------------------------------------------------- /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, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 StackPointCloud, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/sh 2 | BINARY ?= packet-cloud-storage-interface 3 | BUILD_IMAGE?=packethost/csi-packet 4 | BUILDER_IMAGE?=packethost/go-build 5 | PACKAGE_NAME?=github.com/packethost/csi-packet 6 | GIT_VERSION?=$(shell git log -1 --format="%h") 7 | VERSION?=$(GIT_VERSION) 8 | RELEASE_TAG ?= $(shell git tag --points-at HEAD) 9 | ifneq (,$(RELEASE_TAG)) 10 | VERSION=$(RELEASE_TAG)-$(VERSION) 11 | endif 12 | GO_FILES := $(shell find . -type f -not -path './vendor/*' -name '*.go') 13 | FROMTAG ?= latest 14 | LDFLAGS ?= -ldflags '-extldflags "-static" -X "$(PACKAGE_NAME)/pkg/version.VERSION=$(VERSION)"' 15 | 16 | # which arches can we support 17 | ARCHES=$(shell cat arches.txt) 18 | 19 | QEMU_VERSION?=4.2.0-7 20 | QEMU_IMAGE?=multiarch/qemu-user-static:$(QEMU_VERSION) 21 | 22 | # BUILDARCH is the host architecture 23 | # ARCH is the target architecture 24 | # we need to keep track of them separately 25 | BUILDARCH ?= $(shell uname -m) 26 | BUILDOS ?= $(shell uname -s | tr A-Z a-z) 27 | 28 | # canonicalized names for host architecture 29 | ifeq ($(BUILDARCH),aarch64) 30 | BUILDARCH=arm64 31 | endif 32 | ifeq ($(BUILDARCH),x86_64) 33 | BUILDARCH=amd64 34 | endif 35 | 36 | # unless otherwise set, I am building for my own architecture, i.e. not cross-compiling 37 | ARCH ?= $(BUILDARCH) 38 | 39 | # canonicalized names for target architecture 40 | ifeq ($(ARCH),aarch64) 41 | override ARCH=arm64 42 | endif 43 | ifeq ($(ARCH),x86_64) 44 | override ARCH=amd64 45 | endif 46 | 47 | # I also need the names for the hub images 48 | REPOARCH ?= $(ARCH) 49 | # canonicalize these because they are not consistent 50 | ifeq ($(REPOARCH),arm64) 51 | override REPOARCH=arm64v8 52 | endif 53 | 54 | 55 | IMAGENAME ?= $(BUILD_IMAGE):$(IMAGETAG)-$(ARCH) 56 | 57 | # Manifest tool, until `docker manifest` is fully ready. As of this writing, it remains experimental 58 | MANIFEST_VERSION ?= 1.0.0 59 | MANIFEST_URL = https://github.com/estesp/manifest-tool/releases/download/v$(MANIFEST_VERSION)/manifest-tool-$(BUILDOS)-$(BUILDARCH) 60 | 61 | # these macros create a list of valid architectures for pushing manifests 62 | space := 63 | space += 64 | comma := , 65 | prefix_linux = $(addprefix linux/,$(strip $1)) 66 | join_platforms = $(subst $(space),$(comma),$(call prefix_linux,$(strip $1))) 67 | 68 | export GO111MODULE=on 69 | DIST_DIR=./dist/bin 70 | DIST_BINARY = $(DIST_DIR)/$(BINARY)-$(ARCH) 71 | DIST_BINARY_FILE = $(shell basename $(DIST_BINARY)) 72 | BUILD_CMD = GOOS=linux GOARCH=$(ARCH) CGO_ENABLED=0 73 | ifdef DOCKERBUILD 74 | BUILD_CMD = docker run --rm \ 75 | -e GOARCH=$(ARCH) \ 76 | -e GOOS=linux \ 77 | -e CGO_ENABLED=0 \ 78 | -v $(CURDIR):/go/src/$(PACKAGE_NAME) \ 79 | -w /go/src/$(PACKAGE_NAME) \ 80 | $(BUILDER_IMAGE) 81 | endif 82 | 83 | GOBIN ?= $(shell go env GOPATH)/bin 84 | LINTER ?= $(GOBIN)/golangci-lint 85 | MANIFEST_TOOL ?= $(GOBIN)/manifest-tool 86 | 87 | pkgs: 88 | ifndef PKG_LIST 89 | $(eval PKG_LIST := $(shell $(BUILD_CMD) go list ./... | grep -v vendor)) 90 | endif 91 | 92 | .PHONY: fmt-check lint test vet golint tag version 93 | 94 | $(DIST_DIR): 95 | mkdir -p $@ 96 | 97 | ## report the git tag that would be used for the images 98 | tag: 99 | @echo $(GIT_VERSION) 100 | 101 | ## report the version that would be put in the binary 102 | version: 103 | @echo $(VERSION) 104 | 105 | 106 | ## Check the file format 107 | fmt-check: 108 | @if [ -n "$(shell $(BUILD_CMD) gofmt -l ${GO_FILES})" ]; then \ 109 | $(BUILD_CMD) gofmt -s -e -d ${GO_FILES}; \ 110 | exit 1; \ 111 | fi 112 | 113 | golangci-lint: $(LINTER) 114 | $(LINTER): 115 | mkdir -p hacks && cd hacks && (go mod init hacks || true) && go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.27.0 116 | 117 | golint: 118 | ifeq (, $(shell which golint)) 119 | go get -u golang.org/x/lint/golint 120 | endif 121 | 122 | ## Lint the files 123 | lint: pkgs golangci-lint 124 | @$(BUILD_CMD) $(LINTER) run --disable-all --enable=golint pkg/... cmd/... 125 | 126 | ## Run unittests 127 | test: pkgs 128 | @$(BUILD_CMD) go test -short ${PKG_LIST} 129 | 130 | ## Vet the files 131 | vet: pkgs 132 | @$(BUILD_CMD) go vet ${PKG_LIST} 133 | 134 | ## Read about data race https://golang.org/doc/articles/race_detector.html 135 | ## to not test file for race use `// +build !race` at top 136 | ## Run data race detector 137 | race: pkgs 138 | @$(BUILD_CMD) go test -race -short ${PKG_LIST} 139 | 140 | ## Display this help screen 141 | help: 142 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 143 | 144 | ## Delete the csi 145 | undeploy: 146 | kubectl delete --now -f releases/v0.0.0.yaml 147 | 148 | ## Deploy the controller to kubernetes 149 | deploy: 150 | kubectl apply -f releases/v0.0.0.yaml 151 | 152 | 153 | 154 | .PHONY: build build-all image push deploy ci cd dep manifest-tool 155 | 156 | ## Build the binaries for all supported ARCH 157 | build-all: $(addprefix sub-build-, $(ARCHES)) 158 | sub-build-%: 159 | @$(MAKE) ARCH=$* build 160 | 161 | ## Build the binary for a single ARCH 162 | build: $(DIST_BINARY) 163 | $(DIST_BINARY): $(DIST_DIR) 164 | $(BUILD_CMD) go build -v -o $@ $(LDFLAGS) $(PACKAGE_NAME)/cmd/csi-packet-driver 165 | 166 | ## Report the supported arches 167 | arches: 168 | @echo $(ARCHES) 169 | 170 | ## copy a binary to an install destination 171 | install: 172 | ifneq (,$(DESTDIR)) 173 | mkdir -p $(DESTDIR) 174 | cp $(DIST_BINARY) $(DESTDIR)/$(DIST_BINARY_FILE) 175 | endif 176 | 177 | manifest-tool: $(MANIFEST_TOOL) 178 | $(MANIFEST_TOOL): 179 | curl -L -o $@ $(MANIFEST_URL) 180 | chmod +x $@ 181 | 182 | ## make the images for all supported ARCH 183 | image-all: $(addprefix sub-image-, $(ARCHES)) 184 | sub-image-%: 185 | @$(MAKE) ARCH=$* image 186 | 187 | ## make the image for a single ARCH 188 | image: 189 | docker image build -t $(BUILD_IMAGE):latest-$(ARCH) -f Dockerfile --build-arg BINARCH=${ARCH} --build-arg REPOARCH=${REPOARCH} . 190 | 191 | # Targets used when cross building. 192 | .PHONY: register 193 | # Enable binfmt adding support for miscellaneous binary formats. 194 | # This is only needed when running non-native binaries. 195 | register: 196 | docker pull $(QEMU_IMAGE) 197 | docker run --rm --privileged $(QEMU_IMAGE) --reset -p yes || true 198 | 199 | ## push the multi-arch manifest 200 | push-manifest: manifest-tool imagetag 201 | # path to credentials based on manifest-tool's requirements here https://github.com/estesp/manifest-tool#sample-usage 202 | $(GOBIN)/manifest-tool push from-args --platforms $(call join_platforms,$(ARCHES)) --template $(BUILD_IMAGE):$(IMAGETAG)-ARCH --target $(BUILD_IMAGE):$(IMAGETAG) 203 | 204 | ## push the images for all supported ARCH 205 | push-all: imagetag $(addprefix sub-push-, $(ARCHES)) 206 | sub-push-%: 207 | @$(MAKE) ARCH=$* push IMAGETAG=$(IMAGETAG) 208 | 209 | push: imagetag 210 | docker push $(IMAGENAME) 211 | 212 | # ensure we have a real imagetag 213 | imagetag: 214 | ifndef IMAGETAG 215 | $(error IMAGETAG is undefined - run using make IMAGETAG=X.Y.Z) 216 | endif 217 | 218 | ## tag the images for all supported ARCH 219 | tag-images-all: $(addprefix sub-tag-image-, $(ARCHES)) 220 | sub-tag-image-%: 221 | @$(MAKE) ARCH=$* IMAGETAG=$(IMAGETAG) tag-images 222 | 223 | tag-images: imagetag 224 | docker tag $(BUILD_IMAGE):$(FROMTAG)-$(ARCH) $(IMAGENAME) 225 | 226 | ## ensure that a particular tagged image exists across all support archs 227 | pull-images-all: $(addprefix sub-pull-image-, $(ARCHES)) 228 | sub-pull-image-%: 229 | @$(MAKE) ARCH=$* IMAGETAG=$(IMAGETAG) pull-images 230 | 231 | ## ensure that a particular tagged image exists locally; if not, pull it 232 | pull-images: imagetag 233 | @if [ "$$(docker image ls -q $(IMAGENAME))" = "" ]; then \ 234 | docker pull $(IMAGENAME); \ 235 | fi 236 | 237 | ## clean up all artifacts 238 | clean: 239 | $(eval IMAGE_TAGS := $(shell docker image ls | awk "/^$(subst /,\/,$(BUILD_IMAGE))\s/"' {print $$2}' )) 240 | docker image rm $(addprefix $(BUILD_IMAGE):,$(IMAGE_TAGS)) 241 | rm -rf dist/ 242 | 243 | ############################################################################### 244 | # CI/CD 245 | ############################################################################### 246 | .PHONY: ci cd build deploy push release confirm pull-images 247 | ## Run what CI runs 248 | # race has an issue with alpine, see https://github.com/golang/go/issues/14481 249 | # image-all is removed so we can build locally not as part of CI 250 | ci: build-all fmt-check lint test vet # image-all race 251 | 252 | confirm: 253 | ifndef CONFIRM 254 | $(error CONFIRM is undefined - run using make CONFIRM=true) 255 | endif 256 | 257 | cd: confirm image-all 258 | ifndef BRANCH_NAME 259 | $(error BRANCH_NAME is undefined - run using make BRANCH_NAME=var or set an environment variable) 260 | endif 261 | $(MAKE) tag-images-all push-all push-manifest IMAGETAG=${BRANCH_NAME} 262 | $(MAKE) tag-images-all push-all push-manifest IMAGETAG=${GIT_VERSION} 263 | 264 | ## cut a release by using the latest git tag should only be run for an image that already exists and was pushed out 265 | release: confirm 266 | ifeq (,$(RELEASE_TAG)) 267 | $(error RELEASE_TAG is undefined - this means we are trying to do a release at a commit which does not have a release tag) 268 | endif 269 | $(MAKE) pull-images-all IMAGETAG=${GIT_VERSION} # ensure we have the image with the tag ${GIT_VERSION} or pull it 270 | $(MAKE) tag-images-all FROMTAG=${GIT_VERSION} IMAGETAG=${RELEASE_TAG} # tag the pulled image 271 | $(MAKE) push-all push-manifest IMAGETAG=${RELEASE_TAG} # push it 272 | 273 | 274 | csi: build deploy ## Build and deploy the csi 275 | -------------------------------------------------------------------------------- /OWNERS.md: -------------------------------------------------------------------------------- 1 | # Owners 2 | 3 | This project is governed by [Equinix Metal](https://metal.equinix.com) and benefits from a community of users that collaborate and contribute to its use in Kubernetes on Equinix Metal. 4 | 5 | Members of the Equinix Metal Github organization will strive to triage issues in a timely manner, see [SUPPORT.md](SUPPORT.md) for details. 6 | 7 | See the [packethost/standards glossary](https://github.com/packethost/standards/blob/master/glossary.md#ownersmd) for more details about this file. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Container Storage Interface (CSI) plugin for Equinix Metal 2 | 3 | [![GitHub release](https://img.shields.io/github/release/packethost/csi-packet/all.svg?style=flat-square)](https://github.com/packethost/csi-packet/releases) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/packethost/csi-packet)](https://goreportcard.com/report/github.com/packethost/csi-packet) 5 | ![Continuous Integration](https://github.com/packethost/csi-packet/workflows/Continuous%20Integration/badge.svg) 6 | [![Docker Pulls](https://img.shields.io/docker/pulls/packethost/csi-packet.svg)](https://hub.docker.com/r/packethost/csi-packet/) 7 | [![Slack](https://slack.equinixmetal.com/badge.svg)](https://slack.equinixmetal.com) 8 | [![Twitter Follow](https://img.shields.io/twitter/follow/equinixmetal.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=equinixmetal) 9 | [![End of Life](https://img.shields.io/badge/Stability-EndOfLife-black.svg)](https://github.com/packethost/standards/blob/main/end-of-life-statement.md#end-of-life-statements) 10 | 11 | `csi-packet` was the Kubernetes CSI implementation for [Equinix Metal](https://metal.equinix.com/) Block Storage provided by [Datera](https://datera.io/). Read more about the CSI standard [here](https://kubernetes-csi.github.io/docs/). 12 | 13 | This repository is [End-Of-Life](https://github.com/packethost/standards/blob/main/end-of-life-statement.md) meaning that this software is no longer supported nor maintained by Equinix Metal or its community. 14 | 15 | *_The following information is obsolete. Please see for alternatives._* 16 | 17 | --- 18 | 19 | **If you have any queries about CSI or would like to raise any bug reports or features requests please [contact support](https://github.com/packethost/csi-packet/blob/master/SUPPORT.md).** 20 | 21 | Please Note: "[Elastic Block Storage is only available in Core Legacy Sites: AMS1, DFW2, EWR1, NRT1, SJC1. If you do not have access to these sites, you may reach out to our support team to request it.](https://metal.equinix.com/developers/docs/resilience-recovery/elastic-block-storage/#legacy-only-sites)" 22 | 23 | ## Requirements 24 | 25 | At the current state of Kubernetes, running the CSI requires a few things. 26 | Please read through the requirements carefully as they are critical to running the CSI on a Kubernetes cluster. 27 | 28 | ### Version 29 | 30 | Recommended versions of Equinix Metal CSI based on your Kubernetes version: 31 | * Equinix Metal CSI version v0.0.2 supports Kubernetes version >=v1.10 32 | 33 | ### Privilege 34 | 35 | In order for CSI to work, your kubernetes cluster **must** allow privileged pods. Both the `kube-apiserver` and the kubelet must start with the flag `--allow-privileged=true`. 36 | 37 | 38 | ## Deploying in a kubernetes cluster 39 | 40 | ### Token 41 | 42 | To run `csi-packet`, you need your Equinix Metal api key and project ID that your cluster is running in. 43 | If you are already logged in, you can create one by clicking on your profile in the upper right then "API keys". 44 | To get project ID click into the project that your cluster is under and select "project settings" from the header. 45 | Under General you will see "Project ID". Once you have this information you will be able to fill in the config needed for the CCM. 46 | 47 | #### Create config 48 | 49 | Copy [deploy/template/secret.yaml](./deploy/template/secret.yaml) to a local file: 50 | 51 | ```bash 52 | cp deploy/template/secret.yaml packet-cloud-config.yaml 53 | ``` 54 | 55 | Replace the placeholder in the copy with your token. When you're done, the `packet-cloud-config.yaml` should look something like this: 56 | 57 | ```yaml 58 | apiVersion: v1 59 | kind: Secret 60 | metadata: 61 | name: packet-cloud-config 62 | namespace: kube-system 63 | stringData: 64 | cloud-sa.json: | 65 | { 66 | "apiKey": "abc123abc123abc123", 67 | "projectID": "abc123abc123abc123" 68 | } 69 | ``` 70 | 71 | Then run: 72 | 73 | ```bash 74 | kubectl apply -f ./packet-cloud-config.yaml` 75 | ``` 76 | 77 | You can confirm that the secret was created in the `kube-system` with the following: 78 | 79 | ```bash 80 | $ kubectl -n kube-system get secrets packet-cloud-config 81 | NAME TYPE DATA AGE 82 | packet-cloud-config Opaque 1 2m 83 | ``` 84 | 85 | **Note:** This is the _exact_ same config as used for [Equinix Metal CCM](https://github.com/packethost/packet-ccm), allowing you to create a single set of credentials in a single secret to support both. 86 | 87 | ### Set up Driver 88 | 89 | ``` 90 | $ kubectl -n kube-system apply -f deploy/kubernetes/setup.yaml 91 | $ kubectl -n kube-system apply -f deploy/kubernetes/node.yaml 92 | $ kubectl -n kube-system apply -f deploy/kubernetes/controller.yaml 93 | ``` 94 | 95 | ### Run demo (optional): 96 | 97 | ``` 98 | $ kubectl apply -f deploy/demo/demo-deployment.yaml 99 | ``` 100 | 101 | ## Command-Line Options 102 | 103 | You can run the binary with `--help` to get command-line options. Important options are: 104 | 105 | * `--endpoint=` : (required) path to the kubelet registration socket. According to the spec, this should be `/var/lib/kubelet/plugins//csi.sock`. Thus we **strongly** recommend you mount it at `/var/lib/kubelet/plugins/csi.packet.net/csi.sock`. The deployment files in this repository assume that path. 106 | * `--v=` : (optional) verbosity level per [logrus](https://github.com/sirupsen/logrus) 107 | * `--config=` : (optional) path to config file, in json format, that contains the Equinix Metal configuration information as set below. 108 | * `--nodeid=` : (optional) override the unique ID of this node as understood by the Equinix Metal API. If not provided, will retrieve the node ID from the Equinix Metal Metadata service. 109 | 110 | ### Config File Format 111 | 112 | The configuration file passed to `--config` must be a json file, and should contain the following keys: 113 | 114 | * `apiKey` : Equinix Metal API key to use 115 | * `projectID` : Equinix Metal project ID 116 | * `facilityID` : Equinix Metal facility ID 117 | 118 | ### Environment Variables 119 | 120 | In addition to passing information via the config file, you can set it in environment variables. Environment variables _always_ override any setting in the config file. The variables are: 121 | 122 | * `PACKET_API_KEY` 123 | * `PACKET_PROJECT_ID` 124 | * `PACKET_FACILITY_ID` 125 | 126 | ## Running the csi-sanity tests 127 | 128 | [csi-sanity](https://github.com/kubernetes-csi/csi-test/tree/master/cmd/csi-sanity) is a set of integration tests that can be run on a host where a csi-plugin is running. 129 | In a kubernetes cluster, _csi-sanity_ can be run on a node and communicate with the daemonset node controller running there. 130 | 131 | The steps are as follows 132 | 133 | 1. Install the `csi-packet` plugin as above into a kubernetes cluster, but use `node_controller_sanity_test.yaml` instead of `node.yaml`. 134 | The crucial difference is to start the driver with the Equinix Metal credentials so that the csi-controller is running. 135 | 2. `ssh` to a node, install a golang environment and build the csi-sanity binaries. 136 | 3. Run `./csi-sanity --ginkgo.v --csi.endpoint=/var/lib/kubelet/plugins/csi.packet.net/csi.sock` 137 | 138 | Please report any failures to this repository. 139 | 140 | ## Build and Design 141 | 142 | To build the Equinix Metal CSI and understand its design, please see [BUILD.md](./BUILD.md). 143 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | Please Note: "[Elastic Block Storage is only available in Core Legacy Sites: AMS1, DFW2, EWR1, NRT1, SJC1. If you do not have access to these sites, you may reach out to our support team to request it.](https://metal.equinix.com/developers/docs/resilience-recovery/elastic-block-storage/#legacy-only-sites)" 2 | 3 | If you require support, please email support@equinixmetal.com, visit the Equinix Metal IRC channel (#equinixmetal on freenode), or subscribe to the [Equnix Metal Community Slack](https://slack.equnixmetal.com/) channel. 4 | 5 | [Contributions](CONTRIBUTING.md) are welcome to help extend this work! 6 | -------------------------------------------------------------------------------- /arches.txt: -------------------------------------------------------------------------------- 1 | arm64 2 | amd64 3 | -------------------------------------------------------------------------------- /build/README.md: -------------------------------------------------------------------------------- 1 | # Old build dir 2 | This is a script that held the old build and test scripts. These are kept for posterity, as everything is now in the Makefile. 3 | 4 | DEPRECATED 5 | 6 | -------------------------------------------------------------------------------- /build/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2016 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | if [ -z "${PKG}" ]; then 22 | echo "PKG must be set" 23 | exit 1 24 | fi 25 | if [ -z "${ARCH}" ]; then 26 | echo "ARCH must be set" 27 | exit 1 28 | fi 29 | if [ -z "${VERSION}" ]; then 30 | echo "VERSION must be set" 31 | exit 1 32 | fi 33 | 34 | export CGO_ENABLED=0 35 | export GOARCH="${ARCH}" 36 | 37 | go install \ 38 | -installsuffix "static" \ 39 | -ldflags "-X ${PKG}/pkg/version.VERSION=${VERSION}" \ 40 | ./... 41 | -------------------------------------------------------------------------------- /build/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2016 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | export CGO_ENABLED=0 22 | 23 | TARGETS=$(for d in "$@"; do echo ./$d/...; done) 24 | 25 | echo "Running tests:" 26 | go test -i -installsuffix "static" ${TARGETS} 27 | go test -installsuffix "static" ${TARGETS} 28 | echo 29 | 30 | echo -n "Checking gofmt: " 31 | ERRS=$(find "$@" -type f -name \*.go | xargs gofmt -l 2>&1 || true) 32 | if [ -n "${ERRS}" ]; then 33 | echo "FAIL - the following files need to be gofmt'ed:" 34 | for e in ${ERRS}; do 35 | echo " $e" 36 | done 37 | echo 38 | exit 1 39 | fi 40 | echo "PASS" 41 | echo 42 | 43 | echo -n "Checking go vet: " 44 | ERRS=$(go vet ${TARGETS} 2>&1 || true) 45 | if [ -n "${ERRS}" ]; then 46 | echo "FAIL" 47 | echo "${ERRS}" 48 | echo 49 | exit 1 50 | fi 51 | echo "PASS" 52 | echo 53 | -------------------------------------------------------------------------------- /cmd/csi-packet-driver/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 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 | "encoding/json" 21 | "flag" 22 | "fmt" 23 | "io/ioutil" 24 | "os" 25 | 26 | "github.com/packethost/csi-packet/pkg/driver" 27 | "github.com/packethost/csi-packet/pkg/packet" 28 | "github.com/packethost/csi-packet/pkg/version" 29 | log "github.com/sirupsen/logrus" 30 | "github.com/spf13/cobra" 31 | ) 32 | 33 | var ( 34 | endpoint string 35 | nodeID string 36 | providerConfig string 37 | ) 38 | 39 | const ( 40 | apiKeyName = "PACKET_API_KEY" 41 | projectIDName = "PACKET_PROJECT_ID" 42 | facilityIDName = "PACKET_FACILITY_ID" 43 | ) 44 | 45 | func init() { 46 | flag.Set("logtostderr", "true") 47 | 48 | // Log as JSON instead of the default text. 49 | log.SetFormatter(&log.JSONFormatter{}) 50 | 51 | // Output to stdout instead of the default stderr 52 | log.SetOutput(os.Stdout) 53 | 54 | log.SetLevel(log.DebugLevel) 55 | } 56 | 57 | func main() { 58 | // log our starting point 59 | log.WithFields(log.Fields{"version": version.VERSION}).Info("started") 60 | 61 | flag.CommandLine.Parse([]string{}) 62 | 63 | cmd := &cobra.Command{ 64 | Use: "Packet", 65 | Short: "CSI Packet driver", 66 | Run: func(cmd *cobra.Command, args []string) { 67 | handle() 68 | }, 69 | } 70 | 71 | cmd.Flags().AddGoFlagSet(flag.CommandLine) 72 | 73 | // optional flag to override node ID 74 | cmd.PersistentFlags().StringVar(&nodeID, "nodeid", "", "node id") 75 | 76 | cmd.PersistentFlags().StringVar(&endpoint, "endpoint", "", "CSI endpoint") 77 | cmd.MarkPersistentFlagRequired("endpoint") 78 | 79 | cmd.PersistentFlags().StringVar(&providerConfig, "config", "", "path to provider config file") 80 | 81 | cmd.ParseFlags(os.Args[1:]) 82 | if err := cmd.Execute(); err != nil { 83 | fmt.Fprintf(os.Stderr, "%s", err.Error()) 84 | os.Exit(1) 85 | } 86 | 87 | os.Exit(0) 88 | } 89 | 90 | func handle() { 91 | // create our config, as needed 92 | var config, rawConfig packet.Config 93 | if providerConfig != "" { 94 | configBytes, err := ioutil.ReadFile(providerConfig) 95 | if err != nil { 96 | fmt.Fprintf(os.Stderr, "Failed to get read configuration file at path %s: %v\n", providerConfig, err) 97 | os.Exit(1) 98 | } 99 | err = json.Unmarshal(configBytes, &rawConfig) 100 | if err != nil { 101 | fmt.Fprintf(os.Stderr, "Failed to process json of configuration file at path %s: %v\n", providerConfig, err) 102 | os.Exit(1) 103 | } 104 | } 105 | 106 | // read env vars; if not set, use rawConfig 107 | apiToken := os.Getenv(apiKeyName) 108 | if apiToken == "" { 109 | apiToken = rawConfig.AuthToken 110 | } 111 | config.AuthToken = apiToken 112 | 113 | projectID := os.Getenv(projectIDName) 114 | if projectID == "" { 115 | projectID = rawConfig.ProjectID 116 | } 117 | config.ProjectID = projectID 118 | 119 | facilityID := os.Getenv(facilityIDName) 120 | if facilityID == "" { 121 | facilityID = rawConfig.FacilityID 122 | } 123 | config.FacilityID = facilityID 124 | 125 | d, err := driver.NewPacketDriver(endpoint, nodeID, config) 126 | if err != nil { 127 | fmt.Fprintf(os.Stderr, "Failed to get packet driver: %v\n", err) 128 | os.Exit(1) 129 | } 130 | d.Run() 131 | } 132 | -------------------------------------------------------------------------------- /deploy/demo/demo-deployment.yaml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolumeClaim 2 | apiVersion: v1 3 | metadata: 4 | name: podpvc 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | storageClassName: csi-packet-standard 9 | resources: 10 | requests: 11 | storage: 100Gi 12 | 13 | --- 14 | 15 | kind: Deployment 16 | apiVersion: apps/v1 17 | metadata: 18 | labels: 19 | run: nginx 20 | name: nginx 21 | spec: 22 | replicas: 1 23 | selector: 24 | matchLabels: 25 | run: nginx 26 | strategy: 27 | rollingUpdate: 28 | maxSurge: 1 29 | maxUnavailable: 1 30 | type: RollingUpdate 31 | template: 32 | metadata: 33 | name: web-server 34 | labels: 35 | run: nginx 36 | spec: 37 | # nodeSelector: 38 | # kubernetes.io/hostname: "10.88.52.141" 39 | containers: 40 | - image: nginx 41 | name: nginx 42 | volumeMounts: 43 | - mountPath: /var/lib/www/html 44 | name: mypvc 45 | volumes: 46 | - name: mypvc 47 | persistentVolumeClaim: 48 | claimName: podpvc 49 | readOnly: false 50 | -------------------------------------------------------------------------------- /deploy/demo/demo-pod.yaml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolumeClaim 2 | apiVersion: v1 3 | metadata: 4 | name: podpvc 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | storageClassName: csi-packet-standard 9 | resources: 10 | requests: 11 | storage: 100Gi 12 | 13 | --- 14 | 15 | apiVersion: v1 16 | kind: Pod 17 | metadata: 18 | name: web-server 19 | spec: 20 | containers: 21 | - name: web-server 22 | image: nginx 23 | volumeMounts: 24 | - mountPath: /var/lib/www/html 25 | name: mypvc 26 | volumes: 27 | - name: mypvc 28 | persistentVolumeClaim: 29 | claimName: podpvc 30 | readOnly: false 31 | -------------------------------------------------------------------------------- /deploy/demo/demo-pv.yaml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolume 2 | apiVersion: v1 3 | metadata: 4 | name: pv1 5 | annotations: 6 | # indicate that this is provisioned dynamically, so the system works properly 7 | pv.kubernetes.io/provisioned-by: csi.packet.net 8 | spec: 9 | # make sure the storage class is one that is from the setup 10 | storageClassName: csi-packet-performance 11 | # by default, the volume will be not deleted if you delete the PVC, change to 12 | # "Delete" if you wish the volume to be deleted automatically with the PVC 13 | persistentVolumeReclaimPolicy: Delete 14 | capacity: 15 | storage: 100Gi 16 | accessModes: 17 | - ReadWriteOnce 18 | csi: 19 | driver: csi.packet.net 20 | fsType: ext4 21 | # You will have to add your Packet block storage volume ID for the volumeHandle 22 | # since a PersistentVolume works with _already_ provisioned storage (static) 23 | # and not automatically created (dynamic). If you want dynamic, use a 24 | # PersistentVolumeClaim (pvc), for example, see demo-pod.yaml 25 | volumeHandle: BLOCK_VOLUME_ID 26 | volumeAttributes: 27 | storage.kubernetes.io/csiProvisionerIdentity: 1572974631219-8081-csi.packet.net 28 | -------------------------------------------------------------------------------- /deploy/demo/demo-statefulset.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: stateful-nginx 6 | labels: 7 | app: stateful-nginx 8 | spec: 9 | ports: 10 | - port: 80 11 | name: web 12 | clusterIP: None 13 | selector: 14 | app: nginx 15 | --- 16 | apiVersion: apps/v1 17 | kind: StatefulSet 18 | metadata: 19 | name: stateful-nginx 20 | spec: 21 | selector: 22 | matchLabels: 23 | app: stateful-nginx 24 | serviceName: "stateful-nginx" 25 | replicas: 2 26 | template: 27 | metadata: 28 | labels: 29 | app: stateful-nginx 30 | spec: 31 | terminationGracePeriodSeconds: 10 32 | containers: 33 | - name: nginx 34 | image: k8s.gcr.io/nginx-slim:0.8 35 | ports: 36 | - containerPort: 80 37 | name: web 38 | volumeMounts: 39 | - name: www 40 | mountPath: /usr/share/nginx/html 41 | volumeClaimTemplates: 42 | - metadata: 43 | name: www 44 | spec: 45 | accessModes: [ "ReadWriteOnce" ] 46 | resources: 47 | requests: 48 | storage: 100Gi 49 | -------------------------------------------------------------------------------- /deploy/kubernetes/controller.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: csi-packet-pd 5 | labels: 6 | app: csi-packet-pd 7 | spec: 8 | selector: 9 | app: csi-packet-pd 10 | ports: 11 | - name: dummy 12 | port: 12345 13 | 14 | --- 15 | 16 | kind: StatefulSet 17 | apiVersion: apps/v1 18 | metadata: 19 | name: csi-packet-controller 20 | namespace: kube-system 21 | spec: 22 | updateStrategy: 23 | type: RollingUpdate 24 | rollingUpdate: 25 | partition: 0 26 | serviceName: "csi-packet-pd" 27 | replicas: 1 28 | selector: 29 | matchLabels: 30 | app: csi-packet-pd-driver 31 | template: 32 | metadata: 33 | labels: 34 | app: csi-packet-pd-driver 35 | spec: 36 | serviceAccount: csi-controller-sa 37 | containers: 38 | - name: csi-external-provisioner 39 | imagePullPolicy: IfNotPresent 40 | image: quay.io/k8scsi/csi-provisioner:v1.0.1 41 | args: 42 | - "--v=5" 43 | - "--provisioner=csi.packet.net" 44 | - "--csi-address=$(ADDRESS)" 45 | env: 46 | - name: ADDRESS 47 | value: /csi/csi.sock 48 | volumeMounts: 49 | - name: socket-dir 50 | mountPath: /csi 51 | - name: csi-attacher 52 | imagePullPolicy: IfNotPresent 53 | image: quay.io/k8scsi/csi-attacher:v1.0.1 54 | args: 55 | - "--v=5" 56 | - "--csi-address=$(ADDRESS)" 57 | env: 58 | - name: ADDRESS 59 | value: /csi/csi.sock 60 | volumeMounts: 61 | - name: socket-dir 62 | mountPath: /csi 63 | - name: packet-driver 64 | imagePullPolicy: Always 65 | image: docker.io/packethost/csi-packet:v1.1.0 66 | args: 67 | - "--endpoint=$(CSI_ENDPOINT)" 68 | - "--config=/etc/cloud-sa/cloud-sa.json" 69 | env: 70 | - name: CSI_ENDPOINT 71 | value: unix:///csi/csi.sock 72 | volumeMounts: 73 | - name: socket-dir 74 | mountPath: /csi 75 | - name: cloud-sa-volume 76 | readOnly: true 77 | mountPath: "/etc/cloud-sa" 78 | volumes: 79 | - name: socket-dir 80 | emptyDir: {} 81 | - name: cloud-sa-volume 82 | secret: 83 | secretName: packet-cloud-config 84 | -------------------------------------------------------------------------------- /deploy/kubernetes/node.yaml: -------------------------------------------------------------------------------- 1 | kind: DaemonSet 2 | apiVersion: apps/v1 3 | metadata: 4 | name: csi-node 5 | namespace: kube-system 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: csi-packet-driver 10 | template: 11 | metadata: 12 | labels: 13 | app: csi-packet-driver 14 | spec: 15 | serviceAccount: csi-node-sa 16 | hostNetwork: true 17 | containers: 18 | - name: csi-driver-registrar 19 | imagePullPolicy: IfNotPresent 20 | image: quay.io/k8scsi/csi-node-driver-registrar:v1.0.1 21 | args: 22 | - "--v=5" 23 | - "--csi-address=$(ADDRESS)" 24 | - "--kubelet-registration-path=/var/lib/kubelet/plugins/csi.packet.net/csi.sock" 25 | env: 26 | - name: ADDRESS 27 | value: /csi/csi.sock 28 | - name: KUBE_NODE_NAME 29 | valueFrom: 30 | fieldRef: 31 | fieldPath: spec.nodeName 32 | volumeMounts: 33 | - name: plugin-dir 34 | mountPath: /csi 35 | - name: registration-dir 36 | mountPath: /registration 37 | # - name: registrar-socket-dir 38 | # mountPath: /var/lib/csi/sockets/ 39 | - name: packet-driver 40 | securityContext: 41 | privileged: true 42 | imagePullPolicy: Always 43 | image: docker.io/packethost/csi-packet:v1.1.0 44 | args: 45 | - "--endpoint=$(CSI_ENDPOINT)" 46 | env: 47 | - name: CSI_ENDPOINT 48 | value: unix:///csi/csi.sock 49 | volumeMounts: 50 | - name: kubelet-dir 51 | mountPath: /var/lib/kubelet/pods 52 | mountPropagation: "Bidirectional" 53 | - name: iscsiadm 54 | mountPath: /sbin/iscsiadm 55 | - name: all-plugin-dir 56 | mountPath: /var/lib/kubelet/plugins 57 | - name: plugin-dir 58 | mountPath: /csi 59 | - name: sys-devices 60 | mountPath: /sys/devices 61 | - mountPath: /dev 62 | name: dev 63 | - mountPath: /etc 64 | name: etc 65 | - mountPath: /run/udev 66 | name: run-udev 67 | - mountPath: /var/lib/iscsi 68 | name: var-lib-iscsi 69 | - name: lib-modules 70 | mountPath: /lib/modules 71 | - mountPath: /usr/share/ca-certificates/ 72 | name: ca-certs-alternative 73 | readOnly: true 74 | volumes: 75 | # TODO(dependency): this will work when kublet registrar functionality exists 76 | #- name: registrar-socket-dir 77 | # hostPath: 78 | # path: /var/lib/kubelet/device-plugins/ 79 | # type: DirectoryOrCreate 80 | - name: registration-dir 81 | hostPath: 82 | path: /var/lib/kubelet/plugins_registry 83 | type: Directory 84 | - name: kubelet-dir 85 | hostPath: 86 | path: /var/lib/kubelet/pods 87 | type: Directory 88 | - name: all-plugin-dir 89 | hostPath: 90 | path: /var/lib/kubelet/plugins 91 | type: DirectoryOrCreate 92 | - name: plugin-dir 93 | hostPath: 94 | path: /var/lib/kubelet/plugins/csi.packet.net/ 95 | type: DirectoryOrCreate 96 | - name: iscsiadm 97 | hostPath: 98 | path: /sbin/iscsiadm 99 | type: File 100 | - name: dev 101 | hostPath: 102 | path: /dev 103 | type: Directory 104 | - name: etc 105 | hostPath: 106 | path: /etc/ 107 | - name: var-lib-iscsi 108 | hostPath: 109 | path: /var/lib/iscsi/ 110 | type: DirectoryOrCreate 111 | - name: sys-devices 112 | hostPath: 113 | path: /sys/devices 114 | type: Directory 115 | - name: run-udev 116 | hostPath: 117 | path: /run/udev/ 118 | type: Directory 119 | - name: lib-modules 120 | hostPath: 121 | path: /lib/modules 122 | type: Directory 123 | - name: ca-certs-alternative 124 | hostPath: 125 | path: /usr/share/ca-certificates/ 126 | type: DirectoryOrCreate 127 | -------------------------------------------------------------------------------- /deploy/kubernetes/node_controller_sanity_test.yaml: -------------------------------------------------------------------------------- 1 | kind: DaemonSet 2 | apiVersion: apps/v1 3 | metadata: 4 | name: csi-node 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: csi-packet-driver 9 | template: 10 | metadata: 11 | labels: 12 | app: csi-packet-driver 13 | spec: 14 | serviceAccount: csi-node-sa 15 | hostNetwork: true 16 | containers: 17 | - name: csi-driver-registrar 18 | imagePullPolicy: Always 19 | image: quay.io/k8scsi/driver-registrar:v0.2.0 20 | args: 21 | - "--v=5" 22 | - "--csi-address=$(ADDRESS)" 23 | env: 24 | - name: ADDRESS 25 | value: /csi/csi.sock 26 | - name: KUBE_NODE_NAME 27 | valueFrom: 28 | fieldRef: 29 | fieldPath: spec.nodeName 30 | volumeMounts: 31 | - name: plugin-dir 32 | mountPath: /csi 33 | # - name: registrar-socket-dir 34 | # mountPath: /var/lib/csi/sockets/ 35 | - name: packet-driver 36 | securityContext: 37 | privileged: true 38 | imagePullPolicy: Always 39 | image: docker.io/packethost/csi-packet:v1.1.0 40 | args: 41 | - "--endpoint=$(CSI_ENDPOINT)" 42 | - "--config=/etc/cloud-sa/cloud-sa.json" 43 | env: 44 | - name: CSI_ENDPOINT 45 | value: unix:///csi/csi.sock 46 | volumeMounts: 47 | - name: cloud-sa-volume 48 | readOnly: true 49 | mountPath: "/etc/cloud-sa" 50 | - name: kubelet-dir 51 | mountPath: /var/lib/kubelet 52 | mountPropagation: "Bidirectional" 53 | - name: iscsiadm 54 | mountPath: /sbin/iscsiadm 55 | - name: plugin-dir 56 | mountPath: /csi 57 | - name: sys-devices 58 | mountPath: /sys/devices 59 | - mountPath: /dev 60 | name: dev 61 | - mountPath: /etc 62 | name: etc 63 | - mountPath: /run/udev 64 | name: run-udev 65 | - mountPath: /var/lib/iscsi 66 | name: var-lib-iscsi 67 | - name: lib-modules 68 | mountPath: /lib/modules 69 | - mountPath: /usr/share/ca-certificates/ 70 | name: ca-certs-alternative 71 | readOnly: true 72 | volumes: 73 | # TODO(dependency): this will work when kublet registrar functionality exists 74 | #- name: registrar-socket-dir 75 | # hostPath: 76 | # path: /var/lib/kubelet/device-plugins/ 77 | # type: DirectoryOrCreate 78 | - name: cloud-sa-volume 79 | secret: 80 | secretName: packet-cloud-config 81 | - name: kubelet-dir 82 | hostPath: 83 | path: /var/lib/kubelet 84 | type: Directory 85 | - name: plugin-dir 86 | hostPath: 87 | path: /var/lib/kubelet/plugins/csi.packet.net/ 88 | type: DirectoryOrCreate 89 | - name: iscsiadm 90 | hostPath: 91 | path: /sbin/iscsiadm 92 | type: File 93 | - name: dev 94 | hostPath: 95 | path: /dev 96 | type: Directory 97 | - name: etc 98 | hostPath: 99 | path: /etc/ 100 | - name: var-lib-iscsi 101 | hostPath: 102 | path: /var/lib/iscsi/ 103 | type: DirectoryOrCreate 104 | - name: sys-devices 105 | hostPath: 106 | path: /sys/devices 107 | type: Directory 108 | - name: run-udev 109 | hostPath: 110 | path: /run/udev/ 111 | type: Directory 112 | - name: lib-modules 113 | hostPath: 114 | path: /lib/modules 115 | type: Directory 116 | - name: ca-certs-alternative 117 | hostPath: 118 | path: /usr/share/ca-certificates/ 119 | type: DirectoryOrCreate 120 | -------------------------------------------------------------------------------- /deploy/kubernetes/setup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: storage.k8s.io/v1beta1 2 | kind: StorageClass 3 | metadata: 4 | name: csi-packet-standard 5 | annotations: 6 | storageclass.kubernetes.io/is-default-class: "true" 7 | provisioner: csi.packet.net 8 | parameters: 9 | plan: standard 10 | volumeBindingMode: Immediate 11 | 12 | --- 13 | 14 | apiVersion: storage.k8s.io/v1beta1 15 | kind: StorageClass 16 | metadata: 17 | name: csi-packet-performance 18 | provisioner: csi.packet.net 19 | parameters: 20 | plan: performance 21 | volumeBindingMode: Immediate 22 | 23 | --- 24 | 25 | kind: ClusterRole 26 | apiVersion: rbac.authorization.k8s.io/v1 27 | metadata: 28 | name: driver-registrar-role 29 | rules: 30 | - apiGroups: [""] 31 | resources: ["nodes"] 32 | verbs: ["get", "update"] 33 | - apiGroups: [""] 34 | resources: ["events"] 35 | verbs: ["list", "watch", "create", "update", "patch"] 36 | 37 | --- 38 | 39 | apiVersion: v1 40 | kind: ServiceAccount 41 | metadata: 42 | name: csi-node-sa 43 | namespace: kube-system 44 | 45 | --- 46 | 47 | kind: ClusterRoleBinding 48 | apiVersion: rbac.authorization.k8s.io/v1 49 | metadata: 50 | name: driver-registrar-binding 51 | subjects: 52 | - kind: ServiceAccount 53 | name: csi-node-sa 54 | namespace: kube-system 55 | roleRef: 56 | kind: ClusterRole 57 | name: driver-registrar-role 58 | apiGroup: rbac.authorization.k8s.io 59 | 60 | --- 61 | 62 | apiVersion: v1 63 | kind: ServiceAccount 64 | metadata: 65 | name: csi-controller-sa 66 | namespace: kube-system 67 | 68 | --- 69 | 70 | kind: ClusterRole 71 | apiVersion: rbac.authorization.k8s.io/v1 72 | metadata: 73 | name: csi-external-attacher 74 | rules: 75 | - apiGroups: [""] 76 | resources: ["persistentvolumes"] 77 | verbs: ["get", "list", "watch", "update", "patch"] 78 | - apiGroups: [""] 79 | resources: ["nodes"] 80 | verbs: ["get", "list", "watch"] 81 | - apiGroups: ["storage.k8s.io"] 82 | resources: ["volumeattachments"] 83 | verbs: ["get", "list", "watch", "update", "patch"] 84 | - apiGroups: ["storage.k8s.io"] 85 | resources: ["csinodes"] 86 | verbs: ["get", "list", "watch"] 87 | --- 88 | 89 | kind: ClusterRoleBinding 90 | apiVersion: rbac.authorization.k8s.io/v1 91 | metadata: 92 | name: csi-controller-attacher-binding 93 | subjects: 94 | - kind: ServiceAccount 95 | name: csi-controller-sa 96 | namespace: kube-system 97 | roleRef: 98 | kind: ClusterRole 99 | name: csi-external-attacher 100 | apiGroup: rbac.authorization.k8s.io 101 | 102 | --- 103 | 104 | kind: ClusterRole 105 | apiVersion: rbac.authorization.k8s.io/v1 106 | metadata: 107 | name: csi-external-provisioner 108 | rules: 109 | - apiGroups: [""] 110 | resources: ["persistentvolumes"] 111 | verbs: ["get", "list", "watch", "create", "delete"] 112 | - apiGroups: [""] 113 | resources: ["persistentvolumeclaims"] 114 | verbs: ["get", "list", "watch", "update"] 115 | - apiGroups: ["storage.k8s.io"] 116 | resources: ["storageclasses"] 117 | verbs: ["get", "list", "watch"] 118 | - apiGroups: [""] 119 | resources: ["events"] 120 | verbs: ["list", "watch", "create", "update", "patch"] 121 | - apiGroups: ["storage.k8s.io"] 122 | resources: ["csinodes"] 123 | verbs: ["get", "list", "watch"] 124 | - apiGroups: [""] 125 | resources: ["nodes"] 126 | verbs: ["get", "list", "watch"] 127 | 128 | --- 129 | 130 | kind: ClusterRoleBinding 131 | apiVersion: rbac.authorization.k8s.io/v1 132 | metadata: 133 | name: csi-controller-provisioner-binding 134 | subjects: 135 | - kind: ServiceAccount 136 | name: csi-controller-sa 137 | namespace: kube-system 138 | roleRef: 139 | kind: ClusterRole 140 | name: csi-external-provisioner 141 | apiGroup: rbac.authorization.k8s.io 142 | -------------------------------------------------------------------------------- /deploy/template/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: packet-cloud-config 5 | namespace: kube-system 6 | stringData: 7 | cloud-sa.json: | 8 | { 9 | "apiKey": "packet auth token", 10 | "projectID": "packet project id" 11 | } 12 | -------------------------------------------------------------------------------- /docs/background.md: -------------------------------------------------------------------------------- 1 | ## Background reading 2 | 3 | 4 | ### CSI vs FlexVolumes 5 | 6 | FlexVolumes are an older spec, since k8s 1.2, and will be supported in the future. Requires root access to install on each node and assumes OS-based tools are installed. "The Storage SIG suggests implementing a CSI driver if possible" 7 | 8 | ### CSI design summary 9 | 10 | Kubernetes will introduce a new in-tree volume plugin called CSI. 11 | 12 | This plugin, in kubelet, will make volume mount and unmount rpcs to a unix domain socket on the host machine. The driver component responds to these requests in a specialized way. (https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/container-storage-interface.md#kubelet-to-csi-driver-communication) 13 | 14 | 15 | Lifecycle management of volume is done by the controller-manager (https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/container-storage-interface.md#master-to-csi-driver-communication) and communication is mediated through the api-server, which requires that the external component watch the k8s api for changes. The design document suggests a sidecar “Kubernetes to CSI” proxy. 16 | 17 | The concern here is that 18 | - communication to the diver is done through a local unix domain socket 19 | - the driver is untrusted and cannot be allowed to run on the master node 20 | - the controller manager runs on the master node 21 | - the driver doesn't have any kubernetes-awareness, doesn't have k8s client code or how to watch the api serer 22 | 23 | This section: https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/container-storage-interface.md#recommended-mechanism-for-deploying-csi-drivers-on-kubernetes 24 | shows the recommended deployment which puts the driver in a container inside a pod, sharing it with a k8s-aware container, with communication between those two via a unix domain socket "in the pod" 25 | 26 | 27 | 28 | ### References 29 | 30 | Packet API: 31 | * https://www.packet.net/developers/api/volumes/ 32 | * https://github.com/packethost/packngo 33 | * https://github.com/ebsarr/packet 34 | * https://github.com/packethost/packet-block-storage/ 35 | 36 | 37 | packet-flex-volume: 38 | * https://github.com/karlbunch/packet-k8s-flexvolume/blob/master/flexvolume/packet/plugin.py#L463 39 | 40 | * create: https://github.com/karlbunch/packet-k8s-flexvolume/blob/master/flexvolume/packet/plugin.py#L350 41 | * attach: 42 | * https://github.com/karlbunch/packet-k8s-flexvolume/blob/master/flexvolume/packet/plugin.py#L497 43 | * https://github.com/packethost/packet-python/blob/master/packet/Volume.py#L38 44 | * iscsi, multipath: https://github.com/karlbunch/packet-k8s-flexvolume/blob/master/flexvolume/packet/plugin.py#L515 45 | * mount: https://github.com/karlbunch/packet-k8s-flexvolume/blob/master/flexvolume/packet/plugin.py#L544 46 | 47 | iscsi: 48 | * https://coreos.com/os/docs/latest/iscsi.html 49 | * https://eucalyptus.atlassian.net/wiki/spaces/STOR/pages/84312154/iscsiadm+basics 50 | * https://linux.die.net/man/8/multipath 51 | * https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/dm_multipath/ 52 | 53 | mount: 54 | * https://coreos.com/os/docs/latest/mounting-storage.html 55 | * https://oguya.ch/posts/2015-09-01-systemd-mount-partition/ 56 | * ? https://github.com/coreos/bugs/issues/2254 57 | * ? https://github.com/kubernetes/kubernetes/issues/59946#issuecomment-380401916 58 | * ? https://github.com/kubernetes/kubernetes/pull/63176 59 | 60 | CSI design 61 | * https://github.com/container-storage-interface/spec/blob/master/spec.md#rpc-interface 62 | 63 | CSI examples 64 | * https://github.com/kubernetes-csi/drivers 65 | * https://github.com/libopenstorage/openstorage/tree/master/csi 66 | * https://github.com/thecodeteam/csi-vsphere 67 | * https://github.com/openebs/csi-openebs/ 68 | * https://github.com/digitalocean/csi-digitalocean 69 | * https://github.com/kubernetes-sigs/gcp-compute-persistent-disk-csi-driver 70 | * https://github.com/kubernetes-sigs/gcp-compute-persistent-disk-csi-driver/blob/master/deploy/kubernetes/README.md 71 | 72 | grpc server 73 | 74 | * https://github.com/GoogleCloudPlatform/compute-persistent-disk-csi-driver/blob/6702720a9de93b57d73fa8912ef04ce6327a00e3/pkg/gce-csi-driver/server.go 75 | * https://github.com/digitalocean/csi-digitalocean/blob/783dcec9b26da4ee9c36b6472e180ebb904c465d/driver/driver.go 76 | * https://dev.to/chilladx/how-we-use-grpc-to-build-a-clientserver-system-in-go-1mi 77 | 78 | 79 | Documentation 80 | * https://kubernetes.io/blog/2018/04/10/container-storage-interface-beta/ 81 | * https://github.com/kubernetes/community/blob/master/contributors/design-proposals/resource-management/device-plugin.md#unix-socket 82 | * https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/container-storage-interface.md 83 | * https://github.com/kubernetes/community/blob/master/sig-storage/volume-plugin-faq.md 84 | * https://github.com/kubernetes/community/blob/master/sig-storage/volume-plugin-faq.md#working-with-out-of-tree-volume-plugin-options 85 | * https://kubernetes-csi.github.io/docs/Drivers.html 86 | * https://kubernetes.io/blog/2018/01/introducing-container-storage-interface/ 87 | * https://kubernetes.io/docs/concepts/storage/volumes/ 88 | * https://github.com/container-storage-interface/spec/blob/master/spec.md#rpc-interface 89 | 90 | grpc 91 | * https://grpc.io/docs/quickstart/go.html 92 | * https://github.com/golang/protobuf 93 | * https://grpc.io/docs/tutorials/basic/go.html 94 | * https://developers.google.com/protocol-buffers/docs/proto3 95 | 96 | 97 | protobuf spec 98 | * https://github.com/container-storage-interface/spec 99 | 100 | 101 | ### Credentials 102 | 103 | https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/container-storage-interface.md#csi-credentials 104 | 105 | 106 | ### Deployment Helpers 107 | 108 | * external-attacher https://github.com/kubernetes-csi/external-attacher 109 | * external-provisioner https://github.com/kubernetes-csi/external-provisioner 110 | * driver-registrar https://github.com/kubernetes-csi/driver-registrar 111 | * liveness-probe https://github.com/kubernetes-csi/livenessprobe 112 | 113 | ### Mounting 114 | 115 | Mounting a filesystem is an os task, not cloud provider. 116 | 117 | DO, GCE create a mounter type 118 | * https://github.com/digitalocean/csi-digitalocean/blob/master/driver/mounter.go 119 | * https://github.com/GoogleCloudPlatform/compute-persistent-disk-csi-driver/blob/master/pkg/mount-manager/mounter.go 120 | VSphere calls out to a separate library 121 | * https://github.com/akutz/gofsutil 122 | why not use [sys](https://godoc.org/golang.org/x/sys/unix#Mount)? Well, it seems we need to exec out in order to call mkfs anyway 123 | 124 | * https://github.com/thecodeteam/csi-vsphere/blob/master/service/node.go 125 | 126 | 127 | on our coreos installs, 128 | * iscsid 129 | * multipathd 130 | are present but not running 131 | 132 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/packethost/csi-packet 2 | 3 | go 1.12 4 | 5 | //replace github.com/packethost/packet-api-server => /Users/adeitcher/Documents/Development/go/src/github.com/packethost/packet-api-server 6 | 7 | require ( 8 | github.com/container-storage-interface/spec v1.1.0 9 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect 10 | github.com/golang/mock v1.1.1 11 | github.com/google/uuid v1.1.1 12 | github.com/gorilla/mux v1.7.3 13 | github.com/kubernetes-csi/csi-test v2.2.0+incompatible 14 | github.com/onsi/ginkgo v1.10.1 // indirect 15 | github.com/onsi/gomega v1.7.0 // indirect 16 | github.com/packethost/packet-api-server v0.0.0-20191210180413-86f9ff63b495 17 | github.com/packethost/packngo v0.2.1-0.20191003144416-9f81c97413a3 18 | github.com/pkg/errors v0.8.0 19 | github.com/sirupsen/logrus v1.0.5 20 | github.com/spf13/cobra v0.0.2 21 | github.com/spf13/pflag v1.0.5 // indirect 22 | github.com/stretchr/objx v0.2.0 // indirect 23 | github.com/stretchr/testify v1.4.0 24 | golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf // indirect 25 | golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 26 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect 27 | golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c 28 | golang.org/x/text v0.3.2 // indirect 29 | golang.org/x/tools v0.0.0-20191030062658-86caa796c7ab // indirect 30 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect 31 | google.golang.org/genproto v0.0.0-20180427144745-86e600f69ee4 // indirect 32 | google.golang.org/grpc v1.12.0 33 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 34 | gopkg.in/yaml.v2 v2.2.4 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2 h1:HTOmFEEYrWi4MW5ZKUx6xfeyM10Sx3kQF65xiQJMPYA= 4 | github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= 5 | github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= 6 | github.com/container-storage-interface/spec v1.0.0 h1:3DyXuJgf9MU6kyULESegQUmozsSxhpyrrv9u5bfwA3E= 7 | github.com/container-storage-interface/spec v1.0.0/go.mod h1:6URME8mwIBbpVyZV93Ce5St17xBiQJQY67NDsuohiy4= 8 | github.com/container-storage-interface/spec v1.1.0 h1:qPsTqtR1VUPvMPeK0UnCZMtXaKGyyLPG8gj/wG6VqMs= 9 | github.com/container-storage-interface/spec v1.1.0/go.mod h1:6URME8mwIBbpVyZV93Ce5St17xBiQJQY67NDsuohiy4= 10 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU= 15 | github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 16 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 17 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 18 | github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540 h1:7CU1IXBpPvxpQ/NqJrpuMXMHAw+FB2vfqtRF8tgW9fw= 19 | github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA= 20 | github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0= 21 | github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= 22 | github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= 23 | github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g= 24 | github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= 25 | github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8= 26 | github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= 27 | github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= 28 | github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ= 29 | github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= 30 | github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= 31 | github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k= 32 | github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= 33 | github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= 34 | github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= 35 | github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg= 36 | github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= 37 | github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= 38 | github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= 39 | github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4= 40 | github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= 41 | github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA= 42 | github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= 43 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 44 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 45 | github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= 46 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 47 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 48 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 49 | github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 50 | github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= 51 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 52 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 53 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 54 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 55 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 56 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 57 | github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= 58 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 59 | github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw= 60 | github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= 61 | github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= 62 | github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= 63 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 64 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 65 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 66 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 67 | github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM= 68 | github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= 69 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 70 | github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 71 | github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 72 | github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 73 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 74 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 75 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 76 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 77 | github.com/kubernetes-csi/csi-test v2.2.0+incompatible h1:ksIV60Q+4mY0Fg8LKvBssjEcvbyxo7nz0eAD6ZLMux0= 78 | github.com/kubernetes-csi/csi-test v2.2.0+incompatible/go.mod h1:YxJ4UiuPWIhMBkxUKY5c267DyA0uDZ/MtAimhx/2TA0= 79 | github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 80 | github.com/magiconair/properties v1.7.6 h1:U+1DqNen04MdEPgFiIwdOUiqZ8qPa37xgogX/sd3+54= 81 | github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 82 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 83 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 84 | github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= 85 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 86 | github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= 87 | github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= 88 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 89 | github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= 90 | github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E= 91 | github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 92 | github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= 93 | github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= 94 | github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw= 95 | github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= 96 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 97 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 98 | github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= 99 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 100 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 101 | github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 102 | github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= 103 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 104 | github.com/packethost/packet-api-server v0.0.0-20191111173258-323bdfe6671d h1:h2FMDNO2YtC+ZDTj9RKH07osLrJ6FA9oCojdv4dmLN4= 105 | github.com/packethost/packet-api-server v0.0.0-20191111173258-323bdfe6671d/go.mod h1:yTi5RK7+tGw57bN7RMYD9olsImqNffYSTA+Acwlrd+4= 106 | github.com/packethost/packet-api-server v0.0.0-20191210180413-86f9ff63b495 h1:u9C1RHz7koww8osqIMoPG1iL6l2qaM3nbn+blIbXDZI= 107 | github.com/packethost/packet-api-server v0.0.0-20191210180413-86f9ff63b495/go.mod h1:yTi5RK7+tGw57bN7RMYD9olsImqNffYSTA+Acwlrd+4= 108 | github.com/packethost/packngo v0.0.0-20180509174913-269c1b559fe7 h1:NEtQ7LnJkbnOEZK6tLTVYJBK1nPjOgi7tlIMMFAc314= 109 | github.com/packethost/packngo v0.0.0-20180509174913-269c1b559fe7/go.mod h1:otzZQXgoO96RTzDB/Hycg0qZcXZsWJGJRSXbmEIJ+4M= 110 | github.com/packethost/packngo v0.2.0/go.mod h1:RQHg5xR1F614BwJyepfMqrKN+32IH0i7yX+ey43rEeQ= 111 | github.com/packethost/packngo v0.2.1-0.20191003144416-9f81c97413a3 h1:vwy7SOzlUTJ11gXkLikGxBV4hUWh39wprc7Q+6FVLIA= 112 | github.com/packethost/packngo v0.2.1-0.20191003144416-9f81c97413a3/go.mod h1:RQHg5xR1F614BwJyepfMqrKN+32IH0i7yX+ey43rEeQ= 113 | github.com/pelletier/go-toml v1.1.0 h1:cmiOvKzEunMsAxyhXSzpL5Q1CRKpVv0KQsnAIcSEVYM= 114 | github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 115 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 116 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 117 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 118 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 119 | github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= 120 | github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 121 | github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 122 | github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 123 | github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= 124 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 125 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 126 | github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I= 127 | github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= 128 | github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs= 129 | github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= 130 | github.com/spf13/afero v1.1.0 h1:bopulORc2JeYaxfHLvJa5NzxviA9PoWhpiiJkru7Ji4= 131 | github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 132 | github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg= 133 | github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= 134 | github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE= 135 | github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 136 | github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig= 137 | github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 138 | github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= 139 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 140 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 141 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 142 | github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso= 143 | github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= 144 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 145 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 146 | github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U= 147 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 148 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 149 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 150 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 151 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 152 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 153 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 154 | github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb h1:lI9ufgFfvuqRctP9Ny8lDDLbSWCMxBPletcSqrnyFYM= 155 | github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= 156 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 157 | github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= 158 | github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= 159 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 160 | golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8 h1:h7zdf0RiEvWbYBKIx4b+q41xoUVnMmvsGZnIVE5syG8= 161 | golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 162 | golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 163 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 164 | golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= 165 | golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 166 | golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf h1:fnPsqIDRbCSgumaMCRpoIoF2s4qxv0xSSS0BVZUE/ss= 167 | golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 168 | golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 169 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= 170 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 171 | golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 172 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 173 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I= 174 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 175 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 176 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 177 | golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 h1:N66aaryRB3Ax92gH0v3hp1QYZ3zWWCCUR/j8Ifh45Ss= 178 | golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 179 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 180 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 181 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 182 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 183 | golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 184 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= 185 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 186 | golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 187 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 188 | golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc= 189 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 190 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 191 | golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c h1:S/FtSvpNLtFBgjTqcKsRpsa6aVsI6iztaz1bQd9BJwE= 192 | golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 193 | golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 194 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 195 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 196 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 197 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 198 | golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 199 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 200 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 201 | golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 202 | golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 203 | golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 204 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 205 | golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 206 | golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 207 | golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd h1:7E3PabyysDSEjnaANKBgums/hyvMI/HoHQ50qZEzTrg= 208 | golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 209 | golang.org/x/tools v0.0.0-20191030062658-86caa796c7ab/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 210 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 211 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 212 | google.golang.org/genproto v0.0.0-20180427144745-86e600f69ee4 h1:0rk3/gV3HbvCeUzVMhdxV3TEVKMVPDnayjN7sYRmcxY= 213 | google.golang.org/genproto v0.0.0-20180427144745-86e600f69ee4/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 214 | google.golang.org/grpc v1.12.0 h1:Mm8atZtkT+P6R43n/dqNDWkPPu5BwRVu/1rJnJCeZH8= 215 | google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 216 | gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= 217 | gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= 218 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 219 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 220 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 221 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 222 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 223 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 224 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 225 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= 226 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= 227 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 228 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 229 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 230 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 231 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 232 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 233 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 234 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 235 | mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= 236 | mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= 237 | mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= 238 | mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= 239 | mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 h1:B1LAOfRqg2QUyCdzfjf46quTSYUTAK5OCwbh6pljHbM= 240 | mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY= 241 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c= 242 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 243 | -------------------------------------------------------------------------------- /isns-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # isns build command changes slightly based on architecture, so this script helps run it 4 | arch=$(uname -m) 5 | 6 | case ${arch} in 7 | x86_64|amd64) 8 | ;; 9 | aarch64|arm64) 10 | cp $(ls -1tRd /usr/share/automake-* | head -1)/config.* aclocal/ 11 | ;; 12 | *) 13 | echo "Unknown arch ${arch}" 14 | exit 1 15 | esac 16 | -------------------------------------------------------------------------------- /packet-scripts/create_and_attach.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "$1" == "" ]] 4 | then 5 | echo "please supply a hostname" 6 | exit 1 7 | fi 8 | DEVICE_NAME="$1" 9 | 10 | if [[ "$PACKET_TOKEN" == "" ]] 11 | then 12 | echo "PACKET_TOKEN is not set" 13 | exit 1 14 | fi 15 | 16 | PROJECT_ID="93125c2a-8b78-4d4f-a3c4-7367d6b7cca8" 17 | STORAGE_PLAN="87728148-3155-4992-a730-8d1e6aca8a32" 18 | FACILITY_ID="2b70eb8f-fa18-47c0-aba7-222a842362fd" 19 | 20 | DEVICE_ID=$(curl -s -X GET \ 21 | -H "Content-Type: application/json" \ 22 | -H "X-Auth-Token: $PACKET_TOKEN" \ 23 | https://api.packet.net/projects/${PROJECT_ID}/devices \ 24 | | jq -r ".devices[] | select(.hostname==\"$DEVICE_NAME\") | .id") 25 | 26 | if [[ "$DEVICE_ID" == "" ]] 27 | then 28 | echo "DEVICE_ID not found for $DEVICE_NAME" 29 | exit 1 30 | fi 31 | echo "DEVICE_ID=$DEVICE_ID" 32 | 33 | LOCAL_VOLUME_ID=$(dd if=/dev/urandom bs=128 count=1 2>/dev/null | base64 | tr -d "=+/" | dd bs=32 count=1 2>/dev/null) 34 | echo "LOCAL_VOLUME_ID=$LOCAL_VOLUME_ID" 35 | 36 | cat < scratch.json 37 | { 38 | "description": "$LOCAL_VOLUME_ID", 39 | "facility_id": "$FACILITY_ID", 40 | "plan_id": "$STORAGE_PLAN", 41 | "size": "100", 42 | "locked": "false", 43 | "billing_cycle": "hourly" 44 | } 45 | EOF 46 | 47 | curl -g -X POST -H "Content-Type: application/json" \ 48 | -d @scratch.json -H "X-Auth-Token: $PACKET_TOKEN" \ 49 | https://api.packet.net/projects/${PROJECT_ID}/storage 2>/dev/null 50 | 51 | echo 52 | 53 | VOLUME_ID=$(curl -s -H "X-Auth-Token: $PACKET_TOKEN" \ 54 | https://api.packet.net/projects/$PROJECT_ID/storage/ \ 55 | | jq -r ".volumes[] | select(.description==\"$LOCAL_VOLUME_ID\") | .id") 56 | 57 | if [[ "$VOLUME_ID" == "" ]] 58 | then 59 | echo "VOLUME_ID not found for $UUID" 60 | exit 1 61 | fi 62 | echo "VOLUME_ID=$VOLUME_ID" 63 | 64 | 65 | 66 | cat < scratch.json 67 | { 68 | "device_id": "$DEVICE_ID" 69 | } 70 | EOF 71 | 72 | curl -g -X POST -d @scratch.json \ 73 | -H "Content-Type: application/json" \ 74 | -H "X-Auth-Token: $PACKET_TOKEN" \ 75 | https://api.packet.net/storage/${VOLUME_ID}/attachments 76 | 77 | 78 | # POST https://api.packet.net/storage/102c28c7-47cf-4dfa-82e6-5eb5ae023514/attachments -------------------------------------------------------------------------------- /packet-scripts/detach_and_delete.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [[ "$1" == "" ]] 5 | then 6 | echo "please supply a hostname" 7 | exit 1 8 | fi 9 | DEVICE_NAME="$1" 10 | 11 | if [[ "$PACKET_TOKEN" == "" ]] 12 | then 13 | echo "PACKET_TOKEN is not set" 14 | exit 1 15 | fi 16 | 17 | PROJECT_ID="93125c2a-8b78-4d4f-a3c4-7367d6b7cca8" 18 | STORAGE_PLAN="87728148-3155-4992-a730-8d1e6aca8a32" 19 | FACILITY_ID="2b70eb8f-fa18-47c0-aba7-222a842362fd" 20 | 21 | DEVICE_ID=$(curl -s -X GET \ 22 | -H "Content-Type: application/json" \ 23 | -H "X-Auth-Token: $PACKET_TOKEN" \ 24 | https://api.packet.net/projects/${PROJECT_ID}/devices \ 25 | | jq -r ".devices[] | select(.hostname==\"$DEVICE_NAME\") | .id") 26 | 27 | if [[ "$DEVICE_ID" == "" ]] 28 | then 29 | echo "DEVICE_ID not found for $DEVICE_NAME" 30 | exit 1 31 | fi 32 | echo "DEVICE_ID=$DEVICE_ID" 33 | 34 | 35 | VOLUME_HREF=$(curl -s \ 36 | -H "X-Auth-Token: $PACKET_TOKEN" \ 37 | https://api.packet.net/devices/$DEVICE_ID \ 38 | | jq -r .volumes[0].href ) 39 | 40 | if [[ "$VOLUME_HREF" == "" ]] 41 | then 42 | echo "No volume found for device $DEVICE_NAME $DEVICE_ID" 43 | exit 1 44 | fi 45 | 46 | VOLUME_ID=$(echo $VOLUME_HREF | sed -e 's/.*\///') 47 | 48 | ATTACHMENT_ID=$(curl -s -X GET \ 49 | -H "Content-Type: application/json" \ 50 | -H "X-Auth-Token: $PACKET_TOKEN" \ 51 | https://api.packet.net/storage/$VOLUME_ID/attachments \ 52 | | jq -r ".attachments[] | select(.device.href==\"/devices/${DEVICE_ID}\") | select(.volume.href==\"${VOLUME_HREF}\") | .id ") 53 | 54 | if [[ "$ATTACHMENT_ID" == "" ]] 55 | then 56 | echo "No attachment found for device $DEVICE_ID and volume reference ${VOLUME_HREF}" 57 | exit 1 58 | fi 59 | echo "ATTACHMENT_ID=$ATTACHMENT_ID" 60 | 61 | # Delete them 62 | 63 | RESULT=$(curl -s -X DELETE -H "X-Auth-Token: $PACKET_TOKEN" https://api.packet.net/storage/attachments/$ATTACHMENT_ID) 64 | if [[ "$RESULT" != "" ]] 65 | then 66 | echo "errr detaching, retry is possible, $RESULT" 67 | exit 1 68 | fi 69 | 70 | RESULT=$(curl -s -X DELETE -s -H "X-Auth-Token: $PACKET_TOKEN" https://api.packet.net/storage/$VOLUME_ID) 71 | if [[ "$RESULT" != "" ]] 72 | then 73 | echo "error deleting volume, $RESULT" 74 | exit 1 75 | fi 76 | 77 | 78 | -------------------------------------------------------------------------------- /pkg/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equinixmetal-archive/csi-packet/ef1883b52ac91e8442fa64122f241c5a8d24c48d/pkg/.DS_Store -------------------------------------------------------------------------------- /pkg/driver/attacher.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | "time" 13 | 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | const ( 18 | multipathTimeout = 10 * time.Second 19 | multipathExec = "/sbin/multipath" 20 | multipathBindings = "/etc/multipath/bindings" 21 | iscsiIface = "kubernetescsi0" 22 | ) 23 | 24 | // IscsiAdm interface provides methods of executing iscsi admin commands 25 | type Attacher interface { 26 | // these interact with iscsiadm and the iscsi target 27 | 28 | // Discover also sets up the iface, so it requires the initiator 29 | Discover(ip, initiator string) error 30 | HasSession(ip, targe string) (bool, error) 31 | Login(ip, target string) error 32 | Logout(ip, target string) error 33 | // these check locally on the local host 34 | GetScsiID(devicePath string) (string, error) 35 | GetDevice(portal, target string) (string, error) 36 | // these do multipath 37 | MultipathReadBindings() (map[string]string, map[string]string, error) 38 | MultipathWriteBindings(map[string]string) error 39 | } 40 | 41 | type AttacherImpl struct { 42 | } 43 | 44 | func (i *AttacherImpl) GetScsiID(devicePath string) (string, error) { 45 | args := []string{"-g", "-u", "-d", devicePath} 46 | out, err := execCommand("/lib/udev/scsi_id", args...) 47 | if err != nil { 48 | return "", err 49 | } 50 | return string(out), nil 51 | } 52 | 53 | // look for file that matches portal, target, look up what it links to 54 | func (i *AttacherImpl) GetDevice(portal, target string) (string, error) { 55 | 56 | pattern := fmt.Sprintf("%s*%s*%s*", "/dev/disk/by-path/", portal, target) 57 | 58 | files, err := filepath.Glob(pattern) 59 | if err != nil { 60 | return "", err 61 | } 62 | if len(files) == 0 { 63 | return "", fmt.Errorf("file not found for pattern %s", pattern) 64 | } 65 | 66 | file := files[0] 67 | finfo, err := os.Lstat(file) 68 | if err != nil { 69 | return "", err 70 | } 71 | if finfo.Mode()&os.ModeSymlink == 0 { 72 | return "", fmt.Errorf("file %s is not a link", file) 73 | } 74 | source, err := filepath.EvalSymlinks(file) 75 | if err != nil { 76 | log.Errorf("cannot get symlink for %s", file) 77 | return "", err 78 | } 79 | return source, nil 80 | } 81 | 82 | func (i *AttacherImpl) Discover(ip, initiator string) error { 83 | // does the desired iface exist? 84 | args := []string{"--mode", "iface", "-o", "show"} 85 | out, err := execCommand("iscsiadm", args...) 86 | // if an error was returned, we cannot do much 87 | if err != nil { 88 | return fmt.Errorf("unable to list all ifaces: %v", err) 89 | } 90 | // parse through them to find the one we want 91 | pat, err := regexp.Compile(`(?m)^` + iscsiIface + ` .*`) 92 | if err != nil { 93 | return fmt.Errorf("error compiling pattern: %v", err) 94 | } 95 | found := pat.FindString(string(out)) 96 | // if the iface does not exist, we must create it 97 | if found == "" { 98 | args := []string{"-I", iscsiIface, "--mode", "iface", "-o", "new"} 99 | _, err := execCommand("iscsiadm", args...) 100 | if err != nil { 101 | return fmt.Errorf("unable to create new iscsi iface %s: %v", iscsiIface, err) 102 | } 103 | // get the configs for the default, and then clone them, while overriding the initiator name 104 | args = []string{"-I", "default", "--mode", "iface", "-o", "show"} 105 | out, err := execCommand("iscsiadm", args...) 106 | if err != nil { 107 | return fmt.Errorf("unable to get parameters for default iface: %v", err) 108 | } 109 | params, err := parseIscsiIfaceShow(string(out)) 110 | if err != nil { 111 | return fmt.Errorf("unable to parse parameters for default iface: %v", err) 112 | } 113 | params["iface.initiatorname"] = initiator 114 | // update new iface records 115 | for key, val := range params { 116 | args := []string{"-I", iscsiIface, "--mode", "iface", "-o", "update", "-n", key, "-v", val} 117 | _, err = execCommand("iscsiadm", args...) 118 | if err != nil { 119 | return fmt.Errorf("unable to set parameter %s for iscsi iface %s: %v", key, iscsiIface, err) 120 | } 121 | } 122 | // now we can use it 123 | } 124 | 125 | args = []string{"-I", iscsiIface, "--mode", "discovery", "--portal", ip, "--type", "sendtargets", "--discover"} 126 | _, err = execCommand("iscsiadm", args...) 127 | return err 128 | } 129 | 130 | // HasSession checks to see if the session exists, may log an extraneous error if the seesion does not exist 131 | func (i *AttacherImpl) HasSession(ip, target string) (bool, error) { 132 | args := []string{"--mode", "session"} 133 | out, err := execCommand("iscsiadm", args...) 134 | if err != nil { 135 | return false, nil // this is almost certainly "No active sessions" 136 | } 137 | pat, err := regexp.Compile(ip + ".*" + target) 138 | if err != nil { 139 | return false, err 140 | } 141 | lines := strings.Split(string(out[:]), "\n") 142 | for _, line := range lines { 143 | found := pat.FindString(line) 144 | if found != "" { 145 | return true, nil 146 | } 147 | } 148 | return false, nil 149 | } 150 | 151 | func (i *AttacherImpl) Login(ip, target string) error { 152 | hasSession, err := i.HasSession(ip, target) 153 | if err != nil { 154 | return err 155 | } 156 | if hasSession { 157 | return nil 158 | } 159 | args := []string{"-I", iscsiIface, "--mode", "node", "--portal", ip, "--targetname", target, "--login"} 160 | _, err = execCommand("iscsiadm", args...) 161 | return err 162 | } 163 | 164 | func (i *AttacherImpl) Logout(ip, target string) error { 165 | hasSession, err := i.HasSession(ip, target) 166 | if err != nil { 167 | return err 168 | } 169 | if !hasSession { 170 | return nil 171 | } 172 | args := []string{"-I", iscsiIface, "--mode", "node", "--portal", ip, "--targetname", target, "--logout"} 173 | _, err = execCommand("iscsiadm", args...) 174 | return err 175 | } 176 | 177 | // read the bindings from /etc/multipath/bindings 178 | // separating into keep/discard sets 179 | // return elements map from volume name to scsi id 180 | func (i *AttacherImpl) MultipathReadBindings() (map[string]string, map[string]string, error) { 181 | 182 | var bindings = map[string]string{} 183 | var discard = map[string]string{} 184 | 185 | if _, err := os.Stat(multipathBindings); err != nil { 186 | if os.IsNotExist(err) { 187 | // file does not exist 188 | return bindings, discard, nil 189 | } 190 | return nil, nil, err 191 | } 192 | 193 | f, err := os.Open(multipathBindings) 194 | if err != nil { 195 | return nil, nil, err 196 | } 197 | defer f.Close() 198 | 199 | scanner := bufio.NewScanner(f) 200 | for scanner.Scan() { 201 | line := scanner.Text() 202 | if len(line) > 0 && line[0] != '#' { 203 | elements := strings.Fields(line) 204 | if len(elements) == 2 { 205 | 206 | if strings.HasPrefix(elements[0], "mpath") { 207 | discard[elements[0]] = elements[1] 208 | } else { 209 | bindings[elements[0]] = elements[1] 210 | } 211 | } 212 | } 213 | } 214 | 215 | return bindings, discard, nil 216 | } 217 | 218 | // write the bindings to /etc/multipath/bindings 219 | func (i *AttacherImpl) MultipathWriteBindings(bindings map[string]string) error { 220 | 221 | f, err := os.Create(multipathBindings) 222 | if err != nil { 223 | return err 224 | } 225 | defer f.Close() 226 | 227 | writer := bufio.NewWriter(f) 228 | for name, id := range bindings { 229 | writer.WriteString(fmt.Sprintf("%s %s\n", name, id)) 230 | } 231 | writer.Flush() 232 | return nil 233 | } 234 | 235 | // multipath hangs when run inside a container, but is safe to terminate 236 | func multipath(args ...string) (string, error) { 237 | 238 | ctx, cancel := context.WithTimeout(context.Background(), multipathTimeout) 239 | defer cancel() 240 | 241 | cmd := exec.CommandContext(ctx, multipathExec, args...) 242 | 243 | output, err := cmd.Output() 244 | 245 | if ctx.Err() == context.DeadlineExceeded { 246 | log.WithFields(log.Fields{"timeout": multipathTimeout, "args": strings.Join(args, " ")}).Info("multipath timed out") 247 | return string(output), nil 248 | } 249 | 250 | return string(output), err 251 | } 252 | 253 | // taken unabashedly from kubernetes kubelet iscsi volume driver, which is licensed Apache 2.0 254 | // see https://github.com/kubernetes/kubernetes/blob/ce42bc382e38dd7cd233b8a350723287f6e79f82/pkg/volume/iscsi/iscsi_util.go 255 | func parseIscsiIfaceShow(data string) (map[string]string, error) { 256 | params := make(map[string]string) 257 | slice := strings.Split(data, "\n") 258 | for _, line := range slice { 259 | if !strings.HasPrefix(line, "iface.") || strings.Contains(line, "") { 260 | continue 261 | } 262 | iface := strings.Fields(line) 263 | if len(iface) != 3 || iface[1] != "=" { 264 | return nil, fmt.Errorf("Error: invalid iface setting: %v", iface) 265 | } 266 | // iscsi_ifacename is immutable once the iface is created 267 | if iface[0] == "iface.iscsi_ifacename" { 268 | continue 269 | } 270 | params[iface[0]] = iface[2] 271 | } 272 | return params, nil 273 | } 274 | -------------------------------------------------------------------------------- /pkg/driver/controller.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/packethost/packngo" 11 | "github.com/pkg/errors" 12 | 13 | csi "github.com/container-storage-interface/spec/lib/go/csi" 14 | "github.com/packethost/csi-packet/pkg/packet" 15 | log "github.com/sirupsen/logrus" 16 | "golang.org/x/net/context" 17 | "google.golang.org/grpc/codes" 18 | "google.golang.org/grpc/status" 19 | ) 20 | 21 | const ( 22 | // VolumeMaxRetries maximum number of times to retry until giving up on a volume create request 23 | VolumeMaxRetries = 10 24 | // VolumeRetryInterval retry interval in seconds between retries to check if a volume create request is ready 25 | VolumeRetryInterval = 1 // in seconds 26 | // AttachMaxRetries maximum number of times to retry until giving up on a volume attach request 27 | AttachMaxRetries = 5 28 | // AttachRetryInterval retry interval in seconds between retries to check if a volume attachment is ready 29 | AttachRetryInterval = 1 // in seconds 30 | // DetachMaxRetries maximum number of times to retry until giving up on a volume detach request 31 | DetachMaxRetries = 30 32 | // AttachRetryInterval retry interval in seconds between retries to check if a volume is detached 33 | DetachRetryInterval = 2 // in seconds 34 | ) 35 | 36 | var _ csi.ControllerServer = &PacketControllerServer{} 37 | 38 | // PacketControllerServer controller server to manage CSI 39 | type PacketControllerServer struct { 40 | Provider packet.VolumeProvider 41 | } 42 | 43 | // NewPacketControllerServer create new PacketControllerServer with the given provider 44 | func NewPacketControllerServer(provider packet.VolumeProvider) *PacketControllerServer { 45 | return &PacketControllerServer{ 46 | Provider: provider, 47 | } 48 | } 49 | 50 | func getSizeRequest(capacityRange *csi.CapacityRange) int { 51 | // size request: 52 | // limit if specified 53 | // required otherwise 54 | // within restrictions of max, min 55 | // default otherwise 56 | var sizeRequestGiB int 57 | if capacityRange == nil { 58 | sizeRequestGiB = packet.DefaultVolumeSizeGi 59 | } else { 60 | maxBytes := capacityRange.GetLimitBytes() 61 | if maxBytes != 0 { 62 | sizeRequestGiB = int(maxBytes / packet.Gibi) 63 | 64 | } else { 65 | minBytes := capacityRange.GetRequiredBytes() 66 | if minBytes != 0 { 67 | sizeRequestGiB = int(minBytes / packet.Gibi) 68 | 69 | } 70 | } 71 | } 72 | if sizeRequestGiB > packet.MaxVolumeSizeGi { 73 | sizeRequestGiB = packet.MaxVolumeSizeGi 74 | } 75 | if sizeRequestGiB < packet.MinVolumeSizeGi { 76 | sizeRequestGiB = packet.MinVolumeSizeGi 77 | } 78 | return sizeRequestGiB 79 | } 80 | 81 | func getPlanID(parameters map[string]string) string { 82 | 83 | var planID string 84 | 85 | volumePlanRequest := parameters["plan"] 86 | switch volumePlanRequest { 87 | case packet.VolumePlanPerformance: 88 | planID = packet.VolumePlanPerformanceID 89 | case packet.VolumePlanStandard: 90 | planID = packet.VolumePlanStandardID 91 | default: 92 | planID = packet.VolumePlanStandardID 93 | 94 | } 95 | return planID 96 | } 97 | 98 | // CreateVolume create a volume in the given context 99 | // according to https://kubernetes-csi.github.io/docs/external-provisioner.html this should return 100 | // when the volume is successfully provisioned or fails 101 | // csi contains no provision for returning from a volume creation *request* and then checking later 102 | // thus, we must either succeed or fail before returning from this function call 103 | func (controller *PacketControllerServer) CreateVolume(ctx context.Context, in *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) { 104 | 105 | if controller == nil || controller.Provider == nil { 106 | return nil, status.Error(codes.Internal, "controller not configured") 107 | } 108 | logger := log.WithFields(log.Fields{"volume_name": in.Name}) 109 | logger.Info("CreateVolume called") 110 | 111 | if in.Name == "" { 112 | return nil, status.Error(codes.InvalidArgument, "Name unspecified for CreateVolume") 113 | } 114 | if in.VolumeCapabilities == nil { 115 | return nil, status.Error(codes.InvalidArgument, "VolumeCapabilities unspecified for CreateVolume") 116 | } 117 | 118 | sizeRequestGiB := getSizeRequest(in.CapacityRange) 119 | planID := getPlanID(in.Parameters) 120 | 121 | logger.WithFields(log.Fields{"planID": planID, "sizeRequestGiB": sizeRequestGiB}).Info("Volume requested") 122 | 123 | // check for pre-existing volume 124 | volumes, httpResponse, err := controller.Provider.ListVolumes(nil) 125 | if err != nil { 126 | return nil, errors.Wrap(err, httpResponse.Status) 127 | } 128 | if httpResponse.StatusCode != http.StatusOK { 129 | return nil, errors.Errorf("bad status from list volumes, %s", httpResponse.Status) 130 | } 131 | for _, volume := range volumes { 132 | 133 | description, err := packet.ReadDescription(volume.Description) 134 | if err == nil && description.Name == in.Name { 135 | logger.Infof("Volume already exists with id %s", volume.ID) 136 | 137 | if volume.Size != sizeRequestGiB { 138 | return nil, status.Errorf(codes.AlreadyExists, "mismatch with existing volume %s, size %d, requested %d", in.Name, volume.Size, sizeRequestGiB) 139 | } 140 | if volume.Plan.ID != planID { 141 | return nil, status.Errorf(codes.AlreadyExists, "mismatch with existing volume %s, plan %+v, requested %s", in.Name, volume.Plan, planID) 142 | } 143 | 144 | out := csi.CreateVolumeResponse{ 145 | Volume: &csi.Volume{ 146 | CapacityBytes: int64(volume.Size) * packet.Gibi, 147 | VolumeId: volume.ID, 148 | }, 149 | } 150 | return &out, nil 151 | } 152 | } 153 | 154 | description := packet.NewVolumeDescription(in.Name) 155 | 156 | volumeCreateRequest := packngo.VolumeCreateRequest{ 157 | Size: sizeRequestGiB, // int `json:"size"` 158 | BillingCycle: packet.BillingHourly, // string `json:"billing_cycle"` 159 | PlanID: planID, // string `json:"plan_id"` 160 | Description: description.String(), // string `json:"description,omitempty"` 161 | // SnapshotPolicies // []*SnapshotPolicy `json:"snapshot_policies,omitempty"` 162 | } 163 | volume, httpResponse, err := controller.Provider.Create(&volumeCreateRequest) 164 | 165 | if err != nil { 166 | return nil, err 167 | } 168 | if httpResponse.StatusCode != http.StatusOK && httpResponse.StatusCode != http.StatusCreated { 169 | return nil, errors.Errorf("bad status from create volume, %s", httpResponse.Status) 170 | } 171 | description, err = packet.ReadDescription(volume.Description) 172 | if err != nil { 173 | return nil, errors.Wrap(err, "unable to read csi description from provider volume") 174 | } 175 | 176 | // as described in the description to this CreateVolume method, we must wait for success or failure 177 | // before returning 178 | volReady := packet.VolumeReady(volume) 179 | for counter := 0; !volReady && counter < VolumeMaxRetries; counter++ { 180 | time.Sleep(VolumeRetryInterval * time.Second) 181 | volume, httpResponse, err := controller.Provider.Get(volume.ID) 182 | if err != nil { 183 | return nil, err 184 | } 185 | if httpResponse.StatusCode != http.StatusOK && httpResponse.StatusCode != http.StatusCreated { 186 | return nil, errors.Errorf("bad status from create volume, %s", httpResponse.Status) 187 | } 188 | volReady = packet.VolumeReady(volume) 189 | } 190 | if !volReady { 191 | return nil, errors.Errorf("volume %s not in ready state after %d seconds", volume.Name, VolumeMaxRetries*VolumeRetryInterval) 192 | } 193 | out := csi.CreateVolumeResponse{ 194 | Volume: &csi.Volume{ 195 | CapacityBytes: int64(volume.Size) * packet.Gibi, 196 | VolumeId: volume.ID, 197 | }, 198 | } 199 | 200 | return &out, nil 201 | } 202 | 203 | // DeleteVolume delete the specific volume 204 | func (controller *PacketControllerServer) DeleteVolume(ctx context.Context, in *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) { 205 | if controller == nil || controller.Provider == nil { 206 | return nil, status.Error(codes.Internal, "controller not configured") 207 | } 208 | logger := log.WithFields(log.Fields{"volume_id": in.VolumeId}) 209 | logger.Info("DeleteVolume called") 210 | 211 | if in.VolumeId == "" { 212 | return nil, status.Error(codes.InvalidArgument, "VolumeId unspecified for DeleteVolume") 213 | } 214 | 215 | httpResponse, err := controller.Provider.Delete(in.GetVolumeId()) 216 | if err != nil { 217 | if httpResponse.StatusCode == http.StatusUnprocessableEntity { 218 | return nil, status.Errorf(codes.FailedPrecondition, "delete should retry, %v", err) 219 | } 220 | return nil, status.Errorf(codes.Unknown, "bad status from delete volumes, %s", httpResponse.Status) 221 | } 222 | switch httpResponse.StatusCode { 223 | case http.StatusOK, http.StatusNoContent, http.StatusNotFound: 224 | return &csi.DeleteVolumeResponse{}, nil 225 | case http.StatusUnprocessableEntity: 226 | return nil, status.Errorf(codes.FailedPrecondition, "code %d indicates retry condition", httpResponse.StatusCode) 227 | } 228 | return nil, status.Errorf(codes.Unknown, "bad status from delete volumes, %s", httpResponse.Status) 229 | } 230 | 231 | // ControllerPublishVolume attaches a volume to a node 232 | func (controller *PacketControllerServer) ControllerPublishVolume(ctx context.Context, in *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) { 233 | if controller == nil || controller.Provider == nil { 234 | return nil, status.Error(codes.Internal, "controller not configured") 235 | } 236 | if in.NodeId == "" { 237 | return nil, status.Error(codes.InvalidArgument, "NodeId unspecified for ControllerPublishVolume") 238 | } 239 | if in.VolumeId == "" { 240 | return nil, status.Error(codes.InvalidArgument, "VolumeId unspecified for ControllerPublishVolume") 241 | } 242 | if in.VolumeCapability == nil { 243 | return nil, status.Error(codes.InvalidArgument, "VolumeCapability unspecified for ControllerPublishVolume") 244 | } 245 | logger := log.WithFields(log.Fields{"volume_id": in.VolumeId}) 246 | logger.Info("ControllerPublishVolume called") 247 | 248 | nodeID := in.NodeId 249 | volumeID := in.VolumeId 250 | 251 | volume, httpResponse, err := controller.Provider.Get(volumeID) 252 | 253 | returnError := processGetError(volumeID, httpResponse, err) 254 | if returnError != nil { 255 | return nil, returnError 256 | } 257 | 258 | // it is possible to try to attach, and it already is attached, but has not yet disconnected 259 | // we are willing to retry up to AttachRetryMax times, with AttachRetryDelay between each 260 | count := 0 261 | 262 | var attachment *packngo.VolumeAttachment 263 | forloop: 264 | for { 265 | attachment, httpResponse, err = controller.Provider.Attach(volumeID, nodeID) 266 | switch { 267 | case err != nil && httpResponse != nil && httpResponse.StatusCode == http.StatusNotFound: 268 | return nil, status.Errorf(codes.NotFound, "node or volume not found attempting to attach %s to %s", volumeID, nodeID) 269 | case err != nil && packet.IsWrongDeviceAttachment(err): 270 | if count > AttachMaxRetries { 271 | return nil, err 272 | } 273 | count++ 274 | time.Sleep(AttachRetryInterval * time.Second) 275 | continue forloop 276 | case err != nil && packet.IsTooManyDevicesAttached(err): 277 | return nil, err 278 | case err != nil: 279 | return nil, status.Errorf(codes.Unknown, "error attempting to attach %s to %s, %v", volumeID, nodeID, err) 280 | case httpResponse.StatusCode != http.StatusOK && httpResponse.StatusCode != http.StatusCreated: 281 | return nil, status.Errorf(codes.Unknown, "bad status from attach volumes, %s", httpResponse.Status) 282 | default: 283 | // if we made it this far, we have a valid attachment 284 | break forloop 285 | } 286 | } 287 | 288 | metadata := make(map[string]string) 289 | metadata["AttachmentId"] = attachment.ID 290 | metadata["VolumeId"] = volumeID 291 | metadata["VolumeName"] = volume.Name 292 | response := &csi.ControllerPublishVolumeResponse{ 293 | PublishContext: metadata, 294 | } 295 | return response, nil 296 | } 297 | 298 | // ControllerUnpublishVolume detaches a volume from a node 299 | func (controller *PacketControllerServer) ControllerUnpublishVolume(ctx context.Context, in *csi.ControllerUnpublishVolumeRequest) (*csi.ControllerUnpublishVolumeResponse, error) { 300 | logger := log.WithFields(log.Fields{"node_id": in.NodeId, "volume_id": in.VolumeId}) 301 | logger.Info("UnpublishVolume called") 302 | 303 | if controller == nil || controller.Provider == nil { 304 | return nil, status.Error(codes.Internal, "controller not configured") 305 | } 306 | 307 | nodeID := in.GetNodeId() 308 | volumeID := in.GetVolumeId() 309 | 310 | if volumeID == "" { 311 | return nil, status.Error(codes.InvalidArgument, "VolumeId unspecified for ControllerUnpublishVolume") 312 | } 313 | 314 | volume, httpResponse, err := controller.Provider.Get(volumeID) 315 | if err != nil { 316 | if httpResponse != nil && httpResponse.StatusCode == http.StatusNotFound { 317 | logger.Infof("volumeId not found, Get() returned %d", httpResponse.StatusCode) 318 | return &csi.ControllerUnpublishVolumeResponse{}, nil 319 | } 320 | return nil, err 321 | } 322 | if httpResponse.StatusCode != http.StatusOK { 323 | return nil, status.Errorf(codes.Unknown, "bad status from get volume %s, %s", volumeID, httpResponse.Status) 324 | } 325 | 326 | // get all of the attachments 327 | attachments := volume.Attachments 328 | if attachments == nil { 329 | return nil, status.Errorf(codes.Unknown, "cannot detach unattached volume %s", volumeID) 330 | } 331 | // go through each attachment. If its deviceID matches our desired nodeID, or if nodeID was blank, 332 | // add to the detach list. Else skip 333 | attachmentIDs := []string{} 334 | for _, attachment := range attachments { 335 | switch { 336 | case nodeID == "": 337 | logger.Infof("empty nodeID; detaching from node %s", attachment.Device.ID) 338 | attachmentIDs = append(attachmentIDs, attachment.ID) 339 | case attachment.Volume.ID == volumeID && attachment.Device.ID == nodeID: 340 | logger.Debugf("matched node ID; detaching from node %s", attachment.Device.ID) 341 | attachmentIDs = append(attachmentIDs, attachment.ID) 342 | default: 343 | logger.Debugf("mismatched node ID; not detaching from node %s", attachment.Device.ID) 344 | } 345 | } 346 | 347 | // if no valid attachments found, report an error 348 | if len(attachmentIDs) == 0 { 349 | return nil, status.Errorf(codes.Unknown, "no attachment ID found for volume %s", volumeID) 350 | } 351 | 352 | count := 0 353 | failed := []string{} 354 | retryIDs := []string{} 355 | 356 | // keep looping until we hit one of: 357 | // - exceed our retry count 358 | // - have no more retries 359 | for { 360 | for _, a := range attachmentIDs { 361 | httpResponse, err = controller.Provider.Detach(a) 362 | switch { 363 | case err != nil && httpResponse != nil && httpResponse.StatusCode == http.StatusNotFound: 364 | logger.WithFields(log.Fields{"attachmentID": a}).Infof("attachmentID not found, Detach() returned %d", httpResponse.StatusCode) 365 | failed = append(failed, fmt.Sprintf("%s: attachmentID not found, Detach() returned %d", a, httpResponse.StatusCode)) 366 | case err != nil && packet.IsDeviceStillAttached(err): 367 | // mark that we need to retry this attachment ID 368 | retryIDs = append(retryIDs, a) 369 | case err != nil: 370 | failed = append(failed, fmt.Sprintf(err.Error())) 371 | case httpResponse != nil && httpResponse.StatusCode != http.StatusOK && httpResponse.StatusCode != http.StatusNotFound: 372 | failed = append(failed, fmt.Sprintf("%s: bad status from detach volume, %s", a, httpResponse.Status)) 373 | } 374 | } 375 | // did we have any to retry or did we hit the max? 376 | if len(retryIDs) <= 0 || count >= DetachMaxRetries { 377 | break 378 | } 379 | // increment the counter 380 | count++ 381 | // reset the attachment IDs 382 | attachmentIDs = retryIDs[:] 383 | retryIDs = retryIDs[:0] 384 | time.Sleep(DetachRetryInterval * time.Second) 385 | } 386 | 387 | // did we have any left to retry? If so, create errors for it 388 | for _, a := range retryIDs { 389 | failed = append(failed, fmt.Sprintf("%s: could not detach after %d retries in %d seconds", a, DetachMaxRetries, DetachRetryInterval*DetachMaxRetries)) 390 | } 391 | 392 | // did we succeed? 393 | if len(failed) != 0 { 394 | return nil, fmt.Errorf(strings.Join(failed, ";")) 395 | } 396 | 397 | logger.Info("successful Detach()") 398 | return &csi.ControllerUnpublishVolumeResponse{}, nil 399 | } 400 | 401 | // ValidateVolumeCapabilities validate that a given volume has the require capabilities 402 | func (controller *PacketControllerServer) ValidateVolumeCapabilities(ctx context.Context, in *csi.ValidateVolumeCapabilitiesRequest) (*csi.ValidateVolumeCapabilitiesResponse, error) { 403 | 404 | if controller == nil || controller.Provider == nil { 405 | return nil, status.Error(codes.Internal, "controller not configured") 406 | } 407 | 408 | if in.VolumeCapabilities == nil { 409 | return nil, status.Error(codes.InvalidArgument, "VolumeCapability unspecified for ValidateVolumeCapabilities") 410 | } 411 | if in.VolumeId == "" { 412 | return nil, status.Error(codes.InvalidArgument, "VolumeId unspecified for ValidateVolumeCapabilities") 413 | } 414 | // we always have to retrieve the volume to check that it exists; it is a CSI spec requirement 415 | volumeID := in.VolumeId 416 | _, httpResponse, err := controller.Provider.Get(volumeID) 417 | returnError := processGetError(volumeID, httpResponse, err) 418 | if returnError != nil { 419 | return nil, returnError 420 | } 421 | 422 | // supported capabilities all defined here instead 423 | supported := map[csi.VolumeCapability_AccessMode_Mode]bool{ 424 | csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER: true, 425 | csi.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY: true, 426 | } 427 | for _, cap := range in.VolumeCapabilities { 428 | mode := cap.AccessMode.Mode 429 | if !supported[mode] { 430 | return &csi.ValidateVolumeCapabilitiesResponse{}, nil 431 | } 432 | } 433 | return &csi.ValidateVolumeCapabilitiesResponse{ 434 | Confirmed: &csi.ValidateVolumeCapabilitiesResponse_Confirmed{ 435 | VolumeCapabilities: in.VolumeCapabilities, 436 | }, 437 | }, nil 438 | } 439 | 440 | // ListVolumes list known volumes 441 | func (controller *PacketControllerServer) ListVolumes(ctx context.Context, in *csi.ListVolumesRequest) (*csi.ListVolumesResponse, error) { 442 | if controller == nil || controller.Provider == nil { 443 | return nil, status.Error(codes.Internal, "controller not configured") 444 | } 445 | 446 | // was there any pagination? 447 | var listOptions *packngo.ListOptions 448 | if in != nil { 449 | listOptions = &packngo.ListOptions{ 450 | PerPage: int(in.MaxEntries), 451 | } 452 | if in.StartingToken != "" { 453 | page, err := strconv.Atoi(in.StartingToken) 454 | if err != nil { 455 | return nil, status.Errorf(codes.Aborted, "starting token must be an integer to indicate which page, %s", in.StartingToken) 456 | } 457 | listOptions.Page = page 458 | } 459 | } 460 | volumes, httpResponse, err := controller.Provider.ListVolumes(listOptions) 461 | if err != nil { 462 | return nil, errors.Wrap(err, httpResponse.Status) 463 | } 464 | if httpResponse.StatusCode != http.StatusOK { 465 | return nil, status.Errorf(codes.Unknown, "bad status from list volumes, %s", httpResponse.Status) 466 | } 467 | entries := []*csi.ListVolumesResponse_Entry{} 468 | for _, volume := range volumes { 469 | entry := &csi.ListVolumesResponse_Entry{ 470 | Volume: &csi.Volume{ 471 | CapacityBytes: int64(volume.Size * 1024 * 1024 * 1024), 472 | VolumeId: volume.ID, 473 | }, 474 | } 475 | entries = append(entries, entry) 476 | } 477 | response := &csi.ListVolumesResponse{} 478 | response.Entries = entries 479 | return response, nil 480 | 481 | } 482 | 483 | // GetCapacity get the available capacity 484 | func (controller *PacketControllerServer) GetCapacity(ctx context.Context, in *csi.GetCapacityRequest) (*csi.GetCapacityResponse, error) { 485 | return nil, status.Error(codes.Unimplemented, "") 486 | } 487 | 488 | // ControllerGetCapabilities get capabilities of the controller 489 | func (controller *PacketControllerServer) ControllerGetCapabilities(ctx context.Context, in *csi.ControllerGetCapabilitiesRequest) (*csi.ControllerGetCapabilitiesResponse, error) { 490 | 491 | // mapping function from defined RPC constant to capability type 492 | rpcCapMapper := func(cap csi.ControllerServiceCapability_RPC_Type) *csi.ControllerServiceCapability { 493 | return &csi.ControllerServiceCapability{ 494 | Type: &csi.ControllerServiceCapability_Rpc{ 495 | Rpc: &csi.ControllerServiceCapability_RPC{ 496 | Type: cap, 497 | }, 498 | }, 499 | } 500 | } 501 | 502 | // XXX review, move definitions elsewhere 503 | var caps []*csi.ControllerServiceCapability 504 | for _, rpcCap := range []csi.ControllerServiceCapability_RPC_Type{ 505 | csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME, 506 | csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME, 507 | csi.ControllerServiceCapability_RPC_LIST_VOLUMES, 508 | } { 509 | caps = append(caps, rpcCapMapper(rpcCap)) 510 | } 511 | 512 | resp := &csi.ControllerGetCapabilitiesResponse{ 513 | Capabilities: caps, 514 | } 515 | 516 | return resp, nil 517 | } 518 | 519 | // CreateSnapshot snapshot a single volume 520 | func (controller *PacketControllerServer) CreateSnapshot(context.Context, *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) { 521 | return nil, status.Error(codes.Unimplemented, "") 522 | } 523 | 524 | // DeleteSnapshot delete an existing snapshot 525 | func (controller *PacketControllerServer) DeleteSnapshot(context.Context, *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) { 526 | return nil, status.Error(codes.Unimplemented, "") 527 | } 528 | 529 | // ListSnapshots list known snapshots 530 | func (controller *PacketControllerServer) ListSnapshots(context.Context, *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) { 531 | return nil, status.Error(codes.Unimplemented, "") 532 | } 533 | 534 | // ControllerExpandVolume expand a volume 535 | func (controller *PacketControllerServer) ControllerExpandVolume(context.Context, *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) { 536 | return nil, status.Error(codes.Unimplemented, "") 537 | } 538 | 539 | // take the packet error return code from Provider.Get and determine what we should do with it 540 | func processGetError(volumeID string, httpResponse *packngo.Response, err error) error { 541 | // if we have no valid response and an error, return the error immediately 542 | // if we have a valid response but not found, return the not found special code 543 | // if we have a valid response but not OK, return a general error 544 | // if any other error, return it 545 | // otherwise, everything is fine, continue 546 | switch { 547 | case httpResponse == nil && err != nil: 548 | return err 549 | case httpResponse != nil && httpResponse.StatusCode == http.StatusNotFound: 550 | return status.Errorf(codes.NotFound, "volume not found %s", volumeID) 551 | case httpResponse != nil && httpResponse.StatusCode != http.StatusOK: 552 | return errors.Errorf("bad status from get volume %s, %s", volumeID, httpResponse.Status) 553 | case err != nil: 554 | return err 555 | default: 556 | return nil 557 | } 558 | } 559 | -------------------------------------------------------------------------------- /pkg/driver/controller_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/packethost/csi-packet/pkg/packet" 10 | "github.com/packethost/csi-packet/pkg/test" 11 | 12 | "github.com/container-storage-interface/spec/lib/go/csi" 13 | "github.com/golang/mock/gomock" 14 | "github.com/packethost/packngo" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | const ( 19 | attachmentID = "60bf5425-e59d-42c3-b9b9-ac0d8cfc86a2" 20 | providerVolumeID = "9b03a6ea-42fb-40c7-abaa-247445b36890" 21 | csiNodeIP = "10.88.52.133" 22 | csiNodeName = "spcfoobar-worker-1" 23 | nodeID = "262c173c-c24d-4ad6-be1a-13fd9a523cfa" 24 | ) 25 | 26 | func TestCreateVolume(t *testing.T) { 27 | csiVolumeName := "kubernetes-volume-request-0987654321" 28 | 29 | mockCtrl := gomock.NewController(t) 30 | defer mockCtrl.Finish() 31 | 32 | provider := test.NewMockVolumeProvider(mockCtrl) 33 | volume := packngo.Volume{ 34 | Size: packet.DefaultVolumeSizeGi, 35 | ID: providerVolumeID, 36 | Description: packet.NewVolumeDescription(csiVolumeName).String(), 37 | State: "active", 38 | } 39 | resp := packngo.Response{ 40 | Response: &http.Response{ 41 | StatusCode: http.StatusOK, 42 | }, 43 | Rate: packngo.Rate{}, 44 | } 45 | provider.EXPECT().ListVolumes().Return([]packngo.Volume{}, &resp, nil) 46 | provider.EXPECT().Create(gomock.Any()).Return(&volume, &resp, nil) 47 | 48 | controller := NewPacketControllerServer(provider) 49 | volumeRequest := csi.CreateVolumeRequest{ 50 | VolumeCapabilities: []*csi.VolumeCapability{ 51 | &csi.VolumeCapability{ 52 | AccessMode: &csi.VolumeCapability_AccessMode{ 53 | Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, 54 | }, 55 | }, 56 | }, 57 | } 58 | volumeRequest.Name = csiVolumeName 59 | volumeRequest.CapacityRange = &csi.CapacityRange{ 60 | RequiredBytes: 10 * packet.Gibi, 61 | LimitBytes: 100 * packet.Gibi, 62 | } 63 | 64 | csiResp, err := controller.CreateVolume(context.TODO(), &volumeRequest) 65 | assert.Nil(t, err) 66 | assert.Equal(t, providerVolumeID, csiResp.GetVolume().VolumeId) 67 | assert.Equal(t, packet.DefaultVolumeSizeGi*packet.Gibi, csiResp.GetVolume().GetCapacityBytes()) 68 | } 69 | 70 | type matchRequest struct { 71 | desc string 72 | request packngo.VolumeCreateRequest 73 | } 74 | 75 | func MatchRequest(desc string, request packngo.VolumeCreateRequest) gomock.Matcher { 76 | return &matchRequest{desc, request} 77 | } 78 | 79 | func (o *matchRequest) Matches(x interface{}) bool { 80 | volumeRequest := x.(*packngo.VolumeCreateRequest) 81 | return volumeRequest.Size == o.request.Size && 82 | volumeRequest.PlanID == o.request.PlanID 83 | } 84 | 85 | type matchGet struct { 86 | desc string 87 | id string 88 | } 89 | 90 | func (o *matchGet) Matches(x interface{}) bool { 91 | id := x.(string) 92 | return id == o.id 93 | } 94 | 95 | func (o *matchRequest) String() string { 96 | return fmt.Sprintf("[%s] has request matching <<%v>>", o.desc, o.request) 97 | } 98 | 99 | func runTestCreateVolume(t *testing.T, description string, volumeRequest csi.CreateVolumeRequest, providerRequest packngo.VolumeCreateRequest, providerVolume packngo.Volume, success bool, delayToSuccess int) { 100 | 101 | mockCtrl := gomock.NewController(t) 102 | defer mockCtrl.Finish() 103 | 104 | provider := test.NewMockVolumeProvider(mockCtrl) 105 | 106 | resp := packngo.Response{ 107 | Response: &http.Response{ 108 | StatusCode: http.StatusOK, 109 | }, 110 | Rate: packngo.Rate{}, 111 | } 112 | provider.EXPECT().ListVolumes().Return([]packngo.Volume{}, &resp, nil) 113 | // provider.EXPECT().Create(gomock.Any()).Return(&providerVolume, &resp, nil) 114 | provider.EXPECT(). 115 | Create(MatchRequest(description, providerRequest)). 116 | Return(&providerVolume, &resp, nil) 117 | 118 | // did we ask for it not to be ready a few times before it is ready? 119 | if delayToSuccess > 0 { 120 | // set the state to queued 121 | providerVolume.State = "queued" 122 | // we do it up to the maximum times 123 | calls := min(delayToSuccess-1, VolumeMaxRetries) 124 | for i := 0; i < calls; i++ { 125 | pv := providerVolume 126 | pv.State = "queued" 127 | provider.EXPECT().Get(pv.ID).Return(&pv, &resp, nil) 128 | } 129 | // for the last one, if not beyond max, set the state to "active" 130 | if delayToSuccess < VolumeMaxRetries { 131 | pv := providerVolume 132 | pv.State = "active" 133 | provider.EXPECT().Get(pv.ID).Return(&pv, &resp, nil) 134 | } 135 | } 136 | 137 | controller := NewPacketControllerServer(provider) 138 | 139 | csiResp, err := controller.CreateVolume(context.TODO(), &volumeRequest) 140 | if success { 141 | assert.Nil(t, err, description) 142 | assert.Equal(t, providerVolume.ID, csiResp.GetVolume().VolumeId, description) 143 | assert.Equal(t, int64(providerVolume.Size)*packet.Gibi, csiResp.GetVolume().GetCapacityBytes(), description) 144 | } else { 145 | assert.NotNil(t, err, description) 146 | } 147 | } 148 | 149 | type VolumeTestCase struct { 150 | description string 151 | volumeRequest csi.CreateVolumeRequest 152 | providerRequest packngo.VolumeCreateRequest 153 | providerVolume packngo.Volume 154 | success bool 155 | delayToSuccess int 156 | } 157 | 158 | func TestCreateVolumes(t *testing.T) { 159 | testCases := []VolumeTestCase{ 160 | VolumeTestCase{ 161 | description: "verify capacity specification", 162 | volumeRequest: csi.CreateVolumeRequest{ 163 | Name: "pv-qT2QXcwbqPB3BAurt1ccs7g6SDVT0qLv", 164 | CapacityRange: &csi.CapacityRange{ 165 | RequiredBytes: 10 * packet.Gibi, 166 | LimitBytes: 173 * packet.Gibi, 167 | }, 168 | VolumeCapabilities: []*csi.VolumeCapability{ 169 | &csi.VolumeCapability{ 170 | AccessMode: &csi.VolumeCapability_AccessMode{ 171 | Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, 172 | }, 173 | }, 174 | }, 175 | }, 176 | providerRequest: packngo.VolumeCreateRequest{ 177 | BillingCycle: packet.BillingHourly, 178 | Description: packet.NewVolumeDescription("pv-qT2QXcwbqPB3BAurt1ccs7g6SDVT0qLv").String(), 179 | Locked: false, 180 | Size: 173, 181 | PlanID: packet.VolumePlanStandardID, 182 | }, 183 | providerVolume: packngo.Volume{ 184 | Size: 173, 185 | ID: "5a3c678a-64a4-41ba-a03c-e7d74a96f06a", 186 | Description: packet.NewVolumeDescription("pv-qT2QXcwbqPB3BAurt1ccs7g6SDVT0qLv").String(), 187 | State: "active", 188 | }, 189 | success: true, 190 | delayToSuccess: 0, 191 | }, 192 | VolumeTestCase{ 193 | description: "verify capacity maximum", 194 | volumeRequest: csi.CreateVolumeRequest{ 195 | Name: "pv-61C4yMq09WV1ZpNIOBKHRQDKoZzyK7ZF", 196 | CapacityRange: &csi.CapacityRange{ 197 | RequiredBytes: 1 * 1024 * 1024, 198 | LimitBytes: 15000 * packet.Gibi, 199 | }, 200 | VolumeCapabilities: []*csi.VolumeCapability{ 201 | &csi.VolumeCapability{ 202 | AccessMode: &csi.VolumeCapability_AccessMode{ 203 | Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, 204 | }, 205 | }, 206 | }, 207 | }, 208 | providerRequest: packngo.VolumeCreateRequest{ 209 | BillingCycle: packet.BillingHourly, 210 | Description: packet.NewVolumeDescription("pv-61C4yMq09WV1ZpNIOBKHRQDKoZzyK7ZF").String(), 211 | Locked: false, 212 | Size: packet.MaxVolumeSizeGi, 213 | PlanID: packet.VolumePlanStandardID, 214 | }, 215 | providerVolume: packngo.Volume{ 216 | Size: packet.DefaultVolumeSizeGi, 217 | ID: "06e45c5c-8bd9-44fd-a9e4-1518105de113", 218 | Description: packet.NewVolumeDescription("pv-61C4yMq09WV1ZpNIOBKHRQDKoZzyK7ZF").String(), 219 | State: "active", 220 | }, 221 | success: true, 222 | delayToSuccess: 0, 223 | }, 224 | VolumeTestCase{ 225 | description: "verify capacity minimum", 226 | volumeRequest: csi.CreateVolumeRequest{ 227 | Name: "pv-pUk6DzHQF3cGMfLCRnXSpDJ2HpzhefKI", 228 | CapacityRange: &csi.CapacityRange{ 229 | RequiredBytes: 1 * 1024 * 1024, 230 | LimitBytes: 1 * 1024 * 1024, 231 | }, 232 | VolumeCapabilities: []*csi.VolumeCapability{ 233 | &csi.VolumeCapability{ 234 | AccessMode: &csi.VolumeCapability_AccessMode{ 235 | Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, 236 | }, 237 | }, 238 | }, 239 | }, 240 | providerRequest: packngo.VolumeCreateRequest{ 241 | BillingCycle: packet.BillingHourly, 242 | Description: packet.NewVolumeDescription("pv-61C4yMq09WV1ZpNIOBKHRQDKoZzyK7ZF").String(), 243 | Locked: false, 244 | Size: packet.MinVolumeSizeGi, 245 | PlanID: packet.VolumePlanStandardID, 246 | }, 247 | providerVolume: packngo.Volume{ 248 | Size: packet.DefaultVolumeSizeGi, 249 | ID: "8c3b6f51-7045-44b8-ab6d-d6df7371471e", 250 | Description: packet.NewVolumeDescription("pv-61C4yMq09WV1ZpNIOBKHRQDKoZzyK7ZF").String(), 251 | State: "active", 252 | }, 253 | success: true, 254 | delayToSuccess: 0, 255 | }, 256 | VolumeTestCase{ 257 | description: "verify capacity default, performance plan type", 258 | volumeRequest: csi.CreateVolumeRequest{ 259 | Name: "pv-pUk6DzHQF3cGMfLCRnXSpDJ2HpzhefKI", 260 | Parameters: map[string]string{"plan": "performance"}, 261 | VolumeCapabilities: []*csi.VolumeCapability{ 262 | &csi.VolumeCapability{ 263 | AccessMode: &csi.VolumeCapability_AccessMode{ 264 | Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, 265 | }, 266 | }, 267 | }, 268 | }, 269 | providerRequest: packngo.VolumeCreateRequest{ 270 | BillingCycle: packet.BillingHourly, 271 | Description: packet.NewVolumeDescription("pv-61C4yMq09WV1ZpNIOBKHRQDKoZzyK7ZF").String(), 272 | Locked: false, 273 | Size: packet.DefaultVolumeSizeGi, 274 | PlanID: packet.VolumePlanPerformanceID, 275 | }, 276 | providerVolume: packngo.Volume{ 277 | Size: packet.DefaultVolumeSizeGi, 278 | ID: "a94ecff0-b221-4d2d-8dc4-432bed506941", 279 | Description: packet.NewVolumeDescription("pv-61C4yMq09WV1ZpNIOBKHRQDKoZzyK7ZF").String(), 280 | State: "active", 281 | }, 282 | success: true, 283 | delayToSuccess: 0, 284 | }, 285 | VolumeTestCase{ 286 | description: "becomes ready after 5 seconds", 287 | volumeRequest: csi.CreateVolumeRequest{ 288 | Name: "pv-succeedin5", 289 | Parameters: map[string]string{"plan": "performance"}, 290 | VolumeCapabilities: []*csi.VolumeCapability{ 291 | &csi.VolumeCapability{ 292 | AccessMode: &csi.VolumeCapability_AccessMode{ 293 | Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, 294 | }, 295 | }, 296 | }, 297 | }, 298 | providerRequest: packngo.VolumeCreateRequest{ 299 | BillingCycle: packet.BillingHourly, 300 | Description: packet.NewVolumeDescription("pv-succeedin5").String(), 301 | Locked: false, 302 | Size: packet.DefaultVolumeSizeGi, 303 | PlanID: packet.VolumePlanPerformanceID, 304 | }, 305 | providerVolume: packngo.Volume{ 306 | Size: packet.DefaultVolumeSizeGi, 307 | ID: "abcdef-22bb-d4d4-cc5f-cw1123456abcdef", 308 | Description: packet.NewVolumeDescription("pv-succeedin5").String(), 309 | State: "active", 310 | }, 311 | success: true, 312 | delayToSuccess: 7, 313 | }, 314 | VolumeTestCase{ 315 | description: "never becomes ready", 316 | volumeRequest: csi.CreateVolumeRequest{ 317 | Name: "pv-failfail123", 318 | Parameters: map[string]string{"plan": "performance"}, 319 | VolumeCapabilities: []*csi.VolumeCapability{ 320 | &csi.VolumeCapability{ 321 | AccessMode: &csi.VolumeCapability_AccessMode{ 322 | Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, 323 | }, 324 | }, 325 | }, 326 | }, 327 | providerRequest: packngo.VolumeCreateRequest{ 328 | BillingCycle: packet.BillingHourly, 329 | Description: packet.NewVolumeDescription("pv-failfail123").String(), 330 | Locked: false, 331 | Size: packet.DefaultVolumeSizeGi, 332 | PlanID: packet.VolumePlanPerformanceID, 333 | }, 334 | providerVolume: packngo.Volume{ 335 | Size: packet.DefaultVolumeSizeGi, 336 | ID: "ff665c4-22bb-d4d4-cc5f-cw1123456abcdef", 337 | Description: packet.NewVolumeDescription("pv-failfail123").String(), 338 | State: "active", 339 | }, 340 | success: false, 341 | delayToSuccess: 1000, 342 | }, 343 | } 344 | 345 | for _, testCase := range testCases { 346 | runTestCreateVolume(t, testCase.description, testCase.volumeRequest, testCase.providerRequest, testCase.providerVolume, testCase.success, testCase.delayToSuccess) 347 | } 348 | } 349 | 350 | func TestIdempotentCreateVolume(t *testing.T) { 351 | 352 | csiVolumeName := "kubernetes-volume-request-0987654321" 353 | 354 | mockCtrl := gomock.NewController(t) 355 | defer mockCtrl.Finish() 356 | 357 | provider := test.NewMockVolumeProvider(mockCtrl) 358 | volumeAlreadyExisting := packngo.Volume{ 359 | Size: packet.DefaultVolumeSizeGi, 360 | ID: providerVolumeID, 361 | Description: packet.NewVolumeDescription(csiVolumeName).String(), 362 | Plan: &packngo.Plan{ 363 | Name: packet.VolumePlanStandard, 364 | ID: packet.VolumePlanStandardID, 365 | }, 366 | } 367 | resp := packngo.Response{ 368 | Response: &http.Response{ 369 | StatusCode: http.StatusOK, 370 | }, 371 | Rate: packngo.Rate{}, 372 | } 373 | provider.EXPECT().ListVolumes().Return([]packngo.Volume{volumeAlreadyExisting}, &resp, nil) 374 | 375 | controller := NewPacketControllerServer(provider) 376 | volumeRequest := csi.CreateVolumeRequest{ 377 | Name: csiVolumeName, 378 | CapacityRange: &csi.CapacityRange{ 379 | RequiredBytes: packet.DefaultVolumeSizeGi * packet.Gibi, 380 | LimitBytes: packet.DefaultVolumeSizeGi * packet.Gibi, 381 | }, 382 | VolumeCapabilities: []*csi.VolumeCapability{ 383 | &csi.VolumeCapability{ 384 | AccessMode: &csi.VolumeCapability_AccessMode{ 385 | Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, 386 | }, 387 | }, 388 | }, 389 | Parameters: map[string]string{ 390 | "plan": packet.VolumePlanStandard, 391 | }, 392 | } 393 | 394 | csiResp, err := controller.CreateVolume(context.TODO(), &volumeRequest) 395 | assert.Nil(t, err) 396 | assert.Equal(t, volumeAlreadyExisting.ID, csiResp.GetVolume().VolumeId) 397 | } 398 | 399 | func TestListVolumes(t *testing.T) { 400 | 401 | mockCtrl := gomock.NewController(t) 402 | defer mockCtrl.Finish() 403 | 404 | provider := test.NewMockVolumeProvider(mockCtrl) 405 | 406 | resp := packngo.Response{ 407 | Response: &http.Response{ 408 | StatusCode: http.StatusOK, 409 | }, 410 | Rate: packngo.Rate{}, 411 | } 412 | provider.EXPECT().ListVolumes().Return([]packngo.Volume{}, &resp, nil) 413 | 414 | controller := NewPacketControllerServer(provider) 415 | volumeRequest := csi.ListVolumesRequest{} 416 | 417 | csiResp, err := controller.ListVolumes(context.TODO(), &volumeRequest) 418 | assert.Nil(t, err) 419 | assert.NotNil(t, csiResp) 420 | assert.Equal(t, 0, len(csiResp.Entries)) 421 | 422 | } 423 | 424 | func TestDeleteVolume(t *testing.T) { 425 | 426 | mockCtrl := gomock.NewController(t) 427 | defer mockCtrl.Finish() 428 | 429 | provider := test.NewMockVolumeProvider(mockCtrl) 430 | 431 | resp := packngo.Response{ 432 | Response: &http.Response{ 433 | StatusCode: http.StatusOK, 434 | }, 435 | Rate: packngo.Rate{}, 436 | } 437 | provider.EXPECT().Delete(providerVolumeID).Return(&resp, nil) 438 | 439 | controller := NewPacketControllerServer(provider) 440 | volumeRequest := csi.DeleteVolumeRequest{ 441 | VolumeId: providerVolumeID, 442 | } 443 | 444 | csiResp, err := controller.DeleteVolume(context.TODO(), &volumeRequest) 445 | assert.Nil(t, err) 446 | assert.NotNil(t, csiResp) 447 | 448 | } 449 | 450 | func TestPublishVolume(t *testing.T) { 451 | 452 | providerVolumeName := "name-assigned-by-provider" 453 | 454 | mockCtrl := gomock.NewController(t) 455 | defer mockCtrl.Finish() 456 | 457 | provider := test.NewMockVolumeProvider(mockCtrl) 458 | 459 | resp := packngo.Response{ 460 | Response: &http.Response{ 461 | StatusCode: http.StatusOK, 462 | }, 463 | Rate: packngo.Rate{}, 464 | } 465 | nodeIPAddress := packngo.IPAddressAssignment{} 466 | nodeIPAddress.Address = csiNodeIP 467 | volumeResp := packngo.Volume{ 468 | ID: providerVolumeID, 469 | Name: providerVolumeName, 470 | Attachments: []*packngo.VolumeAttachment{ 471 | &packngo.VolumeAttachment{ 472 | ID: attachmentID, 473 | Volume: packngo.Volume{ 474 | ID: providerVolumeID, 475 | }, 476 | Device: packngo.Device{DeviceRaw: packngo.DeviceRaw{ID: nodeID}}, 477 | }, 478 | }, 479 | } 480 | attachResp := packngo.VolumeAttachment{ 481 | ID: attachmentID, 482 | Volume: volumeResp, 483 | Device: packngo.Device{DeviceRaw: packngo.DeviceRaw{ID: nodeID}}, 484 | } 485 | 486 | provider.EXPECT().Get(providerVolumeID).Return(&volumeResp, &resp, nil) 487 | 488 | provider.EXPECT().Attach(providerVolumeID, nodeID).Return(&attachResp, &resp, nil) 489 | 490 | controller := NewPacketControllerServer(provider) 491 | volumeRequest := csi.ControllerPublishVolumeRequest{ 492 | VolumeId: providerVolumeID, 493 | NodeId: nodeID, 494 | VolumeCapability: &csi.VolumeCapability{}, 495 | } 496 | 497 | csiResp, err := controller.ControllerPublishVolume(context.TODO(), &volumeRequest) 498 | assert.Nil(t, err) 499 | assert.NotNil(t, csiResp) 500 | assert.NotNil(t, csiResp.GetPublishContext()) 501 | assert.Equal(t, attachmentID, csiResp.PublishContext["AttachmentId"]) 502 | assert.Equal(t, providerVolumeID, csiResp.PublishContext["VolumeId"]) 503 | assert.Equal(t, providerVolumeName, csiResp.PublishContext["VolumeName"]) 504 | 505 | } 506 | 507 | func TestUnpublishVolume(t *testing.T) { 508 | 509 | mockCtrl := gomock.NewController(t) 510 | defer mockCtrl.Finish() 511 | 512 | provider := test.NewMockVolumeProvider(mockCtrl) 513 | 514 | resp := packngo.Response{ 515 | Response: &http.Response{ 516 | StatusCode: http.StatusOK, 517 | }, 518 | Rate: packngo.Rate{}, 519 | } 520 | attachedVolume := packngo.Volume{ 521 | ID: providerVolumeID, 522 | Attachments: []*packngo.VolumeAttachment{ 523 | &packngo.VolumeAttachment{ 524 | ID: attachmentID, 525 | Volume: packngo.Volume{ 526 | ID: providerVolumeID, 527 | }, 528 | Device: packngo.Device{DeviceRaw: packngo.DeviceRaw{ID: nodeID}}, 529 | }, 530 | }, 531 | } 532 | 533 | provider.EXPECT().Get(providerVolumeID).Return(&attachedVolume, &resp, nil) 534 | provider.EXPECT().Detach(attachmentID).Return(&resp, nil) 535 | 536 | controller := NewPacketControllerServer(provider) 537 | volumeRequest := csi.ControllerUnpublishVolumeRequest{ 538 | VolumeId: providerVolumeID, 539 | NodeId: nodeID, 540 | } 541 | 542 | csiResp, err := controller.ControllerUnpublishVolume(context.TODO(), &volumeRequest) 543 | assert.Nil(t, err) 544 | assert.NotNil(t, csiResp) 545 | 546 | } 547 | 548 | func TestGetCapacity(t *testing.T) { 549 | mockCtrl := gomock.NewController(t) 550 | defer mockCtrl.Finish() 551 | provider := test.NewMockVolumeProvider(mockCtrl) 552 | 553 | capacityRequest := csi.GetCapacityRequest{} 554 | controller := NewPacketControllerServer(provider) 555 | csiResp, err := controller.GetCapacity(context.TODO(), &capacityRequest) 556 | assert.NotNil(t, err, "this method is not implemented") 557 | assert.Nil(t, csiResp, "this method is not implemented") 558 | } 559 | 560 | type volumeCapabilityTestCase struct { 561 | capabilitySet []*csi.VolumeCapability 562 | packetSupported *csi.ValidateVolumeCapabilitiesResponse_Confirmed 563 | description string 564 | } 565 | 566 | func getVolumeCapabilityTestCases() []volumeCapabilityTestCase { 567 | 568 | snwCap := csi.VolumeCapability{ 569 | AccessMode: &csi.VolumeCapability_AccessMode{Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER}, 570 | } 571 | snroCap := csi.VolumeCapability{ 572 | AccessMode: &csi.VolumeCapability_AccessMode{Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY}, 573 | } 574 | mnmwCap := csi.VolumeCapability{ 575 | AccessMode: &csi.VolumeCapability_AccessMode{Mode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER}, 576 | } 577 | mnroCap := csi.VolumeCapability{ 578 | AccessMode: &csi.VolumeCapability_AccessMode{Mode: csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY}, 579 | } 580 | mnswCap := csi.VolumeCapability{ 581 | AccessMode: &csi.VolumeCapability_AccessMode{Mode: csi.VolumeCapability_AccessMode_MULTI_NODE_SINGLE_WRITER}, 582 | } 583 | 584 | return []volumeCapabilityTestCase{ 585 | 586 | { 587 | capabilitySet: []*csi.VolumeCapability{&snwCap}, 588 | packetSupported: &csi.ValidateVolumeCapabilitiesResponse_Confirmed{ 589 | VolumeCapabilities: []*csi.VolumeCapability{ 590 | &snwCap, 591 | }, 592 | }, 593 | description: "single node writer", 594 | }, 595 | { 596 | capabilitySet: []*csi.VolumeCapability{&snroCap}, 597 | packetSupported: &csi.ValidateVolumeCapabilitiesResponse_Confirmed{ 598 | VolumeCapabilities: []*csi.VolumeCapability{ 599 | &snroCap, 600 | }, 601 | }, 602 | description: "single node read only", 603 | }, 604 | { 605 | capabilitySet: []*csi.VolumeCapability{&mnroCap}, 606 | description: "multi node read only", 607 | }, 608 | { 609 | capabilitySet: []*csi.VolumeCapability{&mnswCap}, 610 | description: "multinode single writer", 611 | }, 612 | { 613 | capabilitySet: []*csi.VolumeCapability{&mnmwCap}, 614 | description: "multi node multi writer", 615 | }, 616 | { 617 | capabilitySet: []*csi.VolumeCapability{&mnmwCap, &mnroCap, &mnswCap, &snroCap, &snwCap}, 618 | description: "all capabilities", 619 | }, 620 | { 621 | capabilitySet: []*csi.VolumeCapability{&snroCap, &snwCap}, 622 | packetSupported: &csi.ValidateVolumeCapabilitiesResponse_Confirmed{ 623 | VolumeCapabilities: []*csi.VolumeCapability{ 624 | &snroCap, &snwCap, 625 | }, 626 | }, 627 | description: "single node capabilities", 628 | }, 629 | } 630 | } 631 | 632 | func TestValidateVolumeCapabilities(t *testing.T) { 633 | 634 | mockCtrl := gomock.NewController(t) 635 | defer mockCtrl.Finish() 636 | provider := test.NewMockVolumeProvider(mockCtrl) 637 | 638 | controller := NewPacketControllerServer(provider) 639 | 640 | for _, testCase := range getVolumeCapabilityTestCases() { 641 | 642 | request := &csi.ValidateVolumeCapabilitiesRequest{ 643 | VolumeCapabilities: testCase.capabilitySet, 644 | VolumeId: providerVolumeID, 645 | } 646 | 647 | provider.EXPECT().Get(providerVolumeID) 648 | resp, err := controller.ValidateVolumeCapabilities(context.TODO(), request) 649 | assert.Nil(t, err) 650 | assert.Equal(t, testCase.packetSupported, resp.Confirmed, testCase.description) 651 | 652 | } 653 | 654 | } 655 | 656 | // min returns the smaller of x or y. 657 | func min(x, y int) int { 658 | if x > y { 659 | return y 660 | } 661 | return x 662 | } 663 | -------------------------------------------------------------------------------- /pkg/driver/driver.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/packethost/csi-packet/pkg/packet" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | const ( 11 | DriverName = "csi.packet.net" 12 | ) 13 | 14 | var ( 15 | server NonBlockingGRPCServer 16 | ) 17 | 18 | // PacketDriver driver for packet cloud 19 | type PacketDriver struct { 20 | name string 21 | nodeID string 22 | endpoint string 23 | config packet.Config 24 | Logger *log.Entry 25 | Attacher Attacher 26 | Mounter Mounter 27 | Initializer Initializer 28 | } 29 | 30 | // NewPacketDriver create a new PacketDriver 31 | func NewPacketDriver(endpoint, nodeID string, config packet.Config) (*PacketDriver, error) { 32 | // if the nodeID was not specified, we retrieve it from metadata 33 | var err error 34 | nid := nodeID 35 | if nid == "" { 36 | md := packet.MetadataDriver{BaseURL: config.MetadataURL} 37 | nid, err = md.GetNodeID() 38 | if err != nil { 39 | return nil, fmt.Errorf("unable to get node ID from metadata: %v", err) 40 | } 41 | } 42 | return &PacketDriver{ 43 | // name https://github.com/container-storage-interface/spec/blob/master/spec.md#getplugininfo 44 | name: DriverName, // this could be configurable, but must match a plugin directory name for kubelet to use 45 | nodeID: nid, 46 | endpoint: endpoint, 47 | config: config, 48 | Logger: log.WithFields(log.Fields{"node": nid, "endpoint": endpoint}), 49 | // default attacher and mounter 50 | Attacher: &AttacherImpl{}, 51 | Mounter: &MounterImpl{}, 52 | Initializer: &InitializerImpl{}, 53 | }, nil 54 | } 55 | 56 | // Run execute 57 | func (d *PacketDriver) Run() { 58 | server = NewNonBlockingGRPCServer() 59 | identity := NewPacketIdentityServer(d) 60 | metadataDriver := packet.MetadataDriver{BaseURL: d.config.MetadataURL} 61 | var controller *PacketControllerServer 62 | if d.config.AuthToken != "" { 63 | p, err := packet.NewPacketProvider(d.config, metadataDriver) 64 | if err != nil { 65 | d.Logger.Fatalf("Unable to create controller %+v", err) 66 | } 67 | controller = NewPacketControllerServer(p) 68 | } 69 | node, err := NewPacketNodeServer(d, &metadataDriver) 70 | if err != nil { 71 | d.Logger.Fatalf("Unable to create node server %+v", err) 72 | } 73 | 74 | d.Logger.Info("Starting server") 75 | server.Start(d.endpoint, 76 | identity, 77 | controller, 78 | node) 79 | server.Wait() 80 | } 81 | 82 | // Stop 83 | func (d *PacketDriver) Stop() { 84 | server.Stop() 85 | } 86 | -------------------------------------------------------------------------------- /pkg/driver/driver_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http/httptest" 7 | "net/url" 8 | "os" 9 | "testing" 10 | 11 | "github.com/google/uuid" 12 | "github.com/kubernetes-csi/csi-test/pkg/sanity" 13 | "github.com/packethost/csi-packet/pkg/packet" 14 | packetServer "github.com/packethost/packet-api-server/pkg/server" 15 | "github.com/packethost/packet-api-server/pkg/store" 16 | "github.com/packethost/packngo" 17 | 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | const ( 22 | socket = "/tmp/csi.sock" 23 | authToken = "AUTH_TOKEN" 24 | projectID = "123456" 25 | facilityID = "EWR1" 26 | nodeName = "node-sanity-test" 27 | driverName = "sanity-test" 28 | ) 29 | 30 | type apiServerError struct { 31 | t *testing.T 32 | } 33 | 34 | func (a *apiServerError) Error(err error) { 35 | a.t.Fatal(err) 36 | } 37 | func TestPacketDriver(t *testing.T) { 38 | // where we will connect 39 | endpoint := "unix://" + socket 40 | // remove any existing one 41 | if err := os.Remove(socket); err != nil && !os.IsNotExist(err) { 42 | t.Fatalf("failed to remove unix domain socket file %s, error: %s", socket, err) 43 | } 44 | defer os.Remove(socket) 45 | 46 | backend := store.NewMemory() 47 | dev, err := backend.CreateDevice(projectID, nodeName, &packngo.Plan{}, &packngo.Facility{}) 48 | if err != nil { 49 | t.Fatalf("error creating device: %v", err) 50 | } 51 | // mock endpoint 52 | fake := packetServer.PacketServer{ 53 | Store: backend, 54 | ErrorHandler: &apiServerError{ 55 | t: t, 56 | }, 57 | MetadataDevice: dev.ID, 58 | } 59 | ts := httptest.NewServer(fake.CreateHandler()) 60 | defer ts.Close() 61 | 62 | url, _ := url.Parse(ts.URL) 63 | urlString := url.String() 64 | 65 | // Setup the full driver and its environment 66 | // normally we care about all of these settings, but since this all is stubbed out, it does not matter 67 | config := packet.Config{ 68 | AuthToken: authToken, 69 | ProjectID: projectID, 70 | FacilityID: facilityID, 71 | BaseURL: &urlString, 72 | MetadataURL: &urlString, 73 | } 74 | driver := &PacketDriver{ 75 | endpoint: endpoint, 76 | name: driverName, 77 | nodeID: dev.ID, 78 | config: config, 79 | Logger: log.WithFields(log.Fields{"node": nodeName, "endpoint": endpoint}), 80 | Attacher: &AttacherMock{ 81 | sessions: map[string]iscsiSession{}, 82 | bindings: map[string]string{}, 83 | maxDevice: 0, 84 | }, 85 | Mounter: &MounterMock{ 86 | bindmounts: map[string]string{}, 87 | blockmounts: map[string]string{}, 88 | }, 89 | Initializer: &InitializerMock{}, 90 | } 91 | defer driver.Stop() 92 | 93 | // run the driver 94 | go driver.Run() 95 | 96 | mntDir, err := ioutil.TempDir("", "mnt") 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | // we remove them now, because sanity expects to be given the parent where to create a directory, and fails when it already exists 101 | os.RemoveAll(mntDir) 102 | defer os.RemoveAll(mntDir) 103 | 104 | mntStageDir, err := ioutil.TempDir("", "mnt-stage") 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | // we remove them now, because sanity expects to be given the parent where to create a directory, and fails when it already exists 109 | os.RemoveAll(mntStageDir) 110 | defer os.RemoveAll(mntStageDir) 111 | 112 | sanityConfig := &sanity.Config{ 113 | TargetPath: mntDir, 114 | StagingPath: mntStageDir, 115 | Address: endpoint, 116 | } 117 | 118 | // call the test suite 119 | sanity.Test(t, sanityConfig) 120 | } 121 | 122 | /***** 123 | mock for attacher (iscsiadm) and mounter commands 124 | *****/ 125 | type iscsiSession struct { 126 | ip string 127 | iqn string 128 | dev string 129 | id string 130 | } 131 | type AttacherMock struct { 132 | sessions map[string]iscsiSession 133 | maxDevice int 134 | bindings map[string]string 135 | } 136 | 137 | func (a *AttacherMock) sessionName(ip, iqn string) string { 138 | return fmt.Sprintf("%s %s", ip, iqn) 139 | } 140 | func (a *AttacherMock) GetScsiID(devicePath string) (string, error) { 141 | for _, v := range a.sessions { 142 | if v.dev == devicePath { 143 | return v.id, nil 144 | } 145 | } 146 | return "", fmt.Errorf("device %s not found", devicePath) 147 | } 148 | func (a *AttacherMock) GetDevice(portal, iqn string) (string, error) { 149 | if v, ok := a.sessions[a.sessionName(portal, iqn)]; ok { 150 | return v.dev, nil 151 | } 152 | return "", fmt.Errorf("device %s %s not found", portal, iqn) 153 | } 154 | func (a *AttacherMock) Discover(ip, initiator string) error { 155 | return nil 156 | } 157 | func (a *AttacherMock) HasSession(ip, iqn string) (bool, error) { 158 | if _, ok := a.sessions[a.sessionName(ip, iqn)]; ok { 159 | return true, nil 160 | } 161 | return false, nil 162 | } 163 | func (a *AttacherMock) Login(ip, iqn string) error { 164 | a.maxDevice++ 165 | a.sessions[a.sessionName(ip, iqn)] = iscsiSession{ 166 | ip: ip, 167 | iqn: iqn, 168 | dev: fmt.Sprintf("/dev/%d", a.maxDevice), // create a device for it 169 | id: uuid.New().String(), 170 | } 171 | return nil 172 | } 173 | func (a *AttacherMock) Logout(ip, iqn string) error { 174 | delete(a.sessions, a.sessionName(ip, iqn)) 175 | return nil 176 | } 177 | func (a *AttacherMock) MultipathReadBindings() (map[string]string, map[string]string, error) { 178 | return a.bindings, map[string]string{}, nil 179 | } 180 | func (a *AttacherMock) MultipathWriteBindings(bindings map[string]string) error { 181 | a.bindings = bindings 182 | return nil 183 | } 184 | 185 | type MounterMock struct { 186 | bindmounts map[string]string // maps target to src 187 | blockmounts map[string]string // maps target to device 188 | } 189 | 190 | func (m *MounterMock) Bindmount(src, target string) error { 191 | m.bindmounts[target] = src 192 | return nil 193 | } 194 | func (m *MounterMock) Unmount(path string) error { 195 | delete(m.bindmounts, path) 196 | delete(m.blockmounts, path) 197 | return nil 198 | } 199 | func (m *MounterMock) MountMappedDevice(device, target string) error { 200 | m.blockmounts[target] = device 201 | return nil 202 | } 203 | func (m *MounterMock) FormatMappedDevice(device string) error { 204 | // we do not do anything here 205 | return nil 206 | } 207 | func (m *MounterMock) GetMappedDevice(device string) (BlockInfo, error) { 208 | return BlockInfo{ 209 | Name: "name", 210 | FsType: "ext4", 211 | Label: "label", 212 | UUID: uuid.New().String(), 213 | Mountpoint: "/mnt/foo", 214 | }, nil 215 | } 216 | 217 | type InitializerMock struct { 218 | } 219 | 220 | func (i *InitializerMock) NodeInit(initiatorName string) error { 221 | return nil 222 | } 223 | -------------------------------------------------------------------------------- /pkg/driver/exec.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // generic execCommand function which logs on error 11 | func execCommand(command string, args ...string) ([]byte, error) { 12 | out, err := exec.Command(command, args...).CombinedOutput() 13 | if err != nil { 14 | log.WithFields(log.Fields{"command": command, "args": strings.Join(args, " "), "out": string(out[:]), "error": err.Error()}).Error("Error") 15 | return nil, err 16 | } 17 | return out, nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/driver/identity.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | csi "github.com/container-storage-interface/spec/lib/go/csi" 5 | "github.com/packethost/csi-packet/pkg/version" 6 | log "github.com/sirupsen/logrus" 7 | "golang.org/x/net/context" 8 | "google.golang.org/grpc/codes" 9 | "google.golang.org/grpc/status" 10 | ) 11 | 12 | var _ csi.IdentityServer = &PacketIdentityServer{} 13 | 14 | // PacketIdentityServer represent the identity server for Packet 15 | type PacketIdentityServer struct { 16 | Driver *PacketDriver 17 | } 18 | 19 | // NewPacketIdentityServer create a new PacketIdentityServer 20 | func NewPacketIdentityServer(driver *PacketDriver) *PacketIdentityServer { 21 | return &PacketIdentityServer{driver} 22 | } 23 | 24 | // GetPluginInfo get information about the plugin 25 | func (packetIdentity *PacketIdentityServer) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) { 26 | log.Infof("PacketIdentityServer.GetPluginInfo called") 27 | 28 | if packetIdentity.Driver.name == "" { 29 | return nil, status.Error(codes.Unavailable, "Driver name not configured") 30 | } 31 | 32 | return &csi.GetPluginInfoResponse{ 33 | Name: packetIdentity.Driver.name, 34 | VendorVersion: version.VERSION, 35 | }, nil 36 | } 37 | 38 | // GetPluginCapabilities get capabilities of the plugin 39 | func (packetIdentity *PacketIdentityServer) GetPluginCapabilities(ctx context.Context, req *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) { 40 | log.Infof("PacketIdentityServer.GetPluginCapabilities called") 41 | return &csi.GetPluginCapabilitiesResponse{ 42 | Capabilities: []*csi.PluginCapability{ 43 | &csi.PluginCapability{ 44 | Type: &csi.PluginCapability_Service_{ 45 | Service: &csi.PluginCapability_Service{ 46 | Type: csi.PluginCapability_Service_CONTROLLER_SERVICE, 47 | }, 48 | }, 49 | }, 50 | }, 51 | }, nil 52 | } 53 | 54 | // Probe probe the identity server 55 | func (packetIdentity *PacketIdentityServer) Probe(ctx context.Context, req *csi.ProbeRequest) (*csi.ProbeResponse, error) { 56 | log.Infof("PacketIdentityServer.Probe called with args: %#v", req) 57 | return &csi.ProbeResponse{}, nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/driver/initializer.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | ) 7 | 8 | const ( 9 | initiatorNameFile = "/etc/iscsi/initiatorname.iscsi" 10 | mpathConfigFile = "/etc/multipath.conf" 11 | mpathConfig = ` 12 | defaults { 13 | polling_interval 3 14 | fast_io_fail_tmo 5 15 | path_selector "round-robin 0" 16 | rr_min_io 100 17 | rr_weight priorities 18 | failback immediate 19 | no_path_retry queue 20 | user_friendly_names yes 21 | } 22 | blacklist { 23 | devnode "^(ram|raw|loop|fd|md|dm-|sr|scd|st)[0-9]*" 24 | devnode "^hd[a-z][[0-9]*]" 25 | devnode "^vd[a-z]" 26 | devnode "^cciss!c[0-9]d[0-9]*[p[0-9]*]" 27 | device { 28 | vendor "Micron" 29 | product ".*" 30 | } 31 | device { 32 | vendor "Intel" 33 | product ".*" 34 | } 35 | device { 36 | vendor "DELL" 37 | product ".*" 38 | } 39 | } 40 | devices { 41 | device { 42 | vendor "DATERA" 43 | product "IBLOCK" 44 | path_grouping_policy group_by_prio 45 | path_checker tur 46 | #checker_timer 5 47 | #prio_callout "/sbin/mpath_prio_alua /dev/%n" 48 | hardware_handler "1 alua" 49 | } 50 | } 51 | ` 52 | ) 53 | 54 | type Initializer interface { 55 | NodeInit(string) error 56 | } 57 | 58 | type InitializerImpl struct { 59 | } 60 | 61 | // NodeInit does all node initialization necessary for iscsi to be configured correctly 62 | func (n *InitializerImpl) NodeInit(initiatorName string) error { 63 | if err := n.SetIscsiInitiator(initiatorName); err != nil { 64 | return err 65 | } 66 | if err := n.ConfigureMultipath(); err != nil { 67 | return err 68 | } 69 | if err := n.RestartServices(); err != nil { 70 | return err 71 | } 72 | return nil 73 | } 74 | 75 | // SetIscsiInitiator sets the name of the iscsi initiator 76 | func (n *InitializerImpl) SetIscsiInitiator(initiatorName string) error { 77 | // get the name of our initiator 78 | // update the file 79 | contents := []byte(fmt.Sprintf("InitiatorName=%s\n", initiatorName)) 80 | err := ioutil.WriteFile(initiatorNameFile, contents, 0644) 81 | if err != nil { 82 | return err 83 | } 84 | return nil 85 | } 86 | 87 | func (n *InitializerImpl) ConfigureMultipath() error { 88 | contents := []byte(mpathConfig) 89 | err := ioutil.WriteFile(mpathConfigFile, contents, 0644) 90 | if err != nil { 91 | return err 92 | } 93 | return nil 94 | } 95 | 96 | // RestartServices should restart services, but we have no way to do that yet 97 | func (n *InitializerImpl) RestartServices() error { 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /pkg/driver/mounter.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "golang.org/x/sys/unix" 10 | 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // represents the lsblk info 15 | type BlockInfo struct { 16 | Name string `json:"name"` 17 | FsType string `json:"fstype"` 18 | Label string `json:"label"` 19 | UUID string `json:"uuid"` 20 | Mountpoint string `json:"mountpoint"` 21 | } 22 | 23 | // represents the lsblk info 24 | type Deviceset struct { 25 | BlockDevices []BlockInfo `json:"blockdevices"` 26 | } 27 | 28 | type Mounter interface { 29 | Bindmount(string, string) error 30 | Unmount(string) error 31 | MountMappedDevice(string, string) error 32 | FormatMappedDevice(string) error 33 | GetMappedDevice(string) (BlockInfo, error) 34 | } 35 | 36 | type MounterImpl struct { 37 | } 38 | 39 | // Methods to format and mount 40 | 41 | func (m *MounterImpl) Bindmount(src, target string) error { 42 | 43 | if _, err := os.Stat(target); err != nil { 44 | if os.IsNotExist(err) { 45 | os.MkdirAll(target, 0755) 46 | } else { 47 | log.Errorf("stat %s, %v", target, err) 48 | return err 49 | } 50 | } 51 | _, err := os.Stat(target) 52 | if err != nil { 53 | log.Errorf("stat %s, %v", target, err) 54 | return err 55 | } 56 | args := []string{"--bind", src, target} 57 | _, err = execCommand("mount", args...) 58 | return err 59 | } 60 | 61 | func (m *MounterImpl) Unmount(path string) error { 62 | err := unix.Unmount(path, 0) 63 | // we are willing to pass on a directory that is not mounted any more 64 | if err != nil && err != unix.EINVAL { 65 | return err 66 | } 67 | return nil 68 | } 69 | 70 | func (m *MounterImpl) MountMappedDevice(device, target string) error { 71 | devicePath := filepath.Join("/dev/mapper/", device) 72 | os.MkdirAll(target, os.ModeDir) 73 | args := []string{"-t", "ext4", "--source", devicePath, "--target", target} 74 | _, err := execCommand("mount", args...) 75 | return err 76 | } 77 | 78 | // ext4 format 79 | func (m *MounterImpl) FormatMappedDevice(device string) error { 80 | devicePath := filepath.Join("/dev/mapper/", device) 81 | args := []string{"-F", devicePath} 82 | fstype := "ext4" 83 | command := "mkfs." + fstype 84 | _, err := execCommand(command, args...) 85 | return err 86 | } 87 | 88 | // get info 89 | func (m *MounterImpl) GetMappedDevice(device string) (BlockInfo, error) { 90 | devicePath := filepath.Join("/dev/mapper/", device) 91 | 92 | // testing issue: must mock out call to Stat as well as to exec.Command 93 | if _, err := os.Stat(devicePath); os.IsNotExist(err) { 94 | return BlockInfo{}, err 95 | } 96 | 97 | // use -J json output so we can parse it into a BlockInfo struct 98 | out, err := execCommand("lsblk", "-J", "-i", "--output", "NAME,FSTYPE,LABEL,UUID,MOUNTPOINT", devicePath) 99 | if err != nil { 100 | return BlockInfo{}, err 101 | } 102 | devices := Deviceset{} 103 | err = json.Unmarshal(out, &devices) 104 | if err != nil { 105 | return BlockInfo{}, err 106 | } 107 | for _, info := range devices.BlockDevices { 108 | if info.Name == device { 109 | return info, nil 110 | } 111 | } 112 | return BlockInfo{}, fmt.Errorf("device %s not found", device) 113 | } 114 | -------------------------------------------------------------------------------- /pkg/driver/node.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "github.com/packethost/csi-packet/pkg/packet" 5 | log "github.com/sirupsen/logrus" 6 | 7 | "github.com/container-storage-interface/spec/lib/go/csi" 8 | 9 | "golang.org/x/net/context" 10 | "google.golang.org/grpc/codes" 11 | "google.golang.org/grpc/status" 12 | ) 13 | 14 | var _ csi.NodeServer = &PacketNodeServer{} 15 | 16 | // PacketNodeServer represents a packet node 17 | type PacketNodeServer struct { 18 | Driver *PacketDriver 19 | MetadataDriver *packet.MetadataDriver 20 | Initialized bool 21 | initiator string 22 | } 23 | 24 | // NewPacketNodeServer create a new PacketNodeServer 25 | func NewPacketNodeServer(driver *PacketDriver, metadata *packet.MetadataDriver) (*PacketNodeServer, error) { 26 | // we do NOT initialize here, since NewPacketNodeServer is called in all cases of this program 27 | // even on a controller 28 | // we wait until our first legitimate call for a Node*() func 29 | return &PacketNodeServer{ 30 | Driver: driver, 31 | MetadataDriver: metadata, 32 | Initialized: false, 33 | }, nil 34 | } 35 | 36 | // NodeStageVolume ~ iscisadmin, multipath, format 37 | func (nodeServer *PacketNodeServer) NodeStageVolume(ctx context.Context, in *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) { 38 | nodeServer.Driver.Logger.Info("NodeStageVolume called") 39 | // validate arguments 40 | // this is the abbreviated name... 41 | volumeName := in.PublishContext["VolumeName"] 42 | if volumeName == "" { 43 | return nil, status.Error(codes.InvalidArgument, "VolumeName unspecified for NodeStageVolume") 44 | } 45 | 46 | // do we know our initiator? 47 | if nodeServer.initiator == "" { 48 | initiatorName, err := nodeServer.MetadataDriver.GetInitiator() 49 | if err != nil { 50 | nodeServer.Driver.Logger.Errorf("NodeGetInfo: metadata error %v", err) 51 | return nil, status.Errorf(codes.Unknown, "metadata error, %s", err.Error()) 52 | } 53 | nodeServer.initiator = initiatorName 54 | } 55 | volumeMetaData, err := nodeServer.MetadataDriver.GetVolumeMetadata(volumeName) 56 | if err != nil { 57 | nodeServer.Driver.Logger.Errorf("NodeStageVolume: %v", err) 58 | return nil, status.Errorf(codes.Unknown, "metadata error, %s", err.Error()) 59 | } 60 | 61 | if len(volumeMetaData.IPs) == 0 { 62 | return nil, status.Errorf(codes.Unknown, "volume %s has no portals", volumeName) 63 | } 64 | 65 | if in.GetVolumeCapability() == nil { 66 | return nil, status.Error(codes.InvalidArgument, "VolumeCapability unspecified for NodeStageVolume") 67 | } 68 | mnt := in.VolumeCapability.GetMount() 69 | // options := mnt.MountFlags 70 | 71 | if mnt.FsType != "" { 72 | if mnt.FsType != "ext4" { 73 | return nil, status.Errorf(codes.InvalidArgument, "fs type %s not supported", mnt.FsType) 74 | } 75 | } 76 | 77 | logger := nodeServer.Driver.Logger.WithFields(log.Fields{ 78 | "volume_id": in.VolumeId, 79 | "volume_name": volumeName, 80 | "staging_target_path": in.StagingTargetPath, 81 | "fsType": mnt.FsType, 82 | "method": "NodeStageVolume", 83 | }) 84 | 85 | // discover and log in to iscsiadmin 86 | for _, ip := range volumeMetaData.IPs { 87 | err = nodeServer.Driver.Attacher.Discover(ip.String(), nodeServer.initiator) // iscsiadm --mode discovery --type sendtargets --portal 10.144.144.226 --discover 88 | if err != nil { 89 | logger.Infof("iscsiadmin discover error, %+v", err) 90 | return nil, status.Errorf(codes.Unknown, "iscsiadmin discover error, %+v", err) 91 | } 92 | err = nodeServer.Driver.Attacher.Login(ip.String(), volumeMetaData.IQN) 93 | if err != nil { 94 | logger.Infof("iscsiadmin login error, %+v", err) 95 | return nil, status.Errorf(codes.Unknown, "iscsiadmin login error, %+v", err) 96 | } 97 | } 98 | 99 | // configure multimap 100 | devicePath, err := nodeServer.Driver.Attacher.GetDevice(volumeMetaData.IPs[0].String(), volumeMetaData.IQN) 101 | if err != nil { 102 | logger.Infof("devicePath error, %+v", err) 103 | return nil, status.Errorf(codes.Unknown, "devicePath error, %+v", err) 104 | } 105 | scsiID, err := nodeServer.Driver.Attacher.GetScsiID(devicePath) 106 | if err != nil { 107 | logger.Infof("scsiID error, path %s, %+v", devicePath, err) 108 | return nil, status.Errorf(codes.Unknown, "scsiIDerror, %+v", err) 109 | } 110 | bindings, discards, err := nodeServer.Driver.Attacher.MultipathReadBindings() 111 | if err != nil { 112 | logger.Infof("readBindings error, %+v", err) 113 | return nil, status.Errorf(codes.Unknown, "readBindings error, %+v", err) 114 | } 115 | bindings[volumeName] = scsiID 116 | err = nodeServer.Driver.Attacher.MultipathWriteBindings(bindings) 117 | if err != nil { 118 | logger.Infof("writeBindings error, %+v", err) 119 | return nil, status.Errorf(codes.Unknown, "writeBindings error, %+v", err) 120 | } 121 | for mappingName := range discards { 122 | multipath("-f", mappingName) 123 | } 124 | // for some reason, you have to do it twice for it to work 125 | multipath(volumeName) 126 | multipath(volumeName) 127 | 128 | check, err := multipath("-ll", devicePath) 129 | logger.Infof("multipath check for %s: %s", devicePath, check) 130 | if check == "" { 131 | logger.Infof("empty multipath check for %s", devicePath) 132 | } 133 | 134 | blockInfo, err := nodeServer.Driver.Mounter.GetMappedDevice(volumeName) 135 | if err != nil { 136 | logger.Infof("getMappedDevice error, %+v", err) 137 | return nil, status.Errorf(codes.Unknown, "getMappedDevice error, %+v", err) 138 | } 139 | if blockInfo.FsType == "" { 140 | err = nodeServer.Driver.Mounter.FormatMappedDevice(volumeName) 141 | if err != nil { 142 | logger.Infof("formatMappedDevice error, %+v", err) 143 | return nil, status.Errorf(codes.Unknown, "formatMappedDevice error, %+v", err) 144 | } 145 | } 146 | 147 | logger.Info("mounting mapped device") 148 | err = nodeServer.Driver.Mounter.MountMappedDevice(volumeName, in.StagingTargetPath) 149 | if err != nil { 150 | logger.Infof("mountMappedDevice error, %v", err) 151 | return nil, status.Errorf(codes.Unknown, "mountMappedDevice error, %+v", err) 152 | } 153 | 154 | logger.Infof("NodeStageVolume complete") 155 | return &csi.NodeStageVolumeResponse{}, nil 156 | } 157 | 158 | // NodeUnstageVolume ~ iscisadmin, multipath 159 | func (nodeServer *PacketNodeServer) NodeUnstageVolume(ctx context.Context, in *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) { 160 | 161 | nodeServer.Driver.Logger.Info("NodeUnstageVolume called") 162 | 163 | if in.VolumeId == "" { 164 | return nil, status.Error(codes.InvalidArgument, "VolumeId unspecified for NodeUnstageVolume") 165 | } 166 | if in.StagingTargetPath == "" { 167 | return nil, status.Error(codes.InvalidArgument, "StagingTargetPath unspecified for NodeUnstageVolume") 168 | } 169 | 170 | volumeID := in.VolumeId 171 | volumeName := packet.VolumeIDToName(volumeID) 172 | 173 | logger := nodeServer.Driver.Logger.WithFields(log.Fields{ 174 | "volume_id": in.VolumeId, 175 | "volume_name": volumeName, 176 | "staging_target_path": in.StagingTargetPath, 177 | "method": "NodeUnstageVolume", 178 | }) 179 | 180 | err := nodeServer.Driver.Mounter.Unmount(in.StagingTargetPath) 181 | if err != nil { 182 | return nil, status.Errorf(codes.Unknown, "unmounting error, %v", err) 183 | } 184 | logger.Infof("Unmounted staging target") 185 | 186 | volumeMetaData, err := nodeServer.MetadataDriver.GetVolumeMetadata(volumeName) 187 | if err != nil { 188 | return nil, status.Errorf(codes.Unknown, "metadata access error, %v ", err) 189 | } 190 | 191 | if len(volumeMetaData.IPs) == 0 { 192 | return nil, status.Errorf(codes.Unknown, "volume %s has no portals", volumeName) 193 | } 194 | 195 | // remove multipath 196 | bindings, discards, err := nodeServer.Driver.Attacher.MultipathReadBindings() 197 | if err != nil { 198 | return nil, status.Errorf(codes.Unknown, "multipath error, %v", err) 199 | } 200 | delete(bindings, volumeName) 201 | err = nodeServer.Driver.Attacher.MultipathWriteBindings(bindings) 202 | if err != nil { 203 | return nil, status.Errorf(codes.Unknown, "multipath error, %v", err) 204 | } 205 | logger.Info("multipath flush") 206 | for mappingName := range discards { 207 | multipath("-f", mappingName) 208 | } 209 | multipath("-f", volumeName) 210 | 211 | for _, ip := range volumeMetaData.IPs { 212 | logger.WithFields(log.Fields{"ip": ip, "iqn": volumeMetaData.IQN}).Info("iscsiadmin logout") 213 | err = nodeServer.Driver.Attacher.Logout(ip.String(), volumeMetaData.IQN) 214 | if err != nil { 215 | return nil, status.Errorf(codes.Unknown, "iscsiadminLogout error, %v", err) 216 | } 217 | } 218 | 219 | logger.Info("NodeUnstageVolume complete") 220 | response := &csi.NodeUnstageVolumeResponse{} 221 | return response, nil 222 | } 223 | 224 | // NodePublishVolume ~ mount 225 | func (nodeServer *PacketNodeServer) NodePublishVolume(ctx context.Context, in *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) { 226 | 227 | nodeServer.Driver.Logger.Info("NodePublishVolume called") 228 | 229 | if in.VolumeId == "" { 230 | return nil, status.Error(codes.InvalidArgument, "VolumeId unspecified for NodeStageVolume") 231 | } 232 | if in.TargetPath == "" { 233 | return nil, status.Error(codes.InvalidArgument, "TargetPath unspecified for NodeStageVolume") 234 | } 235 | if in.StagingTargetPath == "" { 236 | return nil, status.Error(codes.InvalidArgument, "StagingTargetPath unspecified for NodeStageVolume") 237 | } 238 | 239 | logger := nodeServer.Driver.Logger.WithFields(log.Fields{ 240 | "volume_id": in.VolumeId, 241 | "target_path": in.TargetPath, 242 | "staging_target_path": in.StagingTargetPath, 243 | "method": "NodePublishVolume", 244 | }) 245 | 246 | err := nodeServer.Driver.Mounter.Bindmount(in.GetStagingTargetPath(), in.GetTargetPath()) 247 | if err != nil { 248 | return nil, status.Errorf(codes.Unknown, "bind mount error, %+v", err) 249 | } 250 | logger.Info("bind mount complete") 251 | return &csi.NodePublishVolumeResponse{}, nil 252 | } 253 | 254 | // NodeUnpublishVolume ~ unmount 255 | func (nodeServer *PacketNodeServer) NodeUnpublishVolume(ctx context.Context, in *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) { 256 | 257 | nodeServer.Driver.Logger.Info("NodeUnpublishVolume called") 258 | 259 | if in.VolumeId == "" { 260 | return nil, status.Error(codes.InvalidArgument, "VolumeId unspecified for NodeUnpublishVolume") 261 | } 262 | if in.TargetPath == "" { 263 | return nil, status.Error(codes.InvalidArgument, "TargetPath unspecified for NodeUnpublishVolume") 264 | } 265 | 266 | logger := nodeServer.Driver.Logger.WithFields(log.Fields{ 267 | "volume_id": in.VolumeId, 268 | "target_path": in.TargetPath, 269 | "method": "NodePublishVolume", 270 | }) 271 | 272 | err := nodeServer.Driver.Mounter.Unmount(in.GetTargetPath()) 273 | if err != nil { 274 | return nil, status.Errorf(codes.Unknown, "unmount error, %+v", err) 275 | } 276 | logger.Info("unmount complete") 277 | 278 | return &csi.NodeUnpublishVolumeResponse{}, nil 279 | } 280 | 281 | // NodeGetVolumeStats gets the usage stats of the volume 282 | func (nodeServer *PacketNodeServer) NodeGetVolumeStats(ctx context.Context, in *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) { 283 | nodeServer.Driver.Logger.Info("NodeGetVolumeStats called") 284 | // TODO: get info from packet about the usage of this volume 285 | return &csi.NodeGetVolumeStatsResponse{ 286 | Usage: []*csi.VolumeUsage{}, 287 | }, nil 288 | } 289 | 290 | // NodeGetInfo get info for a given node 291 | func (nodeServer *PacketNodeServer) NodeGetInfo(context.Context, *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) { 292 | nodeServer.Driver.Logger.Info("NodeGetInfo called") 293 | // initialize 294 | if !nodeServer.Initialized { 295 | initiatorName, err := nodeServer.MetadataDriver.GetInitiator() 296 | if err != nil { 297 | nodeServer.Driver.Logger.Errorf("NodeGetInfo: metadata error %v", err) 298 | return nil, status.Errorf(codes.Unknown, "metadata error, %s", err.Error()) 299 | } 300 | err = nodeServer.Driver.Initializer.NodeInit(initiatorName) 301 | if err != nil { 302 | nodeServer.Driver.Logger.Errorf("NodeGetInfo: NodeInit error %v", err) 303 | return nil, status.Errorf(codes.Unknown, "NodeInit error, %s", err.Error()) 304 | } 305 | nodeServer.Initialized = true 306 | } 307 | return &csi.NodeGetInfoResponse{ 308 | NodeId: nodeServer.Driver.nodeID, 309 | // MaxVolumesPerNode: 0, 310 | // AccessibleTopology: nil, 311 | }, nil 312 | } 313 | 314 | // NodeGetCapabilities get capabilities of a given node 315 | func (nodeServer *PacketNodeServer) NodeGetCapabilities(ctx context.Context, in *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) { 316 | 317 | nodeServer.Driver.Logger.Info("NodeGetCapabilities called") 318 | // define 319 | nsCapabilitySet := []csi.NodeServiceCapability_RPC_Type{ 320 | csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME, 321 | } 322 | // transform 323 | var nsc []*csi.NodeServiceCapability 324 | for _, nscap := range nsCapabilitySet { 325 | nsc = append(nsc, &csi.NodeServiceCapability{ 326 | Type: &csi.NodeServiceCapability_Rpc{ 327 | Rpc: &csi.NodeServiceCapability_RPC{ 328 | Type: nscap, 329 | }, 330 | }, 331 | }) 332 | } 333 | 334 | return &csi.NodeGetCapabilitiesResponse{ 335 | Capabilities: nsc, 336 | }, nil 337 | } 338 | 339 | // NodeExpandVolume expand a volume 340 | func (nodeServer *PacketNodeServer) NodeExpandVolume(context.Context, *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) { 341 | return nil, status.Error(codes.Unimplemented, "") 342 | } 343 | -------------------------------------------------------------------------------- /pkg/driver/node_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | // 4 | // three steps to mocking a single os/exec.Command call 5 | 6 | // func TestGetMappedDevice(t *testing.T) { 7 | // execCommand = fakeExecLsblk 8 | // defer func() { execCommand = exec.Command }() 9 | 10 | // blockInfo, err := getMappedDevice("md126") 11 | // assert.Nil(t, err) 12 | // assert.NotNil(t, blockInfo) 13 | // assert.Equal(t, "md126", blockInfo.Name) 14 | // assert.Equal(t, "ext4", blockInfo.FsType) 15 | // } 16 | 17 | // func TestMockOfLsblk(*testing.T) { 18 | // if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { 19 | // return 20 | // } 21 | // defer os.Exit(0) 22 | 23 | // lsblkStdout := `{"blockdevices":[{"name":"md126","fstype":"ext4","label":"ROOT","uuid":"afbc32b3-d258-4553-bd1b-4da06768c63f","mountpoint":"/"}]}` 24 | // fmt.Println(lsblkStdout) 25 | // } 26 | 27 | // func fakeExecLsblk(command string, args ...string) *exec.Cmd { 28 | // cs := []string{"-test.run=TestMockOfLsblk", "--", command} 29 | // cs = append(cs, args...) 30 | // cmd := exec.Command(os.Args[0], cs...) 31 | // cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} 32 | // return cmd 33 | // } 34 | -------------------------------------------------------------------------------- /pkg/driver/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The Kubernetes Authors. 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 driver 18 | 19 | import ( 20 | "fmt" 21 | "net" 22 | "os" 23 | "strings" 24 | "sync" 25 | 26 | log "github.com/sirupsen/logrus" 27 | "golang.org/x/net/context" 28 | "google.golang.org/grpc" 29 | 30 | "github.com/container-storage-interface/spec/lib/go/csi" 31 | ) 32 | 33 | // ParseEndpoint parse an endpoint into its parts of protocol and address 34 | func ParseEndpoint(ep string) (string, string, error) { 35 | if strings.HasPrefix(strings.ToLower(ep), "unix://") || strings.HasPrefix(strings.ToLower(ep), "tcp://") { 36 | s := strings.SplitN(ep, "://", 2) 37 | if s[1] != "" { 38 | return s[0], s[1], nil 39 | } 40 | } 41 | return "", "", fmt.Errorf("Invalid endpoint: %v", ep) 42 | } 43 | 44 | func logGRPC(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 45 | 46 | logger := log.WithFields(log.Fields{ 47 | "GRPC.call": info.FullMethod, 48 | "GRPC.request": fmt.Sprintf("%+v", req), 49 | }) 50 | 51 | resp, err := handler(ctx, req) 52 | if err != nil { 53 | logger.Errorf("GRPC error: %v", err) 54 | } else { 55 | logger.Infof("GRPC response: %+v", resp) 56 | } 57 | return resp, err 58 | } 59 | 60 | // NonBlockingGRPCServer Defines Non blocking GRPC server interfaces 61 | type NonBlockingGRPCServer interface { 62 | // Start services at the endpoint 63 | Start(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) 64 | // Waits for the service to stop 65 | Wait() 66 | // Stops the service gracefully 67 | Stop() 68 | // Stops the service forcefully 69 | ForceStop() 70 | } 71 | 72 | // NewNonBlockingGRPCServer create a new NonBlockingGRPCServer 73 | func NewNonBlockingGRPCServer() NonBlockingGRPCServer { 74 | return &nonBlockingGRPCServer{} 75 | } 76 | 77 | // NonBlocking server 78 | type nonBlockingGRPCServer struct { 79 | wg sync.WaitGroup 80 | server *grpc.Server 81 | } 82 | 83 | func (s *nonBlockingGRPCServer) Start(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) { 84 | 85 | s.wg.Add(1) 86 | 87 | go s.serve(endpoint, ids, cs, ns) 88 | 89 | return 90 | } 91 | 92 | func (s *nonBlockingGRPCServer) Wait() { 93 | s.wg.Wait() 94 | } 95 | 96 | func (s *nonBlockingGRPCServer) Stop() { 97 | s.server.GracefulStop() 98 | } 99 | 100 | func (s *nonBlockingGRPCServer) ForceStop() { 101 | s.server.Stop() 102 | } 103 | 104 | func (s *nonBlockingGRPCServer) serve(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) { 105 | 106 | proto, addr, err := ParseEndpoint(endpoint) 107 | if err != nil { 108 | log.Fatal(err.Error()) 109 | } 110 | 111 | if proto == "unix" { 112 | addr = "/" + addr 113 | if err := os.Remove(addr); err != nil && !os.IsNotExist(err) { 114 | log.Fatalf("Failed to remove %s, error: %s", addr, err.Error()) 115 | } 116 | } 117 | 118 | listener, err := net.Listen(proto, addr) 119 | if err != nil { 120 | log.Fatalf("Failed to listen: %v", err) 121 | } 122 | 123 | opts := []grpc.ServerOption{ 124 | grpc.UnaryInterceptor(logGRPC), 125 | } 126 | server := grpc.NewServer(opts...) 127 | s.server = server 128 | 129 | if ids != nil { 130 | csi.RegisterIdentityServer(server, ids) 131 | } 132 | if cs != nil { 133 | csi.RegisterControllerServer(server, cs) 134 | } 135 | if ns != nil { 136 | csi.RegisterNodeServer(server, ns) 137 | } 138 | 139 | logger := log.WithFields(log.Fields{ 140 | "proto": proto, 141 | "address": addr, 142 | }) 143 | 144 | logger.Infof("Listening for connections") 145 | 146 | server.Serve(listener) 147 | s.wg.Done() 148 | } 149 | -------------------------------------------------------------------------------- /pkg/packet/errors.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import "fmt" 4 | 5 | // WrongDeviceAttachmentError error type that volume is attached to a different device 6 | type WrongDeviceAttachmentError struct { 7 | deviceID string 8 | } 9 | 10 | // Error return the error string 11 | func (w WrongDeviceAttachmentError) Error() string { 12 | return fmt.Sprintf("Attached to wrong device: %s", w.deviceID) 13 | } 14 | 15 | // IsWrongDeviceAttachment check if this error is a wrong device attachment 16 | func IsWrongDeviceAttachment(err error) bool { 17 | switch err.(type) { 18 | case *WrongDeviceAttachmentError: 19 | return true 20 | } 21 | return false 22 | } 23 | 24 | // TooManyDevicesAttachedError error type that volume is attached to multiple devices 25 | type TooManyDevicesAttachedError struct { 26 | deviceIDs []string 27 | } 28 | 29 | // Error return the error string 30 | func (t TooManyDevicesAttachedError) Error() string { 31 | return fmt.Sprintf("Attached to multiple devices: %v", t.deviceIDs) 32 | } 33 | 34 | // IsTooManyDevicesAttached check if this error is a too many devices attached error 35 | func IsTooManyDevicesAttached(err error) bool { 36 | switch err.(type) { 37 | case *TooManyDevicesAttachedError: 38 | return true 39 | } 40 | return false 41 | } 42 | 43 | // DeviceStillAttachedError error type that volume still is attached to a device 44 | type DeviceStillAttachedError struct{} 45 | 46 | // Error return the error string 47 | func (d DeviceStillAttachedError) Error() string { 48 | return "Cannot delete when still attached" 49 | } 50 | 51 | // IsDeviceStillAttached check if this error is a device still attached error 52 | func IsDeviceStillAttached(err error) bool { 53 | switch err.(type) { 54 | case *DeviceStillAttachedError: 55 | return true 56 | } 57 | return false 58 | } 59 | -------------------------------------------------------------------------------- /pkg/packet/provider.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | "time" 8 | 9 | "github.com/packethost/packngo" 10 | "github.com/pkg/errors" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | const ( 15 | // ConsumerToken token for packet consumer 16 | ConsumerToken = "csi-packet" 17 | // BillingHourly string to indicate hourly billing 18 | BillingHourly = "hourly" 19 | // volumeInUseMessage message that is returned if volume is in use 20 | volumeInUseMessage = "Cannot detach since volume is actively being used on your server" 21 | ) 22 | 23 | // Config configuration for a volume provider, includes authentication token, project ID and facility ID, and optional override URL to talk to a different packet API endpoint 24 | type Config struct { 25 | AuthToken string `json:"apiKey"` 26 | ProjectID string `json:"projectId"` 27 | FacilityID string `json:"facility-id"` 28 | BaseURL *string `json:"base-url,omitempty"` 29 | MetadataURL *string `json:"metadata-url,omitempty"` 30 | } 31 | 32 | // VolumeProviderPacketImpl the volume provider for Packet 33 | type VolumeProviderPacketImpl struct { 34 | config Config 35 | metadata MetadataDriver 36 | } 37 | 38 | var _ VolumeProvider = &VolumeProviderPacketImpl{} 39 | 40 | // VolumeIDToName convert a volume UUID to its representative name, e.g. "3ee59355-a51a-42a8-b848-86626cc532f0" -> "volume-3ee59355" 41 | func VolumeIDToName(id string) string { 42 | // "3ee59355-a51a-42a8-b848-86626cc532f0" -> "volume-3ee59355" 43 | uuidElements := strings.Split(id, "-") 44 | return fmt.Sprintf("volume-%s", uuidElements[0]) 45 | } 46 | 47 | // NewPacketProvider create a new VolumeProviderPacketImpl from a given Config 48 | func NewPacketProvider(config Config, metadata MetadataDriver) (*VolumeProviderPacketImpl, error) { 49 | if config.AuthToken == "" { 50 | return nil, errors.New("AuthToken not specified") 51 | } 52 | if config.ProjectID == "" { 53 | return nil, errors.New("ProjectID not specified") 54 | } 55 | logger := log.WithFields(log.Fields{"project_id": config.ProjectID}) 56 | logger.Info("Creating provider") 57 | 58 | if config.FacilityID == "" { 59 | facilityCode, err := metadata.GetFacilityCodeMetadata() 60 | if err != nil { 61 | logger.Errorf("Cannot get facility code %v", err) 62 | return nil, errors.Wrap(err, "cannot construct VolumeProviderPacketImpl") 63 | } 64 | c := constructClient(config.AuthToken, config.BaseURL) 65 | facilities, resp, err := c.Facilities.List(&packngo.ListOptions{}) 66 | if err != nil { 67 | if resp.StatusCode == http.StatusForbidden { 68 | return nil, fmt.Errorf("cannot construct VolumeProviderPacketImpl, access denied to search facilities") 69 | } 70 | return nil, errors.Wrap(err, "cannot construct VolumeProviderPacketImpl") 71 | } 72 | for _, facility := range facilities { 73 | if facility.Code != facilityCode { 74 | continue 75 | } 76 | 77 | if !contains(facility.Features, "storage") { 78 | return nil, errors.New("this device's facility does not support storage volumes") 79 | } 80 | 81 | config.FacilityID = facility.ID 82 | logger.WithField("facility_id", facility.ID).Infof("facility found") 83 | break 84 | } 85 | } 86 | 87 | if config.FacilityID == "" { 88 | logger.Errorf("FacilityID not specified and cannot be found") 89 | return nil, fmt.Errorf("FacilityID not specified and cannot be found") 90 | } 91 | 92 | provider := VolumeProviderPacketImpl{config, metadata} 93 | return &provider, nil 94 | } 95 | 96 | func contains(arr []string, str string) bool { 97 | for _, a := range arr { 98 | if a == str { 99 | return true 100 | } 101 | } 102 | 103 | return false 104 | } 105 | 106 | func constructClient(authToken string, baseURL *string) *packngo.Client { 107 | tr := &http.Transport{ 108 | MaxIdleConns: 10, 109 | IdleConnTimeout: 30 * time.Second, 110 | DisableCompression: true, 111 | } 112 | client := &http.Client{Transport: tr} 113 | 114 | // client.Transport = logging.NewTransport("Packet", client.Transport) 115 | if baseURL != nil { 116 | // really should handle error, but packngo does not distinguish now or handle errors, so ignoring for now 117 | client, _ := packngo.NewClientWithBaseURL(ConsumerToken, authToken, client, *baseURL) 118 | return client 119 | } 120 | return packngo.NewClientWithAuth(ConsumerToken, authToken, client) 121 | } 122 | 123 | // Client() returns a new client for accessing Packet's API. 124 | func (p *VolumeProviderPacketImpl) client() *packngo.Client { 125 | return constructClient(p.config.AuthToken, p.config.BaseURL) 126 | } 127 | 128 | // ListVolumes wrap the packet api as an interface method 129 | func (p *VolumeProviderPacketImpl) ListVolumes(options *packngo.ListOptions) ([]packngo.Volume, *packngo.Response, error) { 130 | listOptions := options 131 | if listOptions == nil { 132 | listOptions = &packngo.ListOptions{} 133 | } 134 | return p.client().Volumes.List(p.config.ProjectID, listOptions) 135 | } 136 | 137 | // Get wraps the packet api as an interface method 138 | func (p *VolumeProviderPacketImpl) Get(volumeUUID string) (*packngo.Volume, *packngo.Response, error) { 139 | return p.client().Volumes.Get(volumeUUID, &packngo.GetOptions{Includes: []string{"attachments.volume", "attachments.device"}}) 140 | } 141 | 142 | // Delete wraps the packet api as an interface method 143 | func (p *VolumeProviderPacketImpl) Delete(volumeUUID string) (*packngo.Response, error) { 144 | resp, err := p.client().Volumes.Delete(volumeUUID) 145 | if resp.StatusCode == http.StatusNotFound { 146 | return resp, nil 147 | } 148 | return resp, err 149 | } 150 | 151 | // Create wraps the packet api as an interface method 152 | func (p *VolumeProviderPacketImpl) Create(createRequest *packngo.VolumeCreateRequest) (*packngo.Volume, *packngo.Response, error) { 153 | 154 | createRequest.FacilityID = p.config.FacilityID 155 | 156 | return p.client().Volumes.Create(createRequest, p.config.ProjectID) 157 | } 158 | 159 | // Attach wraps the packet api as an interface method 160 | func (p *VolumeProviderPacketImpl) Attach(volumeID, deviceID string) (*packngo.VolumeAttachment, *packngo.Response, error) { 161 | // if the volume already is attached to a different node, reject it 162 | volume, httpResponse, err := p.client().Volumes.Get(volumeID, &packngo.GetOptions{}) 163 | if err != nil || httpResponse.StatusCode != http.StatusOK { 164 | return nil, httpResponse, errors.Wrap(err, "prechecking existence of volume attachment") 165 | } 166 | // we only allow attaching to one node at a time 167 | switch len(volume.Attachments) { 168 | case 0: 169 | // not attached anywhere, so attach it 170 | return p.client().VolumeAttachments.Create(volumeID, deviceID) 171 | case 1: 172 | // attached to just one node, so it better be is 173 | attachment := volume.Attachments[0] 174 | if attachment.Device.ID == deviceID { 175 | return p.client().VolumeAttachments.Get(attachment.ID, &packngo.GetOptions{}) 176 | } 177 | return nil, nil, WrongDeviceAttachmentError{deviceID: attachment.Device.ID} 178 | default: 179 | // attached to more than one node, that is an error 180 | devices := make([]string, 0) 181 | for _, a := range volume.Attachments { 182 | devices = append(devices, a.Device.ID) 183 | } 184 | return nil, nil, TooManyDevicesAttachedError{deviceIDs: devices} 185 | } 186 | } 187 | 188 | // Detach wraps the packet api as an interface method 189 | func (p *VolumeProviderPacketImpl) Detach(attachmentID string) (*packngo.Response, error) { 190 | response, err := p.client().VolumeAttachments.Delete(attachmentID) 191 | // is this a "volume still attached" error? if so, indicate 192 | if err == nil { 193 | return response, err 194 | } 195 | errResponse, ok := err.(*packngo.ErrorResponse) 196 | if ok && response != nil && response.StatusCode == http.StatusUnprocessableEntity && len(errResponse.Errors) > 0 && strings.HasPrefix(errResponse.Errors[0], volumeInUseMessage) { 197 | return response, &DeviceStillAttachedError{} 198 | } 199 | return response, err 200 | } 201 | 202 | // GetNodes list nodes 203 | func (p *VolumeProviderPacketImpl) GetNodes() ([]packngo.Device, *packngo.Response, error) { 204 | return p.client().Devices.List(p.config.ProjectID, &packngo.ListOptions{}) 205 | } 206 | -------------------------------------------------------------------------------- /pkg/packet/provider_test.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPacketVolumeIDToName(t *testing.T) { 10 | name := VolumeIDToName("3ee59355-a51a-42a8-b848-86626cc532f0") 11 | assert.Equal(t, name, "volume-3ee59355") 12 | } 13 | -------------------------------------------------------------------------------- /pkg/packet/utilities.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | 7 | "github.com/packethost/packngo/metadata" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // { 12 | // "ips": [ 13 | // "10.144.144.144", 14 | // "10.144.145.66" 15 | // ], 16 | // "name": "volume-4b6ed3d8", 17 | // "capacity": { 18 | // "size": "100", 19 | // "unit": "gb" 20 | // }, 21 | // "iqn": "iqn.2013-05.com.daterainc:tc:01:sn:b06f15a423fec58b" 22 | // } 23 | 24 | // CapacityMetaData exists for parsing json metadata 25 | type CapacityMetaData struct { 26 | Size string `json:"size"` 27 | Unit string `json:"unit"` 28 | } 29 | 30 | // VolumeMetadata exists for parsing json metadata 31 | type VolumeMetadata struct { 32 | Name string `json:"name"` 33 | IPs []net.IP `json:"ips"` 34 | Capacity CapacityMetaData `json:"capacity"` 35 | IQN string `json:"iqn"` 36 | } 37 | 38 | type MetadataDriver struct { 39 | BaseURL *string 40 | } 41 | 42 | // GetVolumeMetadata get all the metadata, extract only the parsed volume information, select the desired volume 43 | func (m *MetadataDriver) GetVolumeMetadata(volumeName string) (VolumeMetadata, error) { 44 | empty := VolumeMetadata{} 45 | volumeInfo, err := m.packngoGetPacketVolumeMetadata(volumeName) 46 | if err != nil { 47 | return empty, err 48 | } 49 | 50 | volumeMetaData := VolumeMetadata{ 51 | Name: volumeInfo.Name, 52 | IPs: volumeInfo.IPs, 53 | IQN: volumeInfo.IQN, 54 | Capacity: CapacityMetaData{ 55 | Size: strconv.Itoa(volumeInfo.Capacity.Size), 56 | Unit: volumeInfo.Capacity.Unit, 57 | }, 58 | } 59 | 60 | return volumeMetaData, nil 61 | } 62 | 63 | // GetFacilityCodeMetadata get all the metadata, return the facility code 64 | func (m *MetadataDriver) GetFacilityCodeMetadata() (string, error) { 65 | device, err := m.getMetadata() 66 | if err != nil { 67 | return "", err 68 | } 69 | 70 | return device.Facility, nil 71 | } 72 | 73 | // GetInitiator get the initiator name for iscsi 74 | func (m *MetadataDriver) GetInitiator() (string, error) { 75 | device, err := m.getMetadata() 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | return device.IQN, nil 81 | } 82 | 83 | // GetNodeID get the official packet node ID 84 | func (m *MetadataDriver) GetNodeID() (string, error) { 85 | device, err := m.getMetadata() 86 | if err != nil { 87 | return "", err 88 | } 89 | 90 | return device.ID, nil 91 | } 92 | 93 | // use this when packngo serialization is fixed 94 | // GetVolumeMetadata gets the volume metadata for a named volume 95 | func (m *MetadataDriver) packngoGetPacketVolumeMetadata(volumeName string) (metadata.VolumeInfo, error) { 96 | device, err := m.getMetadata() 97 | if err != nil { 98 | return metadata.VolumeInfo{}, err 99 | } 100 | // logrus.Infof("device metadata: %+v", device) 101 | 102 | var volumeMetaData = metadata.VolumeInfo{} 103 | 104 | for _, vdata := range device.Volumes { 105 | if vdata.Name == volumeName { 106 | volumeMetaData = vdata 107 | break 108 | } 109 | } 110 | 111 | if volumeMetaData.Name == "" { 112 | return metadata.VolumeInfo{}, errors.Errorf("volume %s not found in metadata", volumeName) 113 | } 114 | 115 | return volumeMetaData, nil 116 | } 117 | 118 | // use this when packngo serialization is fixed 119 | // GetFacilityCodeMetadata returns the facility code 120 | func (m *MetadataDriver) packngoGetPacketFacilityCodeMetadata() (string, error) { 121 | 122 | device, err := m.getMetadata() 123 | if err != nil { 124 | return "", err 125 | } 126 | 127 | return device.Facility, nil 128 | } 129 | 130 | func (m *MetadataDriver) getMetadata() (*metadata.CurrentDevice, error) { 131 | if m.BaseURL == nil { 132 | return metadata.GetMetadata() 133 | } 134 | return metadata.GetMetadataFromURL(*m.BaseURL) 135 | } 136 | -------------------------------------------------------------------------------- /pkg/packet/volume.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/packethost/packngo" 8 | ) 9 | 10 | const ( 11 | // Gibi represents a Gibibyte 12 | Gibi int64 = 1024 * 1024 * 1024 13 | // MaxVolumeSizeGi maximum size in Gi 14 | MaxVolumeSizeGi = 10000 15 | // DefaultVolumeSizeGi default size in Gi 16 | DefaultVolumeSizeGi = 100 17 | // MinVolumeSizeGi minimum size in Gi 18 | MinVolumeSizeGi = 10 19 | // VolumePlanStandard standard plan name 20 | VolumePlanStandard = "standard" 21 | // VolumePlanStandardID standard plan ID 22 | VolumePlanStandardID = "87728148-3155-4992-a730-8d1e6aca8a32" 23 | // VolumePlanPerformance performance plan name 24 | VolumePlanPerformance = "performance" 25 | // VolumePlanPerformanceID performance plan ID 26 | VolumePlanPerformanceID = "d6570cfb-38fa-4467-92b3-e45d059bb249" 27 | ) 28 | 29 | // VolumeProvider interface for a volume provider 30 | type VolumeProvider interface { 31 | ListVolumes(*packngo.ListOptions) ([]packngo.Volume, *packngo.Response, error) 32 | Get(volumeID string) (*packngo.Volume, *packngo.Response, error) 33 | Delete(volumeID string) (*packngo.Response, error) 34 | Create(*packngo.VolumeCreateRequest) (*packngo.Volume, *packngo.Response, error) 35 | Attach(volumeID, deviceID string) (*packngo.VolumeAttachment, *packngo.Response, error) 36 | Detach(attachmentID string) (*packngo.Response, error) 37 | GetNodes() ([]packngo.Device, *packngo.Response, error) 38 | } 39 | 40 | // VolumeDescription description of characteristics of a volume 41 | type VolumeDescription struct { 42 | Name string 43 | Created time.Time 44 | } 45 | 46 | // String serialize a VolumeDescription to a string 47 | func (desc VolumeDescription) String() string { 48 | serialized, err := json.Marshal(desc) 49 | if err != nil { 50 | return "" 51 | } 52 | return string(serialized) 53 | } 54 | 55 | // NewVolumeDescription create a new VolumeDescription from a given name 56 | func NewVolumeDescription(name string) VolumeDescription { 57 | return VolumeDescription{ 58 | Name: name, 59 | Created: time.Now(), 60 | } 61 | } 62 | 63 | // ReadDescription read a serialized form of a VolumeDescription into a VolumeDescription struct 64 | func ReadDescription(serialized string) (VolumeDescription, error) { 65 | desc := VolumeDescription{} 66 | err := json.Unmarshal([]byte(serialized), &desc) 67 | return desc, err 68 | } 69 | 70 | // VolumeReady determine if a volume is in the ready state after being created 71 | func VolumeReady(volume *packngo.Volume) bool { 72 | return volume != nil && volume.State == "active" 73 | } 74 | -------------------------------------------------------------------------------- /pkg/test/volume_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: pkg/cloud_provider/volume.go 3 | 4 | // Package mock_cloud_provider is a generated GoMock package. 5 | package test 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | packngo "github.com/packethost/packngo" 12 | ) 13 | 14 | // MockVolumeProvider is a mock of VolumeProvider interface 15 | type MockVolumeProvider struct { 16 | ctrl *gomock.Controller 17 | recorder *MockVolumeProviderMockRecorder 18 | } 19 | 20 | // MockVolumeProviderMockRecorder is the mock recorder for MockVolumeProvider 21 | type MockVolumeProviderMockRecorder struct { 22 | mock *MockVolumeProvider 23 | } 24 | 25 | // NewMockVolumeProvider creates a new mock instance 26 | func NewMockVolumeProvider(ctrl *gomock.Controller) *MockVolumeProvider { 27 | mock := &MockVolumeProvider{ctrl: ctrl} 28 | mock.recorder = &MockVolumeProviderMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockVolumeProvider) EXPECT() *MockVolumeProviderMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // ListVolumes mocks base method 38 | func (m *MockVolumeProvider) ListVolumes(options *packngo.ListOptions) ([]packngo.Volume, *packngo.Response, error) { 39 | ret := m.ctrl.Call(m, "ListVolumes") 40 | ret0, _ := ret[0].([]packngo.Volume) 41 | ret1, _ := ret[1].(*packngo.Response) 42 | ret2, _ := ret[2].(error) 43 | return ret0, ret1, ret2 44 | } 45 | 46 | // ListVolumes indicates an expected call of ListVolumes 47 | func (mr *MockVolumeProviderMockRecorder) ListVolumes() *gomock.Call { 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListVolumes", reflect.TypeOf((*MockVolumeProvider)(nil).ListVolumes)) 49 | } 50 | 51 | // Get mocks base method 52 | func (m *MockVolumeProvider) Get(volumeID string) (*packngo.Volume, *packngo.Response, error) { 53 | ret := m.ctrl.Call(m, "Get", volumeID) 54 | ret0, _ := ret[0].(*packngo.Volume) 55 | ret1, _ := ret[1].(*packngo.Response) 56 | ret2, _ := ret[2].(error) 57 | return ret0, ret1, ret2 58 | } 59 | 60 | // Get indicates an expected call of Get 61 | func (mr *MockVolumeProviderMockRecorder) Get(volumeID interface{}) *gomock.Call { 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockVolumeProvider)(nil).Get), volumeID) 63 | } 64 | 65 | // Delete mocks base method 66 | func (m *MockVolumeProvider) Delete(volumeID string) (*packngo.Response, error) { 67 | ret := m.ctrl.Call(m, "Delete", volumeID) 68 | ret0, _ := ret[0].(*packngo.Response) 69 | ret1, _ := ret[1].(error) 70 | return ret0, ret1 71 | } 72 | 73 | // Delete indicates an expected call of Delete 74 | func (mr *MockVolumeProviderMockRecorder) Delete(volumeID interface{}) *gomock.Call { 75 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockVolumeProvider)(nil).Delete), volumeID) 76 | } 77 | 78 | // Create mocks base method 79 | func (m *MockVolumeProvider) Create(arg0 *packngo.VolumeCreateRequest) (*packngo.Volume, *packngo.Response, error) { 80 | ret := m.ctrl.Call(m, "Create", arg0) 81 | ret0, _ := ret[0].(*packngo.Volume) 82 | ret1, _ := ret[1].(*packngo.Response) 83 | ret2, _ := ret[2].(error) 84 | return ret0, ret1, ret2 85 | } 86 | 87 | // Create indicates an expected call of Create 88 | func (mr *MockVolumeProviderMockRecorder) Create(arg0 interface{}) *gomock.Call { 89 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockVolumeProvider)(nil).Create), arg0) 90 | } 91 | 92 | // Attach mocks base method 93 | func (m *MockVolumeProvider) Attach(volumeID, deviceID string) (*packngo.VolumeAttachment, *packngo.Response, error) { 94 | ret := m.ctrl.Call(m, "Attach", volumeID, deviceID) 95 | ret0, _ := ret[0].(*packngo.VolumeAttachment) 96 | ret1, _ := ret[1].(*packngo.Response) 97 | ret2, _ := ret[2].(error) 98 | return ret0, ret1, ret2 99 | } 100 | 101 | // Attach indicates an expected call of Attach 102 | func (mr *MockVolumeProviderMockRecorder) Attach(volumeID, deviceID interface{}) *gomock.Call { 103 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Attach", reflect.TypeOf((*MockVolumeProvider)(nil).Attach), volumeID, deviceID) 104 | } 105 | 106 | // Detach mocks base method 107 | func (m *MockVolumeProvider) Detach(attachmentID string) (*packngo.Response, error) { 108 | ret := m.ctrl.Call(m, "Detach", attachmentID) 109 | ret0, _ := ret[0].(*packngo.Response) 110 | ret1, _ := ret[1].(error) 111 | return ret0, ret1 112 | } 113 | 114 | // Detach indicates an expected call of Detach 115 | func (mr *MockVolumeProviderMockRecorder) Detach(attachmentID interface{}) *gomock.Call { 116 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Detach", reflect.TypeOf((*MockVolumeProvider)(nil).Detach), attachmentID) 117 | } 118 | 119 | // GetNodes mocks base method 120 | func (m *MockVolumeProvider) GetNodes() ([]packngo.Device, *packngo.Response, error) { 121 | ret := m.ctrl.Call(m, "GetNodes") 122 | ret0, _ := ret[0].([]packngo.Device) 123 | ret1, _ := ret[1].(*packngo.Response) 124 | ret2, _ := ret[2].(error) 125 | return ret0, ret1, ret2 126 | } 127 | 128 | // GetNodes indicates an expected call of GetNodes 129 | func (mr *MockVolumeProviderMockRecorder) GetNodes() *gomock.Call { 130 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodes", reflect.TypeOf((*MockVolumeProvider)(nil).GetNodes)) 131 | } 132 | 133 | // MockNodeVolumeManager is a mock of NodeVolumeManager interface 134 | type MockNodeVolumeManager struct { 135 | ctrl *gomock.Controller 136 | recorder *MockNodeVolumeManagerMockRecorder 137 | } 138 | 139 | // MockNodeVolumeManagerMockRecorder is the mock recorder for MockNodeVolumeManager 140 | type MockNodeVolumeManagerMockRecorder struct { 141 | mock *MockNodeVolumeManager 142 | } 143 | 144 | // NewMockNodeVolumeManager creates a new mock instance 145 | func NewMockNodeVolumeManager(ctrl *gomock.Controller) *MockNodeVolumeManager { 146 | mock := &MockNodeVolumeManager{ctrl: ctrl} 147 | mock.recorder = &MockNodeVolumeManagerMockRecorder{mock} 148 | return mock 149 | } 150 | 151 | // EXPECT returns an object that allows the caller to indicate expected use 152 | func (m *MockNodeVolumeManager) EXPECT() *MockNodeVolumeManagerMockRecorder { 153 | return m.recorder 154 | } 155 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 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 version 18 | 19 | // VERSION is the app-global version string, which should be substituted with a 20 | // real value during build. 21 | var VERSION = "UNKNOWN" 22 | --------------------------------------------------------------------------------