├── .circleci └── config.yml ├── .dockerignore ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── bin ├── .gitignore └── n0version ├── build ├── go │ └── Dockerfile ├── grpc │ ├── go │ │ ├── Dockerfile │ │ ├── entry_point.sh │ │ └── swagger.sh │ └── python │ │ ├── Dockerfile │ │ ├── README.md │ │ └── entry_point.sh ├── n0test │ ├── __init__.py │ ├── ga.py │ ├── ga_test.py │ └── tdt_generator │ │ ├── __init__.py │ │ ├── ga.py │ │ └── ga_test.py └── n0version │ └── main.go ├── deploy └── docker-compose.yml ├── docker-compose.yml ├── docs ├── .gitignore ├── Makefile ├── _static │ └── images │ │ ├── components.svg │ │ ├── dependency_map.svg │ │ ├── go_packages.svg │ │ ├── n0core_network_design.svg │ │ ├── software_architecture.svg │ │ └── synchronous_rpc_system_architecture.svg ├── conf.py ├── developer │ ├── adr │ │ ├── README.rst │ │ ├── asynchronous_mq_system_architecture.md │ │ ├── go_packages.md │ │ ├── lock.md │ │ ├── n0deploy.md │ │ ├── pending.md │ │ ├── synchronous_rpc_system_architecture.md │ │ └── transaction.md │ ├── references.md │ ├── roadmap.md │ ├── tests.md │ └── tools.md ├── index.rst ├── requirements.txt └── user │ ├── overview_n0proto.md │ ├── quick_start.md │ └── usecases │ ├── README.rst │ ├── boot_vm_from_image.md │ ├── boot_vm_from_iso.md │ └── register_blockstorage_as_an_image.md ├── go.mod ├── go.sum ├── n0bff ├── README.md └── main.go ├── n0cli ├── README.md ├── block_storage.go ├── do.go ├── examples │ └── debug-vm │ │ ├── clean.yaml │ │ ├── image-ubuntu1804.yaml │ │ ├── low_level_debug.yaml │ │ ├── test_id_ecdsa │ │ ├── test_id_ecdsa.pub │ │ ├── virtual_machine.yaml │ │ └── virtual_machine_error.yaml ├── grpc_cmd │ ├── gen.go │ ├── grpc.go │ ├── output.go │ └── output_test.go ├── image.go ├── jsonpb.go ├── main.go └── virtual_machine.go ├── n0core ├── .gitignore ├── README.md ├── cmd │ ├── n0core │ │ ├── agent.go │ │ ├── api.go │ │ ├── deploy.go │ │ ├── install.go │ │ └── main.go │ └── n0deploy │ │ ├── README.md │ │ ├── main.go │ │ └── test.n0deploy ├── internal │ └── api │ │ └── iam │ │ └── user │ │ └── api.go └── pkg │ ├── api │ ├── README.md │ ├── deployment │ │ └── image │ │ │ ├── api.go │ │ │ └── api_test.go │ ├── iam │ │ └── user │ │ │ ├── generate_stdapi_test.go │ │ │ └── standard_api.generated.go │ ├── pool │ │ ├── network │ │ │ ├── README.md │ │ │ ├── annotations.go │ │ │ ├── api.go │ │ │ ├── api_mock.go │ │ │ ├── api_test.go │ │ │ ├── budget.go │ │ │ ├── budget_test.go │ │ │ ├── tools.go │ │ │ └── tools_test.go │ │ └── node │ │ │ ├── README.md │ │ │ ├── agent.go │ │ │ ├── annotations.go │ │ │ ├── api.go │ │ │ ├── api_mock.go │ │ │ ├── api_test.go │ │ │ ├── budget.go │ │ │ ├── budget_test.go │ │ │ ├── connection.go │ │ │ ├── tools.go │ │ │ └── tools_test.go │ ├── provisioning │ │ ├── README.md │ │ ├── blockstorage │ │ │ ├── agent.go │ │ │ ├── agent.pb.go │ │ │ ├── agent.proto │ │ │ ├── agent_mock.go │ │ │ ├── agent_test.go │ │ │ ├── annotations.go │ │ │ ├── api.go │ │ │ ├── api_mock.go │ │ │ ├── api_test.go │ │ │ ├── generate_stdapi_test.go │ │ │ └── template_api.generated.go │ │ └── virtualmachine │ │ │ ├── agent.go │ │ │ ├── agent.pb.go │ │ │ ├── agent.proto │ │ │ ├── agent_mock.go │ │ │ ├── agent_test.go │ │ │ ├── annotations.go │ │ │ ├── api.go │ │ │ ├── api_mock.go │ │ │ ├── api_test.go │ │ │ ├── n0test.CreateVirtualMachine.json │ │ │ ├── n0test.CreateVirtualMachine.py │ │ │ ├── n0test.CreateVirtualMachine.seed.json │ │ │ ├── n0test.CreateVirtualMachine_test.go │ │ │ ├── n0test.md │ │ │ └── statik.go │ └── standard_api │ │ ├── error.go │ │ └── generator.go │ ├── datastore │ ├── README.md │ ├── docs.go │ ├── embed │ │ ├── datastore.go │ │ └── datastore_test.go │ ├── errors.go │ ├── etcd │ │ ├── store.go │ │ └── store_test.go │ ├── interface.go │ ├── lock │ │ ├── interface.go │ │ ├── lock.go │ │ ├── lock_test.go │ │ ├── thread.go │ │ ├── util.go │ │ └── util_test.go │ ├── memory │ │ ├── store.go │ │ └── store_test.go │ ├── store │ │ ├── README.md │ │ ├── errors.go │ │ ├── interface.go │ │ ├── leveldb │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ └── sqlite │ │ │ ├── store.go │ │ │ └── store_test.go │ ├── test.pb.go │ └── test.proto │ ├── deploy │ ├── agent.go │ ├── agent_test.go │ ├── local.go │ ├── local_test.go │ ├── remote.go │ └── remote_test.go │ ├── driver │ ├── README.md │ ├── cloudinit │ │ ├── README.md │ │ └── configdrive │ │ │ ├── config.go │ │ │ ├── config_test.go │ │ │ ├── test_id_ecdsa │ │ │ └── test_id_ecdsa.pub │ ├── dnsmasq │ │ ├── README.md │ │ └── command.go │ ├── iproute2 │ │ ├── README.md │ │ ├── bridge.go │ │ ├── bridge_test.go │ │ ├── interface.go │ │ ├── tap.go │ │ ├── tap_test.go │ │ ├── vlan.go │ │ └── vlan_test.go │ ├── iptables │ │ ├── masquerade.go │ │ └── masquerade_test.go │ ├── n0deploy │ │ ├── local.go │ │ ├── parse.go │ │ ├── parse_test.go │ │ └── ssh.go │ ├── qemu │ │ ├── README.md │ │ ├── command.go │ │ ├── command_test.go │ │ ├── misc.go │ │ ├── misc_test.go │ │ ├── network.go │ │ ├── network_test.go │ │ ├── volume.go │ │ └── volume_test.go │ └── qemu_img │ │ ├── README.md │ │ ├── download.go │ │ ├── download_test.go │ │ ├── img.go │ │ └── img_test.go │ └── util │ ├── dag │ ├── dag.go │ └── dag_test.go │ ├── generator │ ├── generator.go │ └── generator_test.go │ ├── grpc │ └── error.go │ ├── net │ ├── ip.go │ ├── ip_test.go │ ├── linux.go │ ├── linux_test.go │ ├── mac.go │ └── mac_test.go │ ├── race │ └── io.go │ └── string │ ├── name.go │ └── name_test.go ├── n0proto.go ├── budget │ └── v0 │ │ ├── compute.pb.go │ │ ├── network_interface.pb.go │ │ └── storage.pb.go ├── configuration │ └── v0 │ │ ├── secret.pb.go │ │ └── ssh_execution.pb.go ├── deployment │ └── v0 │ │ ├── image.pb.go │ │ └── image.pb.gw.go ├── iam │ └── v0 │ │ ├── user.pb.go │ │ └── user.pb.gw.go ├── pkg │ ├── dag │ │ ├── dag.go │ │ └── dag_test.go │ └── transaction │ │ ├── docs.go │ │ ├── transaction.go │ │ └── transaction_test.go ├── pool │ └── v0 │ │ ├── network.pb.go │ │ ├── network.pb.gw.go │ │ ├── node.pb.go │ │ └── node.pb.gw.go └── provisioning │ └── v0 │ ├── block_storage.pb.go │ ├── block_storage.pb.gw.go │ ├── virtual_machine.pb.go │ ├── virtual_machine.pb.gw.go │ ├── virtual_router.pb.go │ └── virtual_router.pb.gw.go ├── n0proto.swagger.json └── n0stack.swagger.json ├── n0proto ├── README.md └── n0stack │ ├── budget │ ├── README.md │ └── v0 │ │ ├── compute.proto │ │ ├── network_interface.proto │ │ └── storage.proto │ ├── deployment │ ├── README.md │ └── v0 │ │ └── image.proto │ ├── iam │ └── v0 │ │ └── user.proto │ ├── pool │ ├── README.md │ └── v0 │ │ ├── network.proto │ │ └── node.proto │ └── provisioning │ ├── README.md │ └── v0 │ ├── block_storage.proto │ ├── virtual_machine.proto │ └── virtual_router.proto └── setup.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | versioning: 4 | docker: 5 | - image: circleci/golang:1.11 6 | working_directory: /go/src/github.com/n0stack/n0stack 7 | master: master 8 | steps: 9 | - checkout 10 | - add_ssh_keys: 11 | fingerprints: 12 | - "b5:40:bd:39:2d:5b:2d:2c:f0:98:98:22:2d:97:56:b9" 13 | - run: 14 | name: git-configure 15 | command: | 16 | git config --global user.name "n0stack bot" 17 | git config --global user.email "h-otter@outlook.jp" 18 | echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 19 | - run: 20 | name: increment version 21 | command: make increment 22 | - run: 23 | name: push 24 | command: | 25 | git add -f VERSION 26 | git commit -m "increment version to $(cat VERSION) [skip ci]" 27 | git push origin master 28 | # push n0core and n0cli to GitHub releases 29 | - run: 30 | name: install ghr 31 | command: | 32 | rm go.sum # TODO: Failするので暫定処置 33 | GO111MODULE=on make vendor 34 | make release-to-github 35 | workflows: 36 | version: 2 37 | all: 38 | jobs: 39 | # - build: 40 | # - test: 41 | # requires: 42 | # - build 43 | - versioning: 44 | # requires: 45 | # - test 46 | filters: 47 | branches: 48 | only: master 49 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !bin/ 4 | !n0cli/ 5 | !n0core/ 6 | !n0proto.go/ 7 | 8 | !VERSION 9 | !LICENSE 10 | !Makefile 11 | !go.mod 12 | !go.sum 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What / 変更点 2 | 3 | ## Why / 変更した理由 4 | 5 | ## How (Optional) / 概要 6 | 7 | ## How affect / 影響範囲 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | vendor/ 15 | artifacts/ 16 | 17 | **/debug 18 | .vscode/ 19 | 20 | # bin/ 21 | .go-build/ 22 | 23 | # using in docker-compose file 24 | **/sandbox 25 | 26 | middleware 27 | **/*.iso 28 | **/*.img 29 | 30 | # Byte-compiled / optimized / DLL files 31 | __pycache__/ 32 | *.py[cod] 33 | *$py.class 34 | 35 | # C extensions 36 | *.so 37 | 38 | # Distribution / packaging 39 | .Python 40 | # build/ 41 | build/bdist.* 42 | build/lib 43 | develop-eggs/ 44 | dist/ 45 | downloads/ 46 | eggs/ 47 | .eggs/ 48 | lib/ 49 | lib64/ 50 | parts/ 51 | sdist/ 52 | var/ 53 | wheels/ 54 | *.egg-info/ 55 | .installed.cfg 56 | *.egg 57 | MANIFEST 58 | 59 | # PyInstaller 60 | # Usually these files are written by a python script from a template 61 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 62 | *.manifest 63 | *.spec 64 | 65 | # Installer logs 66 | pip-log.txt 67 | pip-delete-this-directory.txt 68 | 69 | # Unit test / coverage reports 70 | htmlcov/ 71 | .tox/ 72 | .nox/ 73 | .coverage 74 | .coverage.* 75 | .cache 76 | nosetests.xml 77 | coverage.xml 78 | *.cover 79 | .hypothesis/ 80 | .pytest_cache/ 81 | 82 | # Translations 83 | *.mo 84 | *.pot 85 | 86 | # Django stuff: 87 | *.log 88 | local_settings.py 89 | db.sqlite3 90 | 91 | # Flask stuff: 92 | instance/ 93 | .webassets-cache 94 | 95 | # Scrapy stuff: 96 | .scrapy 97 | 98 | # Sphinx documentation 99 | docs/_build/ 100 | 101 | # PyBuilder 102 | target/ 103 | 104 | # Jupyter Notebook 105 | .ipynb_checkpoints 106 | 107 | # IPython 108 | profile_default/ 109 | ipython_config.py 110 | 111 | # pyenv 112 | .python-version 113 | 114 | # celery beat schedule file 115 | celerybeat-schedule 116 | 117 | # SageMath parsed files 118 | *.sage.py 119 | 120 | # Environments 121 | .env 122 | .venv 123 | env/ 124 | venv/ 125 | ENV/ 126 | env.bak/ 127 | venv.bak/ 128 | 129 | # Spyder project settings 130 | .spyderproject 131 | .spyproject 132 | 133 | # Rope project settings 134 | .ropeproject 135 | 136 | # mkdocs documentation 137 | /site 138 | 139 | # mypy 140 | .mypy_cache/ 141 | .dmypy.json 142 | dmypy.json 143 | 144 | bazel-* 145 | 146 | 147 | # Do not edit version 148 | VERSION 149 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | 5 | 6 | script: 7 | - make all 8 | - make test-small-on-docker 9 | 10 | 11 | notifications: 12 | slack: 13 | secure: Oy+FQmK8Qf1a+yZrQ/UPp0EQ2CjQUlCai4NZUpuzZP2EqGOzTJuwGnwRuIXsFcuhjas4titNGqAA5nU3GqadsfCuHhGF8i1mOAejqqhvbhxiEslr1h340ViM5tBztYRwAAbeuu7nZyTJOFC0sOYiSD9mY4SeGSeCw8kLOkGZxgRjE16gKDkD4CdEbGDhQcQWBiLQoOuJJX/iy5Fp/AhNPMPTEHN89DlptOWAsUMZUCwgkvAYRD9ckNyFx5TCt+AddWc/KHmjmF8RvV6ZUTbVu8eiXQWcYkGe6AeAdRpBXgWGP5Geh+YX3AaDgvei8OSh4B9kcm4VxTRasZ0EDnFuaFnB3sS8Vn0iFsyqxDAyMpglh4PaMQz8sLopB14IPJw19AildHE1j+moTTcnNgabYdwtoSNmzAZ3JDVlGqFAQ1neuY1aLYPoGiIu8Y96OMhTeQfRlqIoUvjxQdaIukltt+4+LSVc6fsnD0yX8YeJ5QPB6e1yKWk7TTIKgC8sD8yf1WDU/yhCUJsK0Drn751dPXViuQdSUS1/vICRAzNNfWmNZG2BNxoREa1tbd5bTPi7YUFJdsNl11y+YQKd589T2d4Nu+ZvuIxuehwGcbtwP+RegzCbOy33RSxRWBNJ5enc9zvCl4g4g99jsCpn5rtFsCOr7mIu6qaMR8u1zZcQmps= 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.11 AS BUILD_GO 2 | 3 | ENV GO111MODULE=on 4 | 5 | COPY . /go/src/github.com/n0stack/n0stack 6 | WORKDIR /go/src/github.com/n0stack/n0stack 7 | 8 | RUN make build-go 9 | 10 | FROM debian:jessie 11 | 12 | RUN apt update \ 13 | && apt install -y openssh-client \ 14 | && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* 15 | 16 | COPY VERSION / 17 | COPY LICENSE / 18 | COPY --from=BUILD_GO /go/src/github.com/n0stack/n0stack/bin/* /usr/local/bin/ 19 | 20 | WORKDIR /root 21 | CMD /bin/bash 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019 Hiroki KAWAHARA 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 134 -------------------------------------------------------------------------------- /bin/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bin/n0version: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0stack/n0stack/172f0417d3025ebd680dd4f7b29ab54fafc5be40/bin/n0version -------------------------------------------------------------------------------- /build/go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.11 2 | 3 | # for n0core 4 | RUN apt-get update \ 5 | && apt-get install -y \ 6 | cloud-image-utils \ 7 | iproute2 \ 8 | qemu-kvm \ 9 | qemu-utils \ 10 | && apt-get clean \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | RUN go get -u golang.org/x/lint/golint 14 | 15 | ENV GO111MODULE=on 16 | ENV DISABLE_KVM=1 17 | -------------------------------------------------------------------------------- /build/grpc/go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.11 AS BUILD 2 | LABEL maintainer="h-otter@outlook.jp" 3 | 4 | RUN go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway \ 5 | && go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger \ 6 | && go get -u github.com/golang/protobuf/protoc-gen-go 7 | 8 | RUN apt update \ 9 | && apt install -y unzip \ 10 | && cd /tmp \ 11 | && wget https://github.com/protocolbuffers/protobuf/releases/download/v3.7.0/protoc-3.7.0-linux-x86_64.zip \ 12 | && unzip protoc-3.7.0-linux-x86_64.zip \ 13 | && mv bin/protoc /usr/bin/ 14 | 15 | RUN cd /tmp \ 16 | && wget https://github.com/go-swagger/go-swagger/releases/download/v0.19.0/swagger_linux_amd64 \ 17 | && chmod 755 swagger_linux_amd64 \ 18 | && mv /tmp/swagger_linux_amd64 /usr/bin/swagger 19 | 20 | WORKDIR /src 21 | COPY entry_point.sh / 22 | COPY swagger.sh / 23 | -------------------------------------------------------------------------------- /build/grpc/go/entry_point.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | 4 | dirs=`find /src -type d | grep -v .git | grep -v test` 5 | 6 | for d in $dirs 7 | do 8 | # 複数のファイルを指定できない 9 | ls -1 $d/*.proto > /dev/null 2>&1 10 | if [ "$?" = "0" ]; then 11 | protoc \ 12 | -I/src \ 13 | -I/tmp/include \ 14 | -I${GOPATH}/src \ 15 | -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway \ 16 | -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ 17 | $* $d/*.proto 18 | fi 19 | done 20 | -------------------------------------------------------------------------------- /build/grpc/go/swagger.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | 4 | dirs=`find /src -type d | grep -v .git | grep -v test` 5 | echo "{}" > $*/n0stack.swagger.json 6 | 7 | for d in $dirs 8 | do 9 | # 複数のファイルを指定できない 10 | ls -1 $d/*.proto > /dev/null 2>&1 11 | if [ "$?" = "0" ]; then 12 | protoc \ 13 | -I/src \ 14 | -I/tmp/include \ 15 | -I${GOPATH}/src \ 16 | -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway \ 17 | -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ 18 | --swagger_out=logtostderr=true,allow_merge=true:/tmp \ 19 | $d/*.proto 20 | swagger mixin -o $*/n0stack.swagger.json /tmp/*.swagger.json $*/n0stack.swagger.json 21 | fi 22 | done 23 | -------------------------------------------------------------------------------- /build/grpc/python/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | LABEL maintainer="h-otter@outlook.jp" 3 | 4 | RUN pip install \ 5 | googleapis-common-protos \ 6 | grpcio-tools 7 | 8 | WORKDIR /src 9 | COPY entry_point.sh / 10 | -------------------------------------------------------------------------------- /build/grpc/python/README.md: -------------------------------------------------------------------------------- 1 | # build-grpc-py 2 | 3 | ## how to build 4 | 5 | ```sh 6 | docker build -t n0stack/build-gprc-py build/grpc/python 7 | docker run -it --rm -v $PWD/n0proto:/src n0stack/build-gprc-py /entry_point.sh 8 | ``` 9 | -------------------------------------------------------------------------------- /build/grpc/python/entry_point.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | 4 | mkdir -p /tmp/build/n0proto 5 | mkdir -p /tmp/dst 6 | cp -r /src/* /tmp/build/n0proto 7 | dirs=`find /tmp/build/n0proto -type d | grep -v .git | grep -v test` 8 | rm -r /dst/* 9 | 10 | for d in $dirs 11 | do 12 | # touch $d/__init__.py 13 | 14 | # 複数のファイルを指定できない 15 | ls -1 $d/*.proto > /dev/null 2>&1 16 | if [ "$?" = "0" ]; then 17 | python \ 18 | -m grpc_tools.protoc \ 19 | -I/usr/local/include \ 20 | -I/tmp/build/n0proto \ 21 | --python_out=/tmp/dst \ 22 | --grpc_python_out=/tmp/dst \ 23 | $* $d/*.proto 24 | fi 25 | done 26 | 27 | mv /tmp/dst/n0proto/* /dst 28 | 29 | dirs=`find /dst -type d | grep -v .git | grep -v test` 30 | for d in $dirs 31 | do 32 | touch $d/__init__.py 33 | done 34 | -------------------------------------------------------------------------------- /build/n0test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0stack/n0stack/172f0417d3025ebd680dd4f7b29ab54fafc5be40/build/n0test/__init__.py -------------------------------------------------------------------------------- /build/n0test/ga_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from n0test.ga import Generation 4 | 5 | 6 | class TestGeneration(unittest.TestCase): 7 | def test___init__(self): 8 | prev = Generation(None, None, _scoring_seed=False) 9 | prev._candidate = [{"foo": "bar"}] 10 | prev._score = [-1] 11 | 12 | gen = Generation(None, None, previous=prev) 13 | self.assertEqual(len(gen._previous), 0) 14 | 15 | def test__random_string(self): 16 | self.assertEqual(len(Generation._random_string(10)), 10) 17 | 18 | def test__random_value(self): 19 | self.assertEqual(type(Generation._random_value(1)), int) 20 | self.assertEqual(type(Generation._random_value(1.)), float) 21 | self.assertEqual(type(Generation._random_value("hoge")), str) 22 | self.assertEqual(type(Generation._random_value([])), list) 23 | self.assertEqual(type(Generation._random_value({})), dict) 24 | 25 | def test_cross(self): 26 | gen = Generation(None, None, [ 27 | { 28 | "foo": "bar", 29 | "hoge": "hoge", 30 | }, 31 | { 32 | "foo": "baa", 33 | "hoge": "hage", 34 | }, 35 | ], _scoring_seed=False) 36 | self.assertNotEqual(gen.cross(), gen.cross(), msg="This test fails probability, try a few times") 37 | 38 | def test_mutation(self): 39 | seed = [{"foo": "bar"}] 40 | gen = Generation(None, None, seed, _scoring_seed=False) 41 | self.assertNotEqual(gen.mutation(), seed) 42 | -------------------------------------------------------------------------------- /build/n0test/tdt_generator/__init__.py: -------------------------------------------------------------------------------- 1 | from n0test.tdt_generator.ga import Generation 2 | -------------------------------------------------------------------------------- /build/n0test/tdt_generator/ga_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from n0test.tdt_generator.ga import Generation 4 | 5 | 6 | class TestGeneration(unittest.TestCase): 7 | def test___init__(self): 8 | prev = Generation(seed={"foo": "bar"} , _scoring_seed=False) 9 | prev._score = [-1] 10 | 11 | gen = Generation(previous_generation=prev) 12 | self.assertEqual(len(gen._previous), 0) 13 | 14 | def test__next_value(self): 15 | seed = [{ 16 | "foo": "bar", 17 | }] 18 | gen = Generation(seed=seed, _scoring_seed=False) 19 | 20 | self.assertEqual(gen._next_value(1, 1), 2) 21 | self.assertEqual(gen._next_value(1, -1), 0) 22 | self.assertEqual(gen._next_value("aaa", 1), "aab") 23 | self.assertEqual(gen._next_value("aab", -1), "aaa") 24 | self.assertEqual(gen._next_value({"key": "aaa"}, 1), {"key": "aab"}) 25 | self.assertEqual(gen._next_value({"key": "aab"}, -1), {"key": "aaa"}) 26 | self.assertEqual(gen._next_value(["aaa"], 1), ["aaa", "aaa"]) 27 | self.assertEqual(gen._next_value(["aaa"], 2), ["aab"]) 28 | self.assertEqual(gen._next_value(["aaa", "aaa"], -1), ["aaa"]) 29 | 30 | def test_cross(self): 31 | seed = { 32 | "foo": "bar", 33 | "hoge": "hoge", 34 | } 35 | gen = Generation(seed=seed, _scoring_seed=False) 36 | 37 | gen._persistence = [ 38 | { 39 | "foo": "bar", 40 | "hoge": "hoge", 41 | }, 42 | { 43 | "foo": "baa", 44 | "hoge": "hage", 45 | }, 46 | ] 47 | self.assertNotEqual(gen.cross()[0], gen._persistence[0], msg="This test fails probability, try a few times") 48 | 49 | def test_mutation(self): 50 | seed = { 51 | "foo": "bar", 52 | } 53 | result = { 54 | "foo": "bas", 55 | } 56 | gen = Generation(seed=seed, _scoring_seed=False) 57 | self.assertEqual(gen.mutate(1), result) 58 | 59 | def test__str_to_dec(self): 60 | seed = [{ 61 | "foo": "bar", 62 | }] 63 | gen = Generation(seed=seed, _scoring_seed=False) 64 | 65 | self.assertEqual(gen._str_to_dec(""), 0) 66 | self.assertEqual(gen._str_to_dec("0"), 1) 67 | self.assertEqual(gen._str_to_dec("00"), 101) 68 | self.assertEqual(gen._str_to_dec("\f"), 100) 69 | 70 | 71 | def test__dec_to_str(self): 72 | seed = [{ 73 | "foo": "bar", 74 | }] 75 | gen = Generation(seed=seed, _scoring_seed=False) 76 | 77 | self.assertEqual(gen._dec_to_str(0), "") 78 | self.assertEqual(gen._dec_to_str(1), "0") 79 | self.assertEqual(gen._dec_to_str(101), "00") 80 | self.assertEqual(gen._dec_to_str(100), "\f") 81 | -------------------------------------------------------------------------------- /build/n0version/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "strconv" 9 | 10 | "github.com/urfave/cli" 11 | ) 12 | 13 | var version = "undefined" 14 | 15 | const VERSION_FILE = "VERSION" 16 | 17 | func IsExists(filename string) bool { 18 | _, err := os.Stat(filename) 19 | return err == nil 20 | } 21 | 22 | func getCurrentVersion(filename string) (uint64, error) { 23 | var v uint64 24 | 25 | if IsExists(filename) { 26 | f, err := os.OpenFile(filename, os.O_RDONLY, 0666) 27 | if err != nil { 28 | return 0, fmt.Errorf("Failed to open %s for read: err=%s", filename, err.Error()) 29 | } 30 | defer f.Close() 31 | 32 | raw, err := ioutil.ReadAll(f) 33 | if err != nil { 34 | return 0, fmt.Errorf("Failed to read %s: err=%s", filename, err.Error()) 35 | } 36 | 37 | v, err = strconv.ParseUint(string(raw), 10, 64) 38 | if err != nil { 39 | return 0, fmt.Errorf("Failed to parse string %s as uint64: err=%s", string(raw), err.Error()) 40 | } 41 | } 42 | 43 | return v, nil 44 | } 45 | 46 | func Print(ctx *cli.Context) error { 47 | v, err := getCurrentVersion(VERSION_FILE) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | fmt.Println(v) 53 | 54 | return nil 55 | } 56 | func Increment(ctx *cli.Context) error { 57 | write := ctx.Bool("write") 58 | 59 | v, err := getCurrentVersion(VERSION_FILE) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | v++ 65 | fmt.Println(v) 66 | 67 | if write { 68 | f, err := os.OpenFile(VERSION_FILE, os.O_WRONLY|os.O_CREATE, 0666) 69 | if err != nil { 70 | return fmt.Errorf("Failed to open %s for write: err=%s", VERSION_FILE, err.Error()) 71 | } 72 | defer f.Close() 73 | 74 | s := strconv.FormatUint(v, 10) 75 | if _, err := f.Write([]byte(s)); err != nil { 76 | return fmt.Errorf("Failed to write %s: err=%s", VERSION_FILE, err.Error()) 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func main() { 84 | app := cli.NewApp() 85 | app.Name = "versioning" 86 | app.Version = version 87 | // app.Usage = "" 88 | 89 | app.Commands = []cli.Command{ 90 | { 91 | Name: "increment", 92 | Usage: "versioning increment -write", 93 | Action: Increment, 94 | Flags: []cli.Flag{ 95 | cli.BoolFlag{ 96 | Name: "write", 97 | }, 98 | }, 99 | }, 100 | { 101 | Name: "print", 102 | Usage: "versioning print -write", 103 | Action: Print, 104 | }, 105 | } 106 | 107 | log.SetFlags(log.Lshortfile) 108 | // log.SetOutput(ioutil.Discard) 109 | 110 | if err := app.Run(os.Args); err != nil { 111 | fmt.Fprintf(os.Stderr, "Failed to command: %v\n", err.Error()) 112 | os.Exit(1) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /deploy/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | api: 4 | image: n0stack/n0stack 5 | command: 6 | - /usr/local/bin/n0core 7 | - serve 8 | - api 9 | ports: 10 | - "8080:8080" 11 | - "20180:20180" 12 | volumes: 13 | - /var/lib/n0core/api:/var/lib/n0core 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | api: 4 | image: debian:buster 5 | volumes: 6 | - ./bin/:/n0stack 7 | - ./sandbox/n0core:/var/lib/n0core 8 | command: 9 | - /n0stack/n0core 10 | - serve 11 | - api 12 | ports: 13 | - "8080:8080" 14 | - "20180:20180" 15 | 16 | swagger: 17 | image: swaggerapi/swagger-ui 18 | volumes: 19 | - ./n0proto.swagger.json/n0stack.swagger.json:/usr/share/nginx/html/n0stack.swagger.json 20 | environment: 21 | API_URL: n0stack.swagger.json 22 | # BASE_URL: swagger 23 | 24 | bff: 25 | image: debian:buster 26 | volumes: 27 | - ./bin/:/n0stack 28 | command: 29 | - /n0stack/n0bff 30 | - serve 31 | - bff 32 | ports: 33 | - "8000:8080" 34 | links: 35 | - api 36 | - swagger 37 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/developer/adr/README.rst: -------------------------------------------------------------------------------- 1 | Architectural Decision Records 2 | ############################## 3 | 4 | Status 5 | ====== 6 | 7 | - proposed 8 | - accepted 9 | - deprecated 10 | 11 | Translate to English 12 | ==================== 13 | 14 | I will do when I remember. :pray: 15 | 16 | List 17 | ==== 18 | 19 | .. toctree:: 20 | :maxdepth: 1 21 | :glob: 22 | 23 | * 24 | -------------------------------------------------------------------------------- /docs/developer/adr/asynchronous_mq_system_architecture.md: -------------------------------------------------------------------------------- 1 | # Asynchronous Messaging Queue System Architecture 2 | 3 | ||| 4 | |--|--| 5 | | Status | deprecated | 6 | 7 | ## Context 8 | 9 | OpenStackの問題点を考えた際に、非常に規模が大きいことが問題であると考えていた。 10 | よって、全体的なアーキテクチャを考える際に、OpenStackを小さいスケールにすれば良いはずである。 11 | 12 | ## Decision 13 | 14 | 以上の経緯から、OpenStackのシステムアーキテクチャを踏襲し、ユーザにはHTTPで非同期APIを提供したうえで、コンポーネント間の通信をMessaging Queueで通信することを考えた。 15 | やろうと思ったことは [これ](https://www.slideshare.net/TakeshiTake/n0stack-81419210) を参照してほしい。 16 | 17 | ## Consequences 18 | 19 | ### Pros 20 | 21 | 実際に運用できたわけではないため、よくわからないが一般的に言われている以下のようなメリットあるだろう。 22 | 23 | - 簡単にコンポーネントをスケールされることができる 24 | - コンポーネント間を粗結合にすることができる 25 | 26 | ### Cons 27 | 28 | まず、MQを運用と構築することが難しい。 29 | KafkaやPulsarなどは非常に大規模なミドルウェアであり、真面目に運用しようとすると多くのコストがかかる。 30 | つまり、n0stackを構築することや運用することが非常に難しくなることを示しており、少なくともすべての基盤となるIaaS部分で使うべきではないと判断した。 31 | 32 | また、MQは多くのコンポーネントを運用するために利用ことで真価を発揮する。 33 | しかし、n0stackは構築などを簡単にすることを目指しているため、コンポーネントは少なくしたいと考えている。 34 | そのため、MQを採用しても得られるメリットが少ない。 35 | 36 | くわえて、Exactly onceでキューが送られることが保証されるわけではないため、イベントがべき等になるように設計する必要がある。 37 | Virtual Machineの再起動などクラウド基盤ではイベントを正しく処理できなければならない。 38 | それらの設計方法は知見としてあまり共有されておらず、トピックなどの使い方もよくわからず学習コストが高かった。 39 | というか、自分がいつまで立ってもいい感じに設計できなかったため諦めた。 40 | 41 | ## Reference 42 | 43 | - https://www.slideshare.net/TakeshiTake/n0stack-81419210 44 | -------------------------------------------------------------------------------- /docs/developer/adr/go_packages.md: -------------------------------------------------------------------------------- 1 | # n0core packages 2 | 3 | ||| 4 | |--|--| 5 | | Status | accepted | 6 | 7 | ## Context 8 | 9 | - 上位レイヤのパッケージの依存関係を明確にすることで、開発を効率的に行うことができると考えている 10 | 11 | ## Decision 12 | 13 | 以下のように区分する。 14 | 15 | ![](../../_static/images/go_packages.svg) 16 | 17 | ### n0core/pkg/api 18 | 19 | - API の実装を書く 20 | 21 | ### n0core/pkg/datastore 22 | 23 | - データの永続化、ロック 24 | 25 | ### n0core/pkg/driver 26 | 27 | - 外部依存や副作用があるようなモジュール 28 | 29 | ### n0core/pkg/util 30 | 31 | - 外部依存や副作用がなくてみんなで共通で使えるモジュール 32 | 33 | ### n0core/pkg/deploy 34 | 35 | - バイナリをデプロイするなどの処理を書く 36 | 37 | ### n0proto.go/* 38 | 39 | - n0protoでgRPC定義されたものを、 `make build-n0proto` で自動生成されたもの 40 | 41 | ### n0proto.go/pkg/transaction 42 | 43 | - 処理のトランザクションを管理するモジュール 44 | - TODO: n0core/pkg/util に移す 45 | 46 | ## Consequences 47 | 48 | - 適宜更新 49 | -------------------------------------------------------------------------------- /docs/developer/adr/lock.md: -------------------------------------------------------------------------------- 1 | # Lock about update process 2 | 3 | ||| 4 | |--|--| 5 | | Status | accepted | 6 | 7 | ## Context 8 | 9 | - 同じオブジェクトに対する更新系のエンドポイントが同時に実行された場合、多くの問題が発生する 10 | - 両方処理された場合、あとに終了する操作のみがDBに反映され、実体が二つ存在することになる 11 | - 一貫性がないため、同じIPがスケジューリングされたりする 12 | - 一貫性と整合性を保証する必要がある 13 | - TODO: どの強さのものなのかは少し勉強してからかく 14 | 15 | ## Decision 16 | 17 | - `github.com/n0stack/n0stack/n0core/pkg/datastore/lock` に実装を行った 18 | - apiは更新系のエンドポイント開始時に `Datastore.Lock(key string)` をかける 19 | - ロックをかけることで一貫性を保証 20 | - ロックがかかっていないものはdatastoreの更新系を実行できないようにブロック 21 | - 実装の間違いに気づきやすいようにした 22 | - panicにしても良さそう? 23 | - ロックに失敗した場合即時返していたが、 `ReserveStorage` などのエンドポイントのエラーレートが非常に高くなってしまったので、ロックがかかるまで一定時間リトライするものを実装した 24 | - Createはロックできない時点で他の人が作っているはずで、ユーザーの不備なはずなのですぐ返す 25 | - Deleteは少し危険な気がするので安全側に倒すなら待たない 26 | - 拡張メソッドは基本的に他のユーザーがロックをかけている可能性があるので、待つ 27 | - 参照系に関しては制約をかけていない 28 | - 高いパフォーマンスが優先されると考えたため 29 | - 少し古いデータを見せられても致命的な問題が起こるとは考えにくい 30 | - DBの機能を使わなかった理由は、今後n0core以外のデーモンを消していくつもりであるため 31 | - n0coreはすべての起点であるため、依存を可能な限り減らしたい 32 | 33 | ## Consequences 34 | 35 | - 適宜更新 36 | 37 | ## Reference 38 | 39 | - #115 40 | -------------------------------------------------------------------------------- /docs/developer/adr/n0deploy.md: -------------------------------------------------------------------------------- 1 | # n0deploy 2 | 3 | ||| 4 | |--|--| 5 | | Status | accepted | 6 | 7 | ## Context 8 | 9 | - アプリケーションのデプロイは検証環境と本番環境を可能な限り近づけるべきである 10 | - 検証環境で動いても本番環境では動かない、またはその逆が発生する確率が非常に高く、それは開発効率を著しく低下させる 11 | - VMのデプロイ手法を再現可能な状態で管理するにはまだ課題がある 12 | - DockerfileでVMにデプロイする場合、VMはミュータブルであるためビルドキャッシュを有効化することが難しく、ゼロから環境を構築するため必要があるため継続的に開発するには遅い 13 | - AnsibleでVMにデプロイすることが一般的だが記述量が増える傾向にあるため、小さなアプリケーションにはコストがかかりすぎ、大きなアプリケーションは記述量が多すぎてメンテナンスできないという問題があった 14 | 15 | ## Decision 16 | 17 | - Dockerfileを参考に `RUN` と `COPY` の機能を実装することで記述量を削減 18 | - 処理を二つに分割することで、継続的に開発を行うことへの足かせを減らす 19 | - Bootstrap: IPの設定や周辺サービスなど環境を構築する事前状態を定義する 20 | - Deploy: 開発しているものを適用する 21 | - つまり `Bootstrap * 1 + Deploy * N` を適用することで開発を行い、 `Bootstrap * 1 + Deploy * 1` を適用することで本番環境に展開する 22 | 23 | ### 文法 24 | 25 | ``` 26 | BOOTSTRAP # optional 27 | RUN apt update \ 28 | && apt upgrade -y \ 29 | && apt install -y ... 30 | 31 | DEPLOY 32 | COPY some_app/ /opt/some_app 33 | ``` 34 | 35 | ## Consequences 36 | 37 | - 適宜更新 38 | -------------------------------------------------------------------------------- /docs/developer/adr/pending.md: -------------------------------------------------------------------------------- 1 | # Pending State 2 | 3 | ||| 4 | |--|--| 5 | | Status | accepted | 6 | 7 | ## Context 8 | 9 | APIが障害になった場合、どこまで処理を行ったかわからず、不整合の原因になると考えられる。特に、VirtualMachineやBlockStorageなど実体の操作が伴うものはレスポンスまでの時間が長いため、API障害の影響を受けやすいと考えられる。 10 | 11 | ## Decision 12 | 13 | - VirtualMachineやBlockStorageのCreateなど実体の操作が伴うものは最初に `PENDING` ステートに設定 14 | - `PENDING` ステートのものは更新を行えないようにする 15 | 16 | これによって、APIの故障によって処理が止まってものは `PENDING` ステートによって操作がロックされ、不整合の拡大を抑制することができる。管理者は手動で不整合が起きていないか確認を行い、復旧することで正常性を維持する。 17 | 18 | ### Example in BlockStorage 19 | 20 | - 作成の場合 21 | - `PENDING` で保存 22 | - 失敗した場合は削除 23 | 24 | ```go 25 | func (a *BlockStorageAPI) CheckAndLock(tx *transaction.Transaction, bs *pprovisioning.BlockStorage) error { 26 | prev := &pprovisioning.BlockStorage{} 27 | if err := a.dataStore.Get(bs.Name, prev); err != nil { 28 | log.Printf("[WARNING] Failed to get data from db: err='%s'", err.Error()) 29 | return grpcutil.WrapGrpcErrorf(codes.Internal, "Failed to get '%s' from db, please retry or contact for the administrator of this cluster", bs.Name) 30 | } else if prev.Name != "" { 31 | return grpcutil.WrapGrpcErrorf(codes.AlreadyExists, "BlockStorage '%s' is already exists", bs.Name) 32 | } 33 | 34 | bs.State = pprovisioning.BlockStorage_PENDING 35 | if err := a.dataStore.Apply(bs.Name, bs); err != nil { 36 | return grpcutil.WrapGrpcErrorf(codes.Internal, "Failed to apply data for db: err='%s'", err.Error()) 37 | } 38 | tx.PushRollback("free optimistic lock", func() error { 39 | return a.dataStore.Delete(bs.Name) 40 | }) 41 | 42 | return nil 43 | } 44 | ``` 45 | 46 | - 更新の場合 47 | - `PENDING` になってないか確認 48 | - `PENDING` で保存 49 | - 失敗した場合は前のステートに変更 50 | 51 | ```go 52 | func (a *BlockStorageAPI) GetAndLock(tx *transaction.Transaction, name string) (*pprovisioning.BlockStorage, error) { 53 | bs := &pprovisioning.BlockStorage{} 54 | if err := a.dataStore.Get(name, bs); err != nil { 55 | log.Printf("[WARNING] Failed to get data from db: err='%s'", err.Error()) 56 | return nil, grpcutil.WrapGrpcErrorf(codes.Internal, "Failed to get '%s' from db, please retry or contact for the administrator of this cluster", name) 57 | } else if bs.Name == "" { 58 | return nil, grpcutil.WrapGrpcErrorf(codes.NotFound, "") 59 | } 60 | 61 | if bs.State == pprovisioning.BlockStorage_PENDING { 62 | return nil, grpcutil.WrapGrpcErrorf(codes.FailedPrecondition, "BlockStorage '%s' is pending", name) 63 | } 64 | 65 | current := bs.State 66 | bs.State = pprovisioning.BlockStorage_PENDING 67 | if err := a.dataStore.Apply(bs.Name, bs); err != nil { 68 | return nil, grpcutil.WrapGrpcErrorf(codes.Internal, "Failed to apply data for db: err='%s'", err.Error()) 69 | } 70 | bs.State = current 71 | tx.PushRollback("free optimistic lock", func() error { 72 | return a.dataStore.Apply(bs.Name, bs) 73 | }) 74 | 75 | return bs, nil 76 | } 77 | ``` 78 | 79 | ## Consequences 80 | 81 | - `PENDING` のものが多くなってくると運用に耐えられなくなると考えられるので、実際に動かしながら確認 82 | - networkにも組み込んでしまったが本来はいらない 83 | - 本来は[これ](lock)の目的で実装していたが、全く効果がなかったので理由が変更になった 84 | -------------------------------------------------------------------------------- /docs/developer/adr/synchronous_rpc_system_architecture.md: -------------------------------------------------------------------------------- 1 | # Synchronous RPC System Architecture 2 | 3 | ||| 4 | |--|--| 5 | | Status | accepted | 6 | 7 | ## Context 8 | 9 | [Asynchronous Messaging Queue System Architecture](asynchronous_mq_system_architecture.md) の反省点としては、MQのような難しすぎる概念を導入しても手に負えず、構築を簡単にするには構成を簡単にする必要があることを学んだ。 10 | 11 | また、Kubernetesはコンテナ管理基盤としてCRDインターフェイスを採用し、ステートレスなりソースの管理に長けている。 12 | しかし、CRDではVirtual Machineの起動、再起動、シャットダウンなどを表現することは難しく、ステートフルなリソースを管理するためにはRPCインターフェイスが必要であると考えた。 13 | 14 | ## Decision 15 | 16 | - シンプルな構成にするため、同期的に処理を伝播していく 17 | - APIでユーザーからのリクエストを受理し、APIが各ノードのAgentに指示をだす 18 | - [Designing Distributed Systems](https://azure.microsoft.com/en-us/resources/designing-distributed-systems/en-us/) でいうScatter/Gatherパターンであり、ミドルウェアは永続化を行うデータベースだけとなる 19 | - gRPCインターフェイスを作る 20 | 21 | ![](../../_static/images/synchronous_rpc_system_architecture.svg) 22 | 23 | ### [Transaction](transaction.md) 24 | 25 | 同期的に実行するため、一つのエンドポイントで多くの処理を行い、失敗する可能性も高い。 26 | よって、原子性 (Atomicity) を保証するために操作をロールバックする仕組みを実装した。 27 | 28 | ### [Resource Operation Lock System](lock.md) 29 | 30 | リソース操作の独立性 (Isolation) を保証するために操作を開始するときにロックする仕組みを実装した。 31 | 32 | ### [Pending State](pending.md) 33 | 34 | 操作が障害などによって中断されるなど、リソースがRPCの操作からではどうしようもなくなったことを示す状態を定義した。 35 | 36 | ## Consequences 37 | 38 | ICTSC 2018で600台近くのVirtual Machineを管理した。詳細は [こちら](https://www.slideshare.net/h-otter/n0stack-in-ictsc-2018) 。 39 | 40 | ### Pros 41 | 42 | - RPCに成功したか、失敗したか、PENDINGステートのゴミが出来上がるかの三択 43 | - リソースの作成は理論上一番はやい構成 44 | - 構成要素が少なく、構築が簡単 45 | 46 | ### Cons 47 | 48 | - CopyBlockImageなど遅いエンドポイントで待たされる 49 | - 遅いエンドポイントではセッションが切れたりユーザーが中断することでPENDINGステートのものが生成される可能性が高い 50 | - エンドポイントごとに処理時間の差が大きいため、APIプロセスの負荷が偏る可能性が高い 51 | 52 | ## Reference 53 | 54 | - https://www.slideshare.net/h-otter/n0stack-in-ictsc-2018 55 | - https://www.slideshare.net/h-otter/n0stack-122135453 56 | -------------------------------------------------------------------------------- /docs/developer/adr/transaction.md: -------------------------------------------------------------------------------- 1 | # Transaction for update process 2 | 3 | ||| 4 | |--|--| 5 | | Status | accepted | 6 | 7 | ## Context 8 | 9 | - n0stackはgRPCを使って粗結合に実装しているため、マイクロサービスと同様の問題を抱えている 10 | - 特に途中で処理が失敗したときにもとに戻す必要がある (原子性) 11 | 12 | ## Decision 13 | 14 | - `github.com/n0stack/n0stack/n0proto.go/pkg/transaction` に実装した 15 | - `Transaction` に行った操作の逆の関数をpush指定いき、失敗したときにその関数をシーケンシャルに逆に呼んでいくことでロールバックを実現する 16 | 17 | ## Consequences 18 | 19 | - 適宜更新 20 | - 正直 `github.com/n0stack/n0stack/n0proto.go/pkg/transaction` の場所は失敗したと思っている 21 | - このトランザクションログを他のAPIと共有できれば、障害時にも強くなりそうだが現状思いつかない 22 | -------------------------------------------------------------------------------- /docs/developer/references.md: -------------------------------------------------------------------------------- 1 | # References 2 | 3 | - [自作クラウド基盤 n0stack と ソフトウェア開発の気持ち](https://www.slideshare.net/h-otter/n0stack-122135453) 4 | -------------------------------------------------------------------------------- /docs/developer/roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | ## 2019 Mar. 4 | 5 | 月末にブログなどを公開するなどして多くの人に見てもらえる環境まで整備したい 6 | 7 | - [ ] クラスタの再構成が必要になりそうな変更を完了する 8 | - [ ] etcdへ依存をなくしてデプロイをさらに簡単にする 9 | - [ ] n0cliの充実,生成自動化 10 | - [ ] 可能ならユーザードキュメントの充実 11 | 12 | ## 2019 Apr. 13 | 14 | - [ ] 開発環境の整備,各種テストの自動化など 15 | - [ ] implement n0bff 16 | - [ ] sshリソースによるデプロイの自動化 17 | 18 | ## 2019 May 19 | 20 | - [ ] 各種ミドルウェアリソースの試験導入,mysql,k8sなど 21 | 22 | -------------------------------------------------------------------------------- /docs/developer/tests.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | The principles about tests on n0stack. 4 | 5 | ## Test size 6 | 7 | - according to https://testing.googleblog.com/2010/12/test-sizes.html 8 | 9 | ### small 10 | 11 | - unit test about logic 12 | - integration test about side effect 13 | - without side effect, for example... 14 | - persistent data 15 | - control middleware 16 | - 副作用は agent に固まっているので、 agent だけモックすることで... 17 | - ロジックの結合テストを small にて行える 18 | - agent からロジックを消すことで分散耐性を向上 19 | - モックの開発工数を減らせる 20 | 21 | #### Goal 22 | 23 | - coverage n0core/pkg/api without agent > 70 % 24 | - coverage n0core/pkg/api with agent > 50 % 25 | - coverage n0core/pkg/datastore/memory > 70 % 26 | 27 | ### medium 28 | 29 | - integration test about side effect on standalone 30 | - gRPC fuzzing about logic 31 | 32 | #### Goal 33 | 34 | - coverage n0core/pkg/api > 70 % 35 | - coverage n0core/pkg/datastore/etcd > 80 % 36 | - coverage n0core/pkg/driver > 60 % 37 | 38 | ### large 39 | 40 | - E2E 41 | 42 | ## TODO 43 | 44 | - [x] 現状のテストが通るようにする 45 | - [x] 各 API のモックの作成と差し替え 46 | - [x] Agent からロジックの切り出し 47 | - [x] Agent のモック作成 48 | - [x] medium -> small に 49 | - [ ] API のテストを書いていく 50 | -------------------------------------------------------------------------------- /docs/developer/tools.md: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | ## Circle CI 4 | 5 | - バージョンのインクリメント 6 | 7 | ## Travis CI 8 | 9 | - test-small と一応すべてのビルドができるか確認している 10 | 11 | ## Go Report Card 12 | 13 | - Go の静的解析に用いている 14 | 15 | ## CODE CLIMATE 16 | 17 | - とりあえずやっているが、Golangだけの現状では大して効果がなく、適当に残してあるだけ 18 | - JS などのコンポーネントもできると思うので、そのときに改めて考える 19 | - protobuf の自動生成されたコードは除外設定を行っている 20 | 21 | ## FOSSA 22 | 23 | - ライセンスの確認を行っており、パスするようにする 24 | - etcd と zap (logger) を Ignore の設定をしている 25 | - 一般的に使われており、深さ 1 では問題ないため暫定処置 26 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. n0stack documentation master file, created by 2 | sphinx-quickstart on Sun Feb 17 02:00:07 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | n0stack documentation 8 | =================================== 9 | 10 | The n0stack is a simple cloud provider using gRPC. 11 | 12 | Description 13 | =========== 14 | 15 | The n0stack is... 16 | 17 | - a cloud provider. 18 | - You can use some features: booting VMs, managing networks and so on (see also /n0proto.) 19 | - simple. 20 | - There are shortcode and fewer options. 21 | - using gRPC. 22 | - A unified interface increase reusability. 23 | - able to be used as library and framework. 24 | - You can concentrate to develop your logic by sharing libraries and frameworks for middleware, test, and deployment. 25 | 26 | Motivation 27 | ========== 28 | 29 | Cloud providers have various forms depending on users. 30 | This problem has been solved with many options and add-ons (e.g. OpenStack configuration file is very long.) 31 | However, it is difficult to adapt to the application by options, then it is necessary to read or rewrite long abstracted codes. 32 | Therefore, I thought that it would be better to code on your hands from beginning. 33 | 34 | There are some problems to develop cloud providers from scratch: no library, software quality, man-hour, and deployment. 35 | The n0stack wants to solve such problems. 36 | 37 | .. ## Demo 38 | 39 | .. TODO: READMEからインポート、現時点では相対パスやバッジなどが面倒 40 | .. .. mdinclude:: ../README.md 41 | 42 | .. toctree:: 43 | :caption: User Documentation 44 | :maxdepth: 1 45 | :glob: 46 | 47 | user/quick_start.md 48 | user/* 49 | user/usecases/README.rst 50 | 51 | .. toctree:: 52 | :caption: Developer Documentation 53 | :maxdepth: 1 54 | :glob: 55 | 56 | developer/adr/README.rst 57 | developer/* 58 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme 3 | recommonmark 4 | m2r 5 | -------------------------------------------------------------------------------- /docs/user/overview_n0proto.md: -------------------------------------------------------------------------------- 1 | # Overview about n0proto 2 | 3 | The n0proto is gRPC definitions for all of n0stack API. 4 | 5 | ## Resources 6 | 7 | ![](../_static/images/dependency_map.svg) 8 | 9 | ### Budget 10 | 11 | Budget define data structure about resource budget: CPU, Memory, IP address, MAC address, storage, and so on. 12 | 13 | Budget はリソースを表すデータ構造である。CPUやメモリなどが含まれる。 14 | 15 | ### Pool 16 | 17 | Pool ensure Budgets. 18 | 19 | Pool は Budget を払い出す。 20 | 21 | #### Node 22 | 23 | - 物理的なサーバ 24 | - CPU、メモリ、ストレージを払い出す 25 | 26 | #### Network 27 | 28 | - 仮想的なネットワーク 29 | - IPアドレスやMACアドレスを払い出す 30 | 31 | ### Provisioning 32 | 33 | Provisioning create virtual resources on ensured budget. 34 | 35 | Poolから予約されたリソースで仮想的なリソースを作り出す。 36 | 37 | #### BlockStorage 38 | 39 | - NodeのStorageから仮想的なブロックストレージを作り出す 40 | - 中身はQcow2ファイル 41 | 42 | 1. Nodeの`ReserveStorage / ScheduleStorage`でストレージを確保 43 | 2. Nodeにインストールされたエージェントを操作するなどして、実態を作成 44 | 45 | #### VirtualMachine 46 | 47 | - NodeのCompute(CPUとメモリ)からVMを作り出す 48 | - この時BlockStorageと、Networkに接続するNetworkInterface(MACアドレスとIPアドレス)を接続することができる 49 | 50 | 1. Nodeの`ReserveCompute / ScheduleCompute`でCPUとメモリを確保 51 | 2. 接続するBlockStorageを`SetInuseBlockStorage`で確保 52 | 3. 接続するNetworkに対して`ReserveNetworkInterface`でMACアドレスとIPアドレスを確保 53 | 4. Nodeにインストールされたエージェントを操作するなどして、実態を作成 54 | 55 | ### Deployment 56 | 57 | Deployment abstract Provisioning operations. 58 | 59 | DeploymentはProvisioningを抽象的にすることでわかりやすくするものである。 60 | 61 | #### Image 62 | 63 | - BlockStorageを抽象化する 64 | - Imageに登録されたBlockStorageからBlockStorageを生成することができる 65 | - 一般にはImageでBlockStorageを生成し、生成したBlockStorageからVMを起動することでOpenstackのGlanceのような使い方ができる ([detail](usecases/boot_vm_with_image.md)) 66 | 67 | 70 | 71 | ## Naming Conventions about API 72 | 73 | ### Standard fields 74 | 75 | #### name 76 | 77 | - Unique key 78 | 79 | #### annotations 80 | 81 | - Field that stores implementation-dependent values 82 | 83 | #### [Resource type]_name 84 | 85 | - Reference to resources 86 | 87 | #### *s 88 | 89 | - List field 90 | 91 | ### Standard methods 92 | 93 | #### List 94 | #### Get 95 | #### Create / Apply 96 | #### Update 97 | #### Delete 98 | -------------------------------------------------------------------------------- /docs/user/quick_start.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | ## n0cli 4 | 5 | The n0cli is a CLI tool to call n0stack gRPC APIs. 6 | 7 | ### Installation 8 | 9 | with docker 10 | 11 | ```sh 12 | docker pull n0stack/n0stack 13 | docker run -it --rm -v /usr/local/bin:/dst n0stack/n0stack cp /usr/local/bin/n0cli /dst/ 14 | ``` 15 | 16 | ### Usage 17 | 18 | - See also command help. 19 | 20 | ```sh 21 | $ n0cli --api-endpoint=$api_ip:20180 get node 22 | { 23 | "nodes": [ 24 | { 25 | "name": "vm-host1", 26 | "annotations": { 27 | "github.com/n0stack/n0stack/n0core/agent_version": "52" 28 | }, 29 | "address": "192.168.122.10", 30 | "serial": "Specified", 31 | "cpu_milli_cores": 1000, 32 | "memory_bytes": "1033236480", 33 | "storage_bytes": "107374182400", 34 | "unit": 1, 35 | "state": "Ready", 36 | "reserved_computes": { 37 | "debug_ipv6": { 38 | "annotations": { 39 | "n0core/provisioning/virtual_machine/virtual_machine/reserved_by": "debug_ipv6" 40 | }, 41 | "request_cpu_milli_core": 10, 42 | "limit_cpu_milli_core": 1000, 43 | "request_memory_bytes": "536870912", 44 | "limit_memory_bytes": "536870912" 45 | } 46 | }, 47 | "reserved_storages": { 48 | "debug-ipv6-network": { 49 | "annotations": { 50 | "n0core/provisioning/block_storage/reserved_by": "debug-ipv6-network" 51 | }, 52 | "request_bytes": "1073741824", 53 | "limit_bytes": "10737418240" 54 | }, 55 | "debug_ipv6_network": { 56 | "annotations": { 57 | "n0core/provisioning/block_storage/reserved_by": "debug_ipv6_network" 58 | }, 59 | "request_bytes": "1073741824", 60 | "limit_bytes": "10737418240" 61 | }, 62 | "ubuntu-1804": { 63 | "annotations": { 64 | "n0core/provisioning/block_storage/reserved_by": "ubuntu-1804" 65 | }, 66 | "request_bytes": "1073741824", 67 | "limit_bytes": "10737418240" 68 | } 69 | } 70 | } 71 | ] 72 | } 73 | ``` 74 | 75 | ### Examples 76 | 77 | See also [Usecases](usecases/README.rst). 78 | -------------------------------------------------------------------------------- /docs/user/usecases/README.rst: -------------------------------------------------------------------------------- 1 | Usecases 2 | ######## 3 | 4 | List 5 | ==== 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | :glob: 10 | 11 | * 12 | -------------------------------------------------------------------------------- /docs/user/usecases/boot_vm_from_iso.md: -------------------------------------------------------------------------------- 1 | # Boot VirtualMachine from ISO 2 | 3 | ## Fetch and register Ubuntu 18.04 Cloud Images 4 | 5 | ```yaml 6 | FetchISO: 7 | type: BlockStorage 8 | action: FetchBlockStorage 9 | args: 10 | name: cloudimage-ubuntu-1804 11 | annotations: 12 | n0core/provisioning/block_storage/request_node_name: vm-host1 13 | request_bytes: 1073741824 # 1GiB 14 | limit_bytes: 10737418240 # 10GiB 15 | source_url: https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img 16 | 17 | ApplyNetwork: 18 | type: Network 19 | action: ApplyNetwork 20 | args: 21 | name: test-network 22 | ipv4_cidr: 192.168.0.0/24 23 | annotations: 24 | n0core/provisioning/virtual_machine/vlan_id: "100" 25 | 26 | CreateVirtualMachine: 27 | type: VirtualMachine 28 | action: CreateVirtualMachine 29 | args: 30 | name: test-vm 31 | annotations: 32 | n0core/provisioning/virtual_machine/request_node_name: vm-host1 33 | request_cpu_milli_core: 10 34 | limit_cpu_milli_core: 1000 35 | request_memory_bytes: 1073741824 # 1GiB 36 | limit_memory_bytes: 1073741824 # 1GiB 37 | block_storage_names: 38 | - cloudimage-ubuntu-1804 39 | nics: 40 | - network_name: test-network 41 | ipv4_address: 192.168.0.1 42 | # cloud-config related options: 43 | login_username: n0user 44 | ssh_authorized_keys: 45 | - ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBITowPn2Ol1eCvXN5XV+Lb6jfXzgDbXyEdtayadDUJtFrcN2m2mjC1B20VBAoJcZtSYkmjrllS06Q26Te5sTYvE= testkey 46 | depends_on: 47 | - CreateBlockStorage 48 | 49 | # You need to set password for user to login via console (not set if default) 50 | OpenConsole: 51 | type: VirtualMachine 52 | action: OpenConsole 53 | args: 54 | name: test-vm 55 | depends_on: 56 | - CreateVirtualMachine 57 | ``` 58 | 59 | Then, you can login virtual machine via ssh by `n0user` user using key below: 60 | 61 | ``` 62 | -----BEGIN EC PRIVATE KEY----- 63 | MHcCAQEEIBAQh+adEg/rjqj9qLE0jI4EqV8kZFDzWTASAwvx6HWdoAoGCCqGSM49 64 | AwEHoUQDQgAEhOjA+fY6XV4K9c3ldX4tvqN9fOANtfIR21rJp0NQm0Wtw3abaaML 65 | UHbRUECglxm1JiSaOuWVLTpDbpN7mxNi8Q== 66 | -----END EC PRIVATE KEY----- 67 | ``` 68 | 69 | (Ubuntu 18.04 Cloud Image doesn't allow password login to ssh configured above, so you need set password if need to access via VNC console) 70 | 71 | ## Inverse action 72 | 73 | ```yaml 74 | Delete_test-vm: 75 | type: VirtualMachine 76 | action: DeleteVirtualMachine 77 | args: 78 | name: test-vm 79 | 80 | Delete_blockstorage: 81 | type: BlockStorage 82 | action: DeleteBlockStorage 83 | args: 84 | name: cloudimage-ubuntu-1804 85 | depends_on: 86 | - Delete_test-vm 87 | 88 | Delete_test-network: 89 | type: Network 90 | action: DeleteNetwork 91 | args: 92 | name: test-network 93 | depends_on: 94 | - Delete_test-vm 95 | ``` 96 | -------------------------------------------------------------------------------- /docs/user/usecases/register_blockstorage_as_an_image.md: -------------------------------------------------------------------------------- 1 | # Register blockstorage as an Image 2 | 3 | You can manage blockstorages by registering to image, versioning blockstorage with tag. 4 | 5 | ```yaml 6 | FetchISO: 7 | type: BlockStorage 8 | action: FetchBlockStorage 9 | args: 10 | name: cloudimage-ubuntu-1804 11 | annotations: 12 | n0core/provisioning/block_storage/request_node_name: vm-host1 13 | request_bytes: 1073741824 # 1GiB 14 | limit_bytes: 10737418240 # 10GiB 15 | source_url: https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img 16 | 17 | ApplyImage: 18 | type: Image 19 | action: ApplyImage 20 | args: 21 | name: cloudimage-ubuntu 22 | 23 | RegisterBlockStorage: 24 | type: Image 25 | action: RegisterBlockStorage 26 | args: 27 | image_name: cloudimage-ubuntu 28 | block_storage_name: cloudimage-ubuntu-1804 29 | tags: 30 | - latest 31 | - "18.04" 32 | depends_on: 33 | - ApplyImage 34 | ``` 35 | 36 | ## Generate BlockStorage from Image 37 | 38 | ```yaml 39 | GenerateBlockStorage: 40 | type: Image 41 | action: GenerateBlockStorage 42 | args: 43 | image_name: cloudimage-ubuntu 44 | tag: "18.04" 45 | block_storage_name: test-blockstorage 46 | annotations: 47 | n0core/provisioning/block_storage/request_node_name: vm-host1 48 | request_bytes: 1073741824 49 | limit_bytes: 10737418240 50 | ``` 51 | 52 | ## Delete image 53 | 54 | ```yaml 55 | Remove_cloudimage-ubuntu: 56 | type: Image 57 | action: DeleteImage 58 | args: 59 | name: cloudimage-ubuntu 60 | depends_on: 61 | - Delete_test-vm 62 | ``` 63 | 64 | ## Delete image (detailed) 65 | 66 | ```yaml 67 | Untag_1804_from_cloudimage-ubuntu: 68 | type: Image 69 | action: UntagImage 70 | args: 71 | name: cloudimage-ubuntu 72 | tag: "18.04" 73 | depends_on: 74 | - Delete_test-vm 75 | 76 | Untag_latest_from_cloudimage-ubuntu: 77 | type: Image 78 | action: UntagImage 79 | args: 80 | name: cloudimage-ubuntu 81 | tag: latest 82 | depends_on: 83 | - Delete_test-vm 84 | 85 | Unregister_cloudimage-ubuntu-1804-from-cloudimage-ubuntu: 86 | type: Image 87 | action: UnregisterBlockStorage 88 | args: 89 | image_name: cloudimage-ubuntu: 90 | block_storage_name: cloudimage-ubuntu-1804 91 | depends_on: 92 | - Untag_1804_from_cloudimage-ubuntu 93 | - Untag_latest_from_cloudimage-ubuntu 94 | 95 | Remove_cloudimage-ubuntu: 96 | type: Image 97 | action: DeleteImage 98 | args: 99 | name: cloudimage-ubuntu 100 | depends_on: 101 | - Unregister_cloudimage-ubuntu-1804-from-cloudimage-ubuntu 102 | 103 | Remove_cloudimage-ubuntu-1804: 104 | type: BlockStorage 105 | action: DeleteBlockStorage 106 | args: 107 | name: cloudimage-ubuntu-1804 108 | depends_on: 109 | - Unregister_cloudimage-ubuntu-1804-from-cloudimage-ubuntu 110 | ``` 111 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/n0stack/n0stack 2 | 3 | require ( 4 | code.cloudfoundry.org/bytefmt v0.0.0-20180906201452-2aa6f33b730c 5 | github.com/StackExchange/wmi v0.0.0-20181212234831-e0a55b97c705 // indirect 6 | github.com/cenkalti/backoff v2.1.1+incompatible 7 | github.com/coreos/etcd v3.3.12+incompatible 8 | github.com/coreos/go-iptables v0.4.1 9 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e 10 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 11 | github.com/digitalocean/go-libvirt v0.0.0-20190419173705-5ea6f2a136d8 // indirect 12 | github.com/digitalocean/go-qemu v0.0.0-20181112162955-dd7bb9c771b8 13 | github.com/fatih/color v1.7.0 14 | github.com/go-ole/go-ole v1.2.4 // indirect 15 | github.com/gogo/protobuf v1.2.1 16 | github.com/golang/protobuf v1.3.1 17 | github.com/google/go-cmp v0.2.0 18 | github.com/gorilla/websocket v1.4.0 // indirect 19 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 20 | github.com/grpc-ecosystem/grpc-gateway v1.8.5 21 | github.com/hashicorp/hcl2 v0.0.0-20190618163856-0b64543c968c 22 | github.com/jinzhu/gorm v1.9.4 23 | github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c 24 | github.com/kr/fs v0.1.0 // indirect 25 | github.com/labstack/echo v3.3.10+incompatible 26 | github.com/labstack/gommon v0.2.8 // indirect 27 | github.com/mattn/go-colorable v0.1.1 // indirect 28 | github.com/mattn/go-isatty v0.0.7 // indirect 29 | github.com/mattn/go-runewidth v0.0.4 // indirect 30 | github.com/mattn/go-sqlite3 v1.10.0 31 | github.com/olekukonko/tablewriter v0.0.1 32 | github.com/pkg/errors v0.8.1 33 | github.com/pkg/sftp v1.10.0 34 | github.com/rakyll/statik v0.1.6 35 | github.com/satori/go.uuid v1.2.0 36 | github.com/shirou/gopsutil v2.18.12+incompatible 37 | github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect 38 | github.com/syndtr/goleveldb v1.0.0 39 | github.com/urfave/cli v1.20.0 40 | github.com/valyala/fasttemplate v1.0.1 // indirect 41 | github.com/vishvananda/netlink v1.0.0 42 | github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc // indirect 43 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 44 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb 45 | google.golang.org/grpc v1.20.1 46 | gopkg.in/yaml.v2 v2.2.2 47 | ) 48 | -------------------------------------------------------------------------------- /n0bff/README.md: -------------------------------------------------------------------------------- 1 | # n0bff 2 | 3 | BFF(Backends for Frontend) of n0stack API. This provide features: API gateway, authentication, authorization and so on. 4 | 5 | - BFFs need high scalability. 6 | - I wanted centralized control of authentication and authorization. 7 | 8 | ## How to develop 9 | 10 | ### build 11 | 12 | ``` 13 | make build-n0bff 14 | ``` 15 | 16 | ### Test run 17 | 18 | ``` 19 | make up 20 | ``` 21 | 22 | - This task up the n0bff container after build n0bff. 23 | -------------------------------------------------------------------------------- /n0bff/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/http/httputil" 9 | "net/url" 10 | "os" 11 | 12 | "github.com/labstack/echo" 13 | "github.com/labstack/echo/middleware" 14 | pdeployment "github.com/n0stack/n0stack/n0proto.go/deployment/v0" 15 | 16 | "github.com/grpc-ecosystem/grpc-gateway/runtime" 17 | "github.com/urfave/cli" 18 | "google.golang.org/grpc" 19 | 20 | ppool "github.com/n0stack/n0stack/n0proto.go/pool/v0" 21 | pprovisioning "github.com/n0stack/n0stack/n0proto.go/provisioning/v0" 22 | ) 23 | 24 | var version = "undefined" 25 | 26 | func main() { 27 | app := cli.NewApp() 28 | app.Name = "n0core" 29 | app.Version = version 30 | app.Usage = "The n0stack cluster manager" 31 | app.EnableBashCompletion = true 32 | 33 | app.Commands = []cli.Command{ 34 | { 35 | Name: "serve", 36 | Usage: "Serve daemons", 37 | Subcommands: []cli.Command{ 38 | { 39 | Name: "bff", 40 | Usage: "Daemon which provide bff for n0stack API", 41 | Action: ServeBFF, 42 | Flags: []cli.Flag{ 43 | cli.StringFlag{ 44 | Name: "etcd-endpoints", 45 | }, 46 | cli.StringFlag{ 47 | // interfaceからも取れるようにしたい 48 | Name: "bind-address", 49 | Value: "0.0.0.0", 50 | }, 51 | cli.IntFlag{ 52 | Name: "bind-port", 53 | Value: 20180, 54 | }, 55 | }, 56 | }, 57 | }, 58 | }, 59 | } 60 | 61 | log.SetFlags(log.Llongfile | log.Ltime | log.Lmicroseconds) 62 | 63 | if err := app.Run(os.Args); err != nil { 64 | fmt.Fprintf(os.Stderr, "Failed to start process, err:%s\n", err.Error()) 65 | os.Exit(1) 66 | } 67 | } 68 | 69 | func ServeBFF(c *cli.Context) error { 70 | ctx := context.Background() 71 | ctx, cancel := context.WithCancel(ctx) 72 | defer cancel() 73 | 74 | mux := runtime.NewServeMux() 75 | opts := []grpc.DialOption{grpc.WithInsecure()} 76 | api := "api:20180" 77 | 78 | // とりあえず動くようにした。 79 | if err := ppool.RegisterNetworkServiceHandlerFromEndpoint(ctx, mux, api, opts); err != nil { 80 | return err 81 | } 82 | if err := pprovisioning.RegisterBlockStorageServiceHandlerFromEndpoint(ctx, mux, api, opts); err != nil { 83 | return err 84 | } 85 | if err := pprovisioning.RegisterVirtualMachineServiceHandlerFromEndpoint(ctx, mux, api, opts); err != nil { 86 | return err 87 | } 88 | if err := pdeployment.RegisterImageServiceHandlerFromEndpoint(ctx, mux, api, opts); err != nil { 89 | return err 90 | } 91 | if err := ppool.RegisterNodeServiceHandlerFromEndpoint(ctx, mux, api, opts); err != nil { 92 | return err 93 | } 94 | 95 | n0core := &url.URL{ 96 | Scheme: "http", 97 | Host: "api:8080", 98 | } 99 | swagger := &url.URL{ 100 | Scheme: "http", 101 | Host: "swagger:8080", 102 | } 103 | // /n0core にプロキシ 104 | e := echo.New() 105 | e.Use(middleware.Logger()) 106 | e.Use(middleware.Recover()) 107 | e.GET("/api/*", echo.WrapHandler(mux)) 108 | // websocket proxy ができてない 109 | e.GET("/n0core/*", echo.WrapHandler(httputil.NewSingleHostReverseProxy(n0core))) 110 | e.GET("/swagger/*", echo.WrapHandler(http.StripPrefix("/swagger", httputil.NewSingleHostReverseProxy(swagger)))) 111 | 112 | log.Printf("[INFO] Started BFF: version=%s", version) 113 | return e.Start("0.0.0.0:8080") 114 | } 115 | -------------------------------------------------------------------------------- /n0cli/README.md: -------------------------------------------------------------------------------- 1 | # n0cli 2 | 3 | CLI for n0stack API. 4 | 5 | 6 | 7 | ## Usage 8 | 9 | See also command help. 10 | 11 | ``` 12 | % bin/n0cli -h 13 | NAME: 14 | n0cli - the n0stack CLI application 15 | 16 | USAGE: 17 | n0cli [global options] command [command options] [arguments...] 18 | 19 | VERSION: 20 | 28 21 | 22 | COMMANDS: 23 | get Get resource if set resource name, List resources if not set 24 | delete Delete resource 25 | do Do DAG tasks (Detail n0stack/pkg/dag) 26 | help, h Shows a list of commands or help for one command 27 | 28 | GLOBAL OPTIONS: 29 | --api-endpoint value (default: "localhost:20180") [$N0CLI_API_ENDPOINT] 30 | --help, -h show help 31 | --version, -v print the version 32 | 33 | ``` 34 | 35 | ``` 36 | % bin/n0cli get -h 37 | NAME: 38 | n0cli get - Get resource if set resource name, List resources if not set 39 | 40 | USAGE: 41 | n0cli get [resource type] [resource name] 42 | ``` 43 | 44 | ``` 45 | % bin/n0cli delete -h 46 | NAME: 47 | n0cli delete - Delete resource 48 | 49 | USAGE: 50 | n0cli delete [resource type] [resource name] 51 | 52 | % bin/n0cli do -h 53 | NAME: 54 | n0cli do - Do DAG tasks (Detail n0stack/pkg/dag) 55 | 56 | USAGE: 57 | n0cli do [file name] 58 | 59 | DESCRIPTION: 60 | 61 | ## File format 62 | 63 | --- 64 | task_name: 65 | type: Network 66 | action: GetNetwork 67 | args: 68 | name: test-network 69 | depend_on: 70 | - dependency_task_name 71 | ignore_error: true 72 | dependency_task_name: 73 | type: ... 74 | --- 75 | 76 | - task_name 77 | - 任意の名前をつけ、ひとつのリクエストに対してユニークなものにする 78 | - type 79 | - gRPC メッセージを指定する 80 | - VirtualMachine や virtual_machine という形で指定できる 81 | - action 82 | - gRPC の RPC を指定する 83 | - GetNetwork など定義のとおりに書く 84 | - args 85 | - gRPC の RPCのリクエストを書く 86 | - depend_on 87 | - DAG スケジューリングに用いられる 88 | - task_name を指定する 89 | - ignore_error 90 | - タスクでエラーが発生しても継続する 91 | ``` 92 | 93 | ## Environment 94 | 95 | - Ubuntu 18.04 LTS (Bionic Beaver) 96 | 97 | ## How to build 98 | 99 | ``` 100 | cd .. 101 | make build-n0cli 102 | ``` 103 | -------------------------------------------------------------------------------- /n0cli/block_storage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/urfave/cli" 8 | 9 | pprovisioning "github.com/n0stack/n0stack/n0proto.go/provisioning/v0" 10 | ) 11 | 12 | func DownloadBlockStorage(c *cli.Context) error { 13 | if c.NArg() != 1 { 14 | return fmt.Errorf("set valid arguments") 15 | } 16 | resourceName := c.Args().Get(0) 17 | 18 | conn, err := ConnectAPI(c) 19 | if err != nil { 20 | return err 21 | } 22 | defer conn.Close() 23 | 24 | cl := pprovisioning.NewBlockStorageServiceClient(conn) 25 | res, err := cl.DownloadBlockStorage(context.Background(), &pprovisioning.DownloadBlockStorageRequest{Name: resourceName}) 26 | if err != nil { 27 | PrintGrpcError(err) 28 | return nil 29 | } 30 | 31 | fmt.Println(res.DownloadUrl) 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /n0cli/do.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/signal" 10 | 11 | "github.com/n0stack/n0stack/n0proto.go/pkg/dag" 12 | "github.com/urfave/cli" 13 | "google.golang.org/grpc" 14 | yaml "gopkg.in/yaml.v2" 15 | ) 16 | 17 | // TODO: 複数ファイルを連結して処理できるようにしたい `n0cli do foo.yaml bar.yaml` みたいな 18 | func Do(ctx *cli.Context) error { 19 | if ctx.NArg() == 1 { 20 | return do(ctx) 21 | } 22 | 23 | return fmt.Errorf("set valid arguments") 24 | } 25 | 26 | // TODO: エラーレスポンス 27 | func do(ctx *cli.Context) error { 28 | filepath := ctx.Args().Get(0) 29 | buf, err := ioutil.ReadFile(filepath) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | tasks := map[string]*dag.Task{} 35 | // _, err := toml.DecodeFile(filepath, &tasks) 36 | if err := yaml.UnmarshalStrict(buf, tasks); err != nil { 37 | return err 38 | } 39 | 40 | endpoint := ctx.GlobalString("api-endpoint") 41 | conn, err := grpc.Dial(endpoint, grpc.WithInsecure()) 42 | if err != nil { 43 | return err 44 | } 45 | defer conn.Close() 46 | log.Printf("[DEBUG] Connected to '%s'\n", endpoint) 47 | 48 | ctxCancel, cancel := context.WithCancel(context.Background()) 49 | c := make(chan os.Signal, 1) 50 | signal.Notify(c, os.Interrupt) 51 | defer func() { 52 | signal.Stop(c) 53 | }() 54 | 55 | go func() { 56 | select { 57 | case <-c: // SIGINT 58 | cancel() // notify DoDAG to cancel 59 | signal.Stop(c) // allow sending SIGINT again to force SIGINT 60 | case <-ctxCancel.Done(): 61 | return 62 | } 63 | }() 64 | 65 | dag.Marshaler = marshaler 66 | if err := dag.CheckDAG(tasks); err != nil { 67 | return err 68 | } 69 | 70 | if ok := dag.DoDAG(ctxCancel, tasks, os.Stdout, conn); !ok { 71 | return fmt.Errorf("Failed to do tasks") 72 | } 73 | 74 | fmt.Fprintf(os.Stderr, "DAG tasks are completed\n") 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /n0cli/examples/debug-vm/clean.yaml: -------------------------------------------------------------------------------- 1 | DeleteVirtualMachine: 2 | type: VirtualMachine 3 | action: DeleteVirtualMachine 4 | args: 5 | name: debug 6 | 7 | ReleaseCompute: 8 | type: Node 9 | action: ReleaseCompute 10 | args: 11 | node_name: run-all-in-one 12 | compute_name: debug 13 | depends_on: 14 | - DeleteVirtualMachine 15 | ignore_error: true 16 | 17 | SetAvailableBlockStorage: 18 | type: BlockStorage 19 | action: SetAvailableBlockStorage 20 | args: 21 | name: debug 22 | depends_on: 23 | - DeleteVirtualMachine 24 | ignore_error: true 25 | 26 | DeleteBlockStorage: 27 | type: BlockStorage 28 | action: DeleteBlockStorage 29 | args: 30 | name: debug 31 | depends_on: 32 | - SetAvailableBlockStorage 33 | 34 | DeleteNetwork: 35 | type: Network 36 | action: DeleteNetwork 37 | args: 38 | name: debug_network 39 | depends_on: 40 | - DeleteVirtualMachine 41 | 42 | DeleteImage: 43 | type: Image 44 | action: DeleteImage 45 | args: 46 | name: cloudimage-ubuntu 47 | depends_on: 48 | - DeleteVirtualMachine 49 | -------------------------------------------------------------------------------- /n0cli/examples/debug-vm/image-ubuntu1804.yaml: -------------------------------------------------------------------------------- 1 | FetchBlockStorage: 2 | type: BlockStorage 3 | action: FetchBlockStorage 4 | args: 5 | name: ubuntu-1804 6 | annotations: 7 | n0core/provisioning/block_storage/request_node_name: vm-host1 8 | request_bytes: 1073741824 9 | limit_bytes: 10737418240 10 | # source_url: https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img 11 | source_url: file:///root/ubuntu-1804 12 | # source_url: file:///home/h-otter/wk/images/cirros-0.4.0-x86_64-disk.img 13 | ignore_error: true 14 | 15 | ApplyImage: 16 | type: Image 17 | action: ApplyImage 18 | args: 19 | name: cloudimage-ubuntu 20 | labels: 21 | test-label: testing 22 | 23 | RegisterBlockStorage: 24 | type: Image 25 | action: RegisterBlockStorage 26 | args: 27 | image_name: cloudimage-ubuntu 28 | block_storage_name: ubuntu-1804 29 | tags: 30 | - "1804" 31 | depends_on: 32 | - FetchBlockStorage 33 | - ApplyImage 34 | -------------------------------------------------------------------------------- /n0cli/examples/debug-vm/low_level_debug.yaml: -------------------------------------------------------------------------------- 1 | ReserveCompute: 2 | type: Node 3 | action: ReserveCompute 4 | args: 5 | node_name: vm-host1 6 | compute_name: debug 7 | request_cpu_milli_core: 10 8 | limit_cpu_milli_core: 1000 9 | request_memory_bytes: 536870912 10 | limit_memory_bytes: 536870912 11 | ignore_error: true 12 | 13 | DeleteVirtualMachine: 14 | type: VirtualMachine 15 | action: DeleteVirtualMachine 16 | args: 17 | name: debug 18 | ignore_error: true 19 | depends_on: 20 | - ReserveCompute 21 | 22 | ReleaseCompute: 23 | type: Node 24 | action: ReleaseCompute 25 | args: 26 | node_name: vm-host1 27 | compute_name: debug 28 | depends_on: 29 | - DeleteVirtualMachine 30 | ignore_error: true 31 | 32 | SetBlockStorage: 33 | type: BlockStorage 34 | action: SetAvailableBlockStorage 35 | args: 36 | name: debug 37 | depends_on: 38 | - DeleteVirtualMachine 39 | ignore_error: true 40 | 41 | DeleteBlockStorage: 42 | type: BlockStorage 43 | action: DeleteBlockStorage 44 | args: 45 | name: debug 46 | depends_on: 47 | - SetBlockStorage 48 | ignore_error: true 49 | 50 | GenerateBlockStorage: 51 | type: Image 52 | action: GenerateBlockStorage 53 | args: 54 | image_name: cloudimage-ubuntu 55 | tag: "1804" 56 | block_storage_name: debug 57 | annotations: 58 | n0core/provisioning/block_storage/request_node_name: vm-host1 59 | request_bytes: 1073741824 60 | limit_bytes: 10737418240 61 | depends_on: 62 | - DeleteBlockStorage 63 | 64 | DownloadBlockStorage: 65 | type: BlockStorage 66 | action: DownloadBlockStorage 67 | args: 68 | name: debug 69 | depends_on: 70 | - GenerateBlockStorage 71 | 72 | DeleteNetwork: 73 | type: Network 74 | action: DeleteNetwork 75 | args: 76 | name: debug_network 77 | depends_on: 78 | - DeleteVirtualMachine 79 | ignore_error: true 80 | 81 | ApplyNetwork: 82 | type: Network 83 | action: ApplyNetwork 84 | args: 85 | name: debug_network 86 | ipv4_cidr: 192.168.0.0/24 87 | annotations: 88 | n0core/provisioning/virtual_machine/vlan_id: "100" 89 | depends_on: 90 | - DeleteNetwork 91 | ignore_error: true 92 | 93 | CreateVirtualMachine: 94 | type: VirtualMachine 95 | action: CreateVirtualMachine 96 | args: 97 | name: debug 98 | annotations: 99 | n0core/provisioning/virtual_machine/request_node_name: vm-host1 100 | request_cpu_milli_core: 10 101 | limit_cpu_milli_core: 1000 102 | request_memory_bytes: 536870912 103 | limit_memory_bytes: 536870912 104 | block_storage_names: 105 | - debug 106 | nics: 107 | - network_name: debug_network 108 | uuid: 056d2ccd-0c4c-44dc-a2c8-39a9d394b51f 109 | login_username: test 110 | ssh_authorized_keys: 111 | - ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBITowPn2Ol1eCvXN5XV+Lb6jfXzgDbXyEdtayadDUJtFrcN2m2mjC1B20VBAoJcZtSYkmjrllS06Q26Te5sTYvE= testkey 112 | depends_on: 113 | - ReleaseCompute 114 | - GenerateBlockStorage 115 | - ApplyNetwork 116 | 117 | OpenConsole: 118 | type: VirtualMachine 119 | action: OpenConsole 120 | args: 121 | name: debug 122 | depends_on: 123 | - CreateVirtualMachine 124 | -------------------------------------------------------------------------------- /n0cli/examples/debug-vm/test_id_ecdsa: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIBAQh+adEg/rjqj9qLE0jI4EqV8kZFDzWTASAwvx6HWdoAoGCCqGSM49 3 | AwEHoUQDQgAEhOjA+fY6XV4K9c3ldX4tvqN9fOANtfIR21rJp0NQm0Wtw3abaaML 4 | UHbRUECglxm1JiSaOuWVLTpDbpN7mxNi8Q== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /n0cli/examples/debug-vm/test_id_ecdsa.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBITowPn2Ol1eCvXN5XV+Lb6jfXzgDbXyEdtayadDUJtFrcN2m2mjC1B20VBAoJcZtSYkmjrllS06Q26Te5sTYvE= testkey 2 | -------------------------------------------------------------------------------- /n0cli/examples/debug-vm/virtual_machine_error.yaml: -------------------------------------------------------------------------------- 1 | CreateVirtualMachine: 2 | type: VirtualMachine 3 | action: CreateVirtualMachine 4 | args: 5 | name: error 6 | annotations: 7 | n0core/provisioning/virtual_machine/request_node_name: vm-host1 8 | request_cpu_milli_core: 10 9 | limit_cpu_milli_core: 1000 10 | request_memory_bytes: 1 11 | limit_memory_bytes: 1 12 | block_storage_names: 13 | - nothing 14 | -------------------------------------------------------------------------------- /n0cli/grpc_cmd/gen.go: -------------------------------------------------------------------------------- 1 | package grpccmd 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/urfave/cli" 9 | ) 10 | 11 | func ParseJsonTag(tag string) string { 12 | if idx := strings.Index(tag, ","); idx != -1 { 13 | return tag[:idx] 14 | } 15 | 16 | return tag 17 | } 18 | 19 | // "name" is standard field, so get by args 20 | func GenerateFlags(targetGRPC interface{}, argsKeys []string) []cli.Flag { 21 | t := reflect.TypeOf(targetGRPC).In(2).Elem() 22 | flags := []cli.Flag{} 23 | 24 | for i := 0; i < t.NumField(); i++ { 25 | field := t.Field(i) 26 | 27 | tag := ParseJsonTag(field.Tag.Get("json")) 28 | if tag == "-" { 29 | continue 30 | } 31 | 32 | hidden := false 33 | for _, a := range argsKeys { 34 | if a == tag { 35 | hidden = true 36 | } 37 | } 38 | 39 | switch field.Type.Kind() { 40 | case reflect.String: 41 | flags = append(flags, cli.StringFlag{Name: tag, Hidden: hidden}) 42 | 43 | case reflect.Bool: 44 | flags = append(flags, cli.BoolFlag{Name: tag, Hidden: hidden}) 45 | 46 | case reflect.Int64: 47 | flags = append(flags, cli.Int64Flag{Name: tag, Hidden: hidden}) 48 | case reflect.Int32: 49 | flags = append(flags, cli.IntFlag{Name: tag, Hidden: hidden}) 50 | case reflect.Uint64: 51 | flags = append(flags, cli.Uint64Flag{Name: tag, Hidden: hidden}) 52 | case reflect.Uint32: 53 | flags = append(flags, cli.UintFlag{Name: tag, Hidden: hidden}) 54 | 55 | case reflect.Slice: 56 | // []string, []structに対応 57 | flags = append(flags, cli.StringSliceFlag{Name: tag, Hidden: hidden}) 58 | 59 | // TODO: []int など 60 | // if field.Type == reflect.TypeOf([]string{}) { 61 | // } 62 | 63 | case reflect.Map: 64 | // --map=key:value のような使い方を想定 65 | flags = append(flags, cli.StringSliceFlag{ 66 | Name: tag, 67 | Usage: "set like --option=[key]:[value]", 68 | Hidden: hidden, 69 | }) 70 | } 71 | } 72 | 73 | return flags 74 | } 75 | 76 | func GenerateAction(ctx context.Context, output OutputMessage, newGrpcClient interface{}, targetGRPC interface{}, argsKeys []string) func(*cli.Context) error { 77 | getter := GenerateGRPCGetter(targetGRPC, argsKeys, newGrpcClient) 78 | 79 | return func(c *cli.Context) error { 80 | conn, err := Connect2gRPC(c) 81 | if err != nil { 82 | return err 83 | } 84 | defer conn.Close() 85 | 86 | m, err := getter(c, ctx, conn) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | if err := output(c, m); err != nil { 92 | return err 93 | } 94 | 95 | return nil 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /n0cli/grpc_cmd/output_test.go: -------------------------------------------------------------------------------- 1 | package grpccmd 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/n0stack/n0stack/n0core/pkg/datastore" 8 | ) 9 | 10 | func TestOutputTable(t *testing.T) { 11 | test := &datastore.Test{ 12 | Name: "testing", 13 | } 14 | 15 | buf := &bytes.Buffer{} 16 | outputter := Outputter{ 17 | Out: buf, 18 | } 19 | if err := outputter.OutputTable(test, []string{"name"}); err != nil { 20 | t.Errorf("Failed to OutputTable: err=%s", err.Error()) 21 | } 22 | 23 | // TODO: compare output 24 | } 25 | -------------------------------------------------------------------------------- /n0cli/jsonpb.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/golang/protobuf/jsonpb" 4 | 5 | var marshaler = &jsonpb.Marshaler{ 6 | EnumsAsInts: false, 7 | EmitDefaults: false, 8 | Indent: " ", 9 | OrigName: true, 10 | } 11 | -------------------------------------------------------------------------------- /n0cli/virtual_machine.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/urfave/cli" 8 | 9 | pprovisioning "github.com/n0stack/n0stack/n0proto.go/provisioning/v0" 10 | ) 11 | 12 | func OpenConsoleOfVirtualMachine(c *cli.Context) error { 13 | if c.NArg() != 1 { 14 | return fmt.Errorf("set valid arguments") 15 | } 16 | resourceName := c.Args().Get(0) 17 | 18 | conn, err := ConnectAPI(c) 19 | if err != nil { 20 | return err 21 | } 22 | defer conn.Close() 23 | 24 | cl := pprovisioning.NewVirtualMachineServiceClient(conn) 25 | res, err := cl.OpenConsole(context.Background(), &pprovisioning.OpenConsoleRequest{Name: resourceName}) 26 | if err != nil { 27 | PrintGrpcError(err) 28 | return nil 29 | } 30 | 31 | fmt.Println(res.ConsoleUrl) 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /n0core/.gitignore: -------------------------------------------------------------------------------- 1 | **/*.db 2 | -------------------------------------------------------------------------------- /n0core/README.md: -------------------------------------------------------------------------------- 1 | # n0core 2 | 3 | The example for implementation of n0stack API. 4 | 5 | ## Environment 6 | 7 | - Ubuntu 18.04 LTS (Bionic Beaver) 8 | - Golang 1.11 9 | 10 | ## How to deploy 11 | 12 | ### API 13 | 14 | - Requires Docker and docker-compose 15 | 16 | ``` 17 | cd deploy/api 18 | docker-compose up 19 | ``` 20 | 21 | ### Agent 22 | 23 | Check agent arguments with `n0core serve agent -h`. 24 | 25 | #### Remote 26 | 27 | - Require root user 28 | - Perform the following processing 29 | - Send self to `/var/lib/n0core/n0core.$VERSION` with sftp 30 | - Run `n0core local` 31 | 32 | ```sh 33 | docker run -it --rm -v $HOME/.ssh:/root/.ssh n0stack/n0stack \ 34 | /usr/bin/n0core deploy agent \ 35 | -i /root/.ssh/id_ecdsa \ 36 | root@$node_ip \ 37 | $agent_args 38 | ``` 39 | 40 | ##### Example 41 | 42 | ```sh 43 | docker run -it --rm -v $HOME/.ssh:/root/.ssh n0stack/n0stack \ 44 | /usr/bin/n0core deploy agent \ 45 | -i /root/.ssh/id_ecdsa \ 46 | root@$node_ip \ 47 | --advertise-address=$node_ip \ 48 | --node-api-endpoint=$api_ip:20180 \ 49 | --location=////1 50 | ``` 51 | 52 | #### Local 53 | 54 | - Require root user 55 | - Perform the following processing 56 | - If n0core service is started, stop n0core service. 57 | - Create symbolic link from self to `/usr/bin/n0core` 58 | - Generate systemd unit file and start systemd service 59 | 60 | ```sh 61 | bin/n0core install agent -a "$agent_args" 62 | ``` 63 | 64 | ## Design 65 | 66 | ### VirtualMachine 67 | 68 | | Features | Yes / No | 69 | |--|--| 70 | | Redundancy | No | 71 | | Scalability | Yes | 72 | 73 | ### BlockStorage 74 | 75 | | Features | Yes / No | 76 | |--|--| 77 | | Redundancy | No | 78 | | Scalability | Yes | 79 | 80 | ### Network 81 | 82 | | Features | Yes / No | 83 | |--|--| 84 | | Redundancy | No | 85 | | Scalability | No (for each network) | 86 | 87 | ![](../docs/_static/images/n0core_network_design.svg) 88 | -------------------------------------------------------------------------------- /n0core/cmd/n0core/deploy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/n0stack/n0stack/n0core/pkg/deploy" 11 | "github.com/pkg/errors" 12 | "github.com/urfave/cli" 13 | "golang.org/x/crypto/ssh" 14 | ) 15 | 16 | func DeployAgent(ctx *cli.Context) error { 17 | if ctx.NArg() < 1 { 18 | return fmt.Errorf("Argument usage: %s", ctx.Command.ArgsUsage) 19 | } 20 | h := strings.Split(ctx.Args()[0], "@") 21 | if len(h) != 2 { 22 | return fmt.Errorf("Argument usage: %s", ctx.Command.ArgsUsage) 23 | } 24 | 25 | user := h[0] 26 | host := h[1] 27 | keyPath := ctx.String("identity-file") 28 | args := strings.Join(ctx.Args()[1:], " ") 29 | target := ctx.String("base-directory") 30 | sendTo := ctx.String("send-to") 31 | 32 | buf, err := ioutil.ReadFile(keyPath) 33 | if err != nil { 34 | return errors.Wrap(err, "Failed to read key file") 35 | } 36 | key, err := ssh.ParsePrivateKey(buf) 37 | if err != nil { 38 | return errors.Wrap(err, "Failed to parse key") 39 | } 40 | 41 | config := &ssh.ClientConfig{ 42 | User: user, 43 | Auth: []ssh.AuthMethod{ 44 | ssh.PublicKeys(key), 45 | }, 46 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 47 | } 48 | config.SetDefaults() 49 | conn, err := ssh.Dial("tcp", host+":22", config) 50 | if err != nil { 51 | return errors.Wrap(err, "Failed to dial ssh") 52 | } 53 | defer conn.Close() 54 | 55 | d, err := deploy.NewRemoteDeployer(conn, sendTo) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | binLocation := "n0core." + version 61 | fmt.Printf("---> [DEPLOY] Sending self to %s...\n", binLocation) 62 | self, err := d.ReadSelf() 63 | if err != nil { 64 | return err 65 | } 66 | if err := d.SendFile(self, binLocation, 0755); err != nil { 67 | return err 68 | } 69 | 70 | cmd := fmt.Sprintf("%s install agent -base-directory %s -arguments '%s'", filepath.Join(sendTo, binLocation), target, args) 71 | fmt.Printf("---> [DEPLOY] Running install '%s'...\n", cmd) 72 | if err := d.Command(cmd, os.Stdout, os.Stderr); err != nil { 73 | return err 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /n0core/cmd/n0core/install.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "github.com/n0stack/n0stack/n0core/pkg/deploy" 10 | "github.com/pkg/errors" 11 | "github.com/urfave/cli" 12 | ) 13 | 14 | const systemdAgentUnitPath = "/etc/systemd/system/n0core-agent.service" 15 | const systemdAgentUnit = "n0core-agent" 16 | 17 | var AgentRequiredPackages = []string{ 18 | "cloud-image-utils", 19 | "ipmitool", 20 | "iproute2", 21 | "qemu-kvm", 22 | "qemu-utils", 23 | } 24 | 25 | func InstallAgent(ctx *cli.Context) error { 26 | args := ctx.String("arguments") 27 | target := ctx.String("base-directory") 28 | 29 | d, err := deploy.NewLocalDeployer(target) 30 | if err != nil { 31 | return errors.Wrap(err, "Failed to create new LocalDeployer") 32 | } 33 | 34 | fmt.Printf("---> [INSTALL] Installing packages: %s\n", strings.Join(AgentRequiredPackages, ", ")) 35 | if err := d.InstallPackages(AgentRequiredPackages, os.Stdout, os.Stderr); err != nil { 36 | return errors.Wrap(err, "Failed to install packages") 37 | } 38 | 39 | fmt.Printf("---> [INSTALL] Set sysctl: net.ipv4.ip_forward=1\n") 40 | if err := d.SetSysctl("net.ipv4.ip_forward", []byte("1")); err != nil { 41 | return errors.Wrap(err, "Failed to set sysctl") 42 | } 43 | 44 | fmt.Println("---> [INSTALL] Stopping systemd unit...") 45 | if err := d.StopDaemon(systemdAgentUnit, os.Stdout, os.Stderr); err != nil { 46 | return errors.Wrap(err, "Failed to stop systemd daemon") 47 | } 48 | 49 | fmt.Printf("---> [INSTALL] Installing self to %s...\n", target) 50 | installPath, err := d.InstallBinary(target) 51 | if err != nil { 52 | return errors.Wrap(err, "Failed to install binary") 53 | } 54 | 55 | binLocation := "/usr/bin/n0core" 56 | fmt.Printf("---> [INSTALL] Linking self to %s...\n", binLocation) 57 | if err := d.Link(installPath, binLocation); err != nil { 58 | return errors.Wrap(err, "Failed to link self") 59 | } 60 | 61 | fmt.Println("---> [INSTALL] Preparing systemd unit...") 62 | cmd := fmt.Sprintf("%s serve agent %s", binLocation, args) 63 | systemd := d.CreateAgentUnit(cmd) 64 | if err := d.SaveFile(systemd, systemdAgentUnitPath, 0644); err != nil { 65 | return errors.Wrap(err, "Failed to save systemd unit file") 66 | } 67 | 68 | fmt.Println("---> [INSTALL] Restarting systemd unit...") 69 | if err := d.RestartDaemon(systemdAgentUnit, os.Stdout, os.Stderr); err != nil { 70 | return errors.Wrap(err, "Failed to restart systemd daemon") 71 | } 72 | 73 | fmt.Println("---> [INSTALL] Waiting 1 secs to show status...") 74 | time.Sleep(1 * time.Second) 75 | if err := d.DaemonStatus(systemdAgentUnit, os.Stdout, os.Stderr); err != nil { 76 | return errors.Wrap(err, "Failed to get status of systemd daemon") 77 | } 78 | 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /n0core/cmd/n0deploy/README.md: -------------------------------------------------------------------------------- 1 | # n0deploy 2 | 3 | VMやベアメタルなどマシンにアプリケーションをデプロイするためのCLI 4 | 5 | EXPERIMENTAL 6 | -------------------------------------------------------------------------------- /n0core/cmd/n0deploy/test.n0deploy: -------------------------------------------------------------------------------- 1 | RUN echo foo \ 2 | && echo bar 3 | COPY test.n0deploy /tmp/test.n0deploy 4 | 5 | DEPLOY 6 | RUN echo hogehoge 7 | -------------------------------------------------------------------------------- /n0core/pkg/api/README.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | - APIの実装を行う 4 | - ドメイン固有のロジックも含める 5 | 6 | ## 楽観的なロック 7 | 8 | - PENDING ステートを使うことで楽観的なロックを行う 9 | - 副作用のような時間のかかる処理の場合は入れること 10 | 11 | ``` 12 | ------------- timeline -------------> 13 | 14 | Reqeust 1 --> set PENDING (optimistic lock) --> some process --> set new state --> response OK 15 | | 16 | | Request 2 --> get PENDING --> response FailedPrecondition 17 | ``` 18 | -------------------------------------------------------------------------------- /n0core/pkg/api/iam/user/generate_stdapi_test.go: -------------------------------------------------------------------------------- 1 | package auser 2 | 3 | import ( 4 | "testing" 5 | 6 | stdapi "github.com/n0stack/n0stack/n0core/pkg/api/standard_api" 7 | "github.com/n0stack/n0stack/n0core/pkg/util/generator" 8 | ) 9 | 10 | func TestGenerate(t *testing.T) { 11 | g := generator.NewGoCodeGenerator("stdapi", "auser") 12 | stdapi.GenerateTemplateAPI(g, "iam", "User") 13 | 14 | if err := g.WriteAsTemplateFileName(); err != nil { 15 | t.Errorf("err=%s", err.Error()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/network/README.md: -------------------------------------------------------------------------------- 1 | # Network 2 | 3 | ## Example 4 | 5 | ``` 6 | grpc_cli call localhost:20182 n0stack.pool.NetworkService/ListNetworks '' 7 | ``` 8 | 9 | ``` 10 | grpc_cli call localhost:20182 n0stack.pool.NetworkService/GetNetwork ' 11 | name: "test-network" 12 | ' 13 | ``` 14 | 15 | ``` 16 | grpc_cli call localhost:20182 n0stack.pool.NetworkService/ApplyNetwork ' 17 | name: "test-network" 18 | ipv4_cidr: "10.100.100.0/24" 19 | domain: "test.local" 20 | ' 21 | ``` 22 | 23 | ``` 24 | grpc_cli call localhost:20182 n0stack.pool.NetworkService/ReserveNetworkInterface ' 25 | name: "test-network" 26 | network_interface_name: "test-reserve" 27 | ' 28 | ``` 29 | 30 | ``` 31 | grpc_cli call localhost:20182 n0stack.pool.NetworkService/ReleaseNetworkInterface ' 32 | name: "test-network" 33 | network_interface_name: "test-reserve" 34 | ' 35 | ``` 36 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/network/annotations.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | const AnnotationNetworkInterfaceDisableDeletionLock = "n0core/pool/network/disable_deletion_lock" 4 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/network/api_mock.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/golang/protobuf/ptypes/empty" 8 | "github.com/n0stack/n0stack/n0core/pkg/datastore/memory" 9 | "github.com/n0stack/n0stack/n0proto.go/pool/v0" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | var factroyIndex = 0 14 | 15 | type MockNetworkAPI struct { 16 | api *NetworkAPI 17 | } 18 | 19 | func NewMockNetworkAPI(datastore *memory.MemoryDatastore) *MockNetworkAPI { 20 | n := CreateNetworkAPI(datastore) 21 | return &MockNetworkAPI{n} 22 | } 23 | 24 | func (a MockNetworkAPI) FactoryNetwork(ctx context.Context) (*ppool.Network, error) { 25 | factroyIndex++ 26 | 27 | return a.api.ApplyNetwork(ctx, &ppool.ApplyNetworkRequest{ 28 | Name: fmt.Sprintf("factory-network%d", factroyIndex), 29 | Domain: fmt.Sprintf("factory-network%d.test", factroyIndex), 30 | Ipv4Cidr: fmt.Sprintf("10.0.%d.0/24", factroyIndex), 31 | Ipv6Cidr: fmt.Sprintf("fc00:%x::1/64", factroyIndex), 32 | }) 33 | } 34 | 35 | func (a MockNetworkAPI) ListNetworks(ctx context.Context, in *ppool.ListNetworksRequest, opts ...grpc.CallOption) (*ppool.ListNetworksResponse, error) { 36 | return a.api.ListNetworks(ctx, in) 37 | } 38 | func (a MockNetworkAPI) GetNetwork(ctx context.Context, in *ppool.GetNetworkRequest, opts ...grpc.CallOption) (*ppool.Network, error) { 39 | return a.api.GetNetwork(ctx, in) 40 | } 41 | func (a MockNetworkAPI) ApplyNetwork(ctx context.Context, in *ppool.ApplyNetworkRequest, opts ...grpc.CallOption) (*ppool.Network, error) { 42 | return a.api.ApplyNetwork(ctx, in) 43 | } 44 | func (a MockNetworkAPI) DeleteNetwork(ctx context.Context, in *ppool.DeleteNetworkRequest, opts ...grpc.CallOption) (*empty.Empty, error) { 45 | return a.api.DeleteNetwork(ctx, in) 46 | } 47 | func (a MockNetworkAPI) ReserveNetworkInterface(ctx context.Context, in *ppool.ReserveNetworkInterfaceRequest, opts ...grpc.CallOption) (*ppool.Network, error) { 48 | return a.api.ReserveNetworkInterface(ctx, in) 49 | } 50 | func (a MockNetworkAPI) ReleaseNetworkInterface(ctx context.Context, in *ppool.ReleaseNetworkInterfaceRequest, opts ...grpc.CallOption) (*empty.Empty, error) { 51 | return a.api.ReleaseNetworkInterface(ctx, in) 52 | } 53 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/network/budget.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/n0stack/n0stack/n0core/pkg/util/net" 8 | "github.com/n0stack/n0stack/n0proto.go/budget/v0" 9 | ) 10 | 11 | func CheckIPv4OnCIDR(request net.IP, cidr *net.IPNet) error { 12 | if !cidr.Contains(request) { 13 | return fmt.Errorf("Requested IPv4 '%s' is over from Network CIDR '%s'", request.String(), cidr.String()) 14 | } 15 | 16 | return nil 17 | } 18 | 19 | func CheckConflictIPv4(request net.IP, reserved map[string]*pbudget.NetworkInterface) error { 20 | for k, v := range reserved { 21 | if request.String() == v.Ipv4Address { 22 | return fmt.Errorf("Network interface '%s' is already have IPv4 address '%s'", k, request.String()) 23 | } 24 | } 25 | 26 | return nil 27 | } 28 | 29 | // O(len(reserved) ^ 2) なので要修正 30 | func ScheduleNewIPv4(cidr *net.IPNet, reserved map[string]*pbudget.NetworkInterface) net.IP { 31 | // escape network address and broadcast address 32 | for ip := netutil.NextIP(cidr.IP); cidr.Contains(netutil.NextIP(ip)); ip = netutil.NextIP(ip) { 33 | if err := CheckConflictIPv4(ip, reserved); err == nil { 34 | return ip 35 | } 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/network/budget_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/n0stack/n0stack/n0proto.go/budget/v0" 8 | ) 9 | 10 | func TestScheduleNewIPv4(t *testing.T) { 11 | _, cidr, _ := net.ParseCIDR("192.168.0.0/30") 12 | 13 | cases := []struct { 14 | // cidr *net.IPNet 15 | reserved map[string]*pbudget.NetworkInterface 16 | result net.IP 17 | }{ 18 | { 19 | map[string]*pbudget.NetworkInterface{ 20 | "hoge": { 21 | Ipv4Address: "192.168.0.1", 22 | }, 23 | }, 24 | net.ParseIP("192.168.0.2"), 25 | }, 26 | { 27 | map[string]*pbudget.NetworkInterface{ 28 | "foo": { 29 | Ipv4Address: "192.168.0.1", 30 | }, 31 | "bar": { 32 | Ipv4Address: "192.168.0.2", 33 | }, 34 | }, 35 | nil, 36 | }, 37 | } 38 | 39 | for _, c := range cases { 40 | ip := ScheduleNewIPv4(cidr, c.reserved) 41 | 42 | if c.result != nil && ip == nil { 43 | t.Errorf("Wrong generated IPv4 address\n\thave:nil\n\twant:%s", c.result) 44 | } else if c.result == nil && ip != nil { 45 | t.Errorf("Wrong generated IPv4 address\n\thave:%s\n\twant:nil", ip) 46 | } else if !ip.Equal(c.result) { 47 | t.Errorf("Wrong generated IPv4 address\n\thave:%s\n\twant:%s", ip, c.result) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/network/tools.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "github.com/n0stack/n0stack/n0proto.go/pool/v0" 5 | ) 6 | 7 | func IsLockedForDeletion(network *ppool.Network) bool { 8 | for _, ni := range network.ReservedNetworkInterfaces { 9 | if ni.Annotations != nil { 10 | if _, ok := ni.Annotations[AnnotationNetworkInterfaceDisableDeletionLock]; ok { 11 | continue 12 | } 13 | } 14 | 15 | return true 16 | } 17 | 18 | return false 19 | } 20 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/network/tools_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/n0stack/n0stack/n0proto.go/budget/v0" 7 | "github.com/n0stack/n0stack/n0proto.go/pool/v0" 8 | ) 9 | 10 | func TestIsLockedForDeletion(t *testing.T) { 11 | cases := []struct { 12 | Name string 13 | Network *ppool.Network 14 | Res bool 15 | }{ 16 | { 17 | Name: "nothing", 18 | Network: &ppool.Network{}, 19 | Res: false, 20 | }, 21 | { 22 | Name: "not lock", 23 | Network: &ppool.Network{ 24 | ReservedNetworkInterfaces: map[string]*pbudget.NetworkInterface{ 25 | "not lock1": &pbudget.NetworkInterface{ 26 | Annotations: map[string]string{ 27 | AnnotationNetworkInterfaceDisableDeletionLock: "true", 28 | }, 29 | }, 30 | "not lock2": &pbudget.NetworkInterface{ 31 | Annotations: map[string]string{ 32 | AnnotationNetworkInterfaceDisableDeletionLock: "true", 33 | }, 34 | }, 35 | }, 36 | }, 37 | Res: false, 38 | }, 39 | { 40 | Name: "lock", 41 | Network: &ppool.Network{ 42 | ReservedNetworkInterfaces: map[string]*pbudget.NetworkInterface{ 43 | "lock": &pbudget.NetworkInterface{}, 44 | }, 45 | }, 46 | Res: true, 47 | }, 48 | 49 | { 50 | Name: "lock after not lock", 51 | Network: &ppool.Network{ 52 | ReservedNetworkInterfaces: map[string]*pbudget.NetworkInterface{ 53 | "not lock": &pbudget.NetworkInterface{ 54 | Annotations: map[string]string{ 55 | AnnotationNetworkInterfaceDisableDeletionLock: "true", 56 | }, 57 | }, 58 | "lock": &pbudget.NetworkInterface{}, 59 | }, 60 | }, 61 | Res: true, 62 | }, 63 | } 64 | 65 | for _, c := range cases { 66 | if IsLockedForDeletion(c.Network) != c.Res { 67 | t.Errorf("[%s] IsLockedForDeletion return value was wrong: network=%v", c.Name, c.Network) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/node/README.md: -------------------------------------------------------------------------------- 1 | # Node 2 | 3 | ## Principle 4 | 5 | - 行いたいことは以下のとおりである 6 | - bare metal provisioning (本来の目的であるが実装予定) 7 | - サーバインベントリ管理 8 | - n0coreのサービスディスカバリ 9 | - 死活監視 10 | - nodeやユーザーがサーバインベントリを登録する 11 | - memberlistでサービスの死活監視を行う 12 | - Nodeとmemberlistの両方があることでサービスとして利用できると考える 13 | 14 | ## Discovery / Alive monitoring 15 | 16 | - With [memberlist](https://github.com/hashicorp/memberlist). 17 | - データの優先度は `memberlist > Node model` 18 | 19 | ### Agentが正常開始 (Node作成 -> memberlist参加) 20 | 21 | 1. AgentがAPIにNodeを保存、APIはmemberlistにないのでNotReadyに 22 | 2. AgentがAPIを起点にmemberlistに参加、APIは通知からNodeをReadyに 23 | 24 | ### Agentが正常開始 (memberlist参加 -> Node作成) 25 | 26 | 1. AgentがAPIを起点にmemberlistに参加、APIはNodeがないのでなにも変更せず 27 | 2. AgentがAPIにNodeを保存、APIのmemberlistにあるのでReadyに 28 | 29 | ### Agentが正常終了 30 | 31 | 1. AgentがAPIからNodeを削除 32 | 2. Agentがmemberlistから抜ける 33 | 34 | ### Agentが異常終了 35 | 36 | 1. Agentがmemberlistから抜ける 37 | 2. APIが離脱を検知し、NodeをNotReadyに 38 | 39 | ### APIが異常終了 (同時にAgentが異常終了した場合も同様) 40 | 41 | - APIが死亡時は動作としては問題がない 42 | 43 | #### APIが復活 44 | 45 | - TODO: AgentとAPIどちらがどちらのmemberlistにジョインするか 46 | - Agentからな気がする 47 | 48 | ### TODO: memberlistとNodeの値が一致しない 49 | 50 | - APIへのリクエスト失敗が考えられる 51 | - `NotReady` or 情報の確実性は `memberlist > Node` なのでNodeを更新 or `Invalid` 52 | 53 | ## Example 54 | 55 | ``` 56 | grpc_cli call localhost:20181 n0stack.pool.NodeService/ListNodes '' 57 | ``` 58 | 59 | ``` 60 | grpc_cli call localhost:20181 n0stack.pool.NodeService/GetNode \ 61 | 'name: "test"' 62 | ``` 63 | 64 | ``` 65 | grpc_cli call localhost:20181 n0stack.pool.NodeService/DeleteNode \ 66 | 'name: "test"' 67 | ``` 68 | 69 | ``` 70 | grpc_cli call localhost:20181 n0stack.pool.NodeService/ReserveCompute ' 71 | name: "test" 72 | compute_name: "test-reserve" 73 | request_cpu_milli_core: 10 74 | limit_cpu_milli_core: 10 75 | request_memory_bytes: 10 76 | limit_memory_bytes: 10 77 | ``` 78 | 79 | ``` 80 | grpc_cli call localhost:20181 n0stack.pool.NodeService/ReleaseCompute ' 81 | name: "test" 82 | compute_name: "test-reserve" 83 | ' 84 | ``` 85 | 86 | ``` 87 | grpc_cli call localhost:20181 n0stack.pool.NodeService/ReserveStorage ' 88 | name: "test" 89 | storage_name: "test-reserve" 90 | request_bytes: 10 91 | limit_bytes: 10 92 | ``` 93 | 94 | ``` 95 | grpc_cli call localhost:20181 n0stack.pool.NodeService/ReleaseStorage ' 96 | name: "test" 97 | storage_name: "ubuntu-1804-iso" 98 | ' 99 | ``` 100 | 101 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/node/agent.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "os/exec" 5 | "runtime" 6 | "strings" 7 | "syscall" 8 | ) 9 | 10 | // IPMIを持っていない場合が考えられるので、とりあえずエラーハンドリングはしていない 11 | func GetIpmiAddress() string { 12 | out, err := exec.Command("ipmitool", "lan", "print").Output() 13 | if err != nil { 14 | return "" 15 | } 16 | 17 | for _, l := range strings.Split(string(out), "\n") { 18 | if strings.Contains(l, "IP Address :") { // これで正しいのかよくわからず、要テスト 19 | s := strings.Split(l, " ") 20 | return s[len(s)-1] 21 | } 22 | } 23 | 24 | return "" 25 | } 26 | 27 | // Serialが取得できなくても動作に問題はないため、エラーハンドリングはしていない 28 | func GetSerial() string { 29 | out, err := exec.Command("dmidecode", "-t", "system").Output() 30 | if err != nil { 31 | return "" 32 | } 33 | 34 | for _, l := range strings.Split(string(out), "\n") { 35 | if strings.Contains(l, "Serial Number:") { 36 | s := strings.Split(l, " ") 37 | return s[len(s)-1] 38 | } 39 | } 40 | 41 | return "" 42 | } 43 | 44 | func GetTotalMemory() uint64 { 45 | si := &syscall.Sysinfo_t{} 46 | err := syscall.Sysinfo(si) 47 | if err != nil { 48 | return 0 49 | } 50 | 51 | return si.Totalram / uint64(si.Unit) 52 | } 53 | 54 | func GetTotalCPUMilliCores() uint32 { 55 | return uint32(runtime.NumCPU()) 56 | } 57 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/node/annotations.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | const AnnotationComputeDisableDeletionLock = "n0core/pool/node/disable_deletion_lock" 4 | 5 | const AnnotationStorageDisableDeletionLock = "n0core/pool/node/disable_deletion_lock" 6 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/node/api_mock.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "context" 5 | 6 | "code.cloudfoundry.org/bytefmt" 7 | "github.com/golang/protobuf/ptypes/empty" 8 | "github.com/n0stack/n0stack/n0core/pkg/datastore/memory" 9 | "github.com/n0stack/n0stack/n0proto.go/pool/v0" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | type MockNodeAPI struct { 14 | a *NodeAPI 15 | } 16 | 17 | const MockNodeIP = "127.0.20.180" 18 | 19 | func NewMockNodeAPI(datastore *memory.MemoryDatastore) *MockNodeAPI { 20 | a := CreateNodeAPI(datastore) 21 | return &MockNodeAPI{a} 22 | } 23 | 24 | func (a MockNodeAPI) SetupMockNode(ctx context.Context) (*ppool.Node, error) { 25 | return a.ApplyNode(ctx, &ppool.ApplyNodeRequest{ 26 | Name: "mocked", 27 | Address: MockNodeIP, 28 | CpuMilliCores: 16000, 29 | MemoryBytes: 64 * bytefmt.GIGABYTE, 30 | StorageBytes: 100 * bytefmt.GIGABYTE, 31 | }) 32 | } 33 | 34 | func (a MockNodeAPI) ListNodes(ctx context.Context, in *ppool.ListNodesRequest, opts ...grpc.CallOption) (*ppool.ListNodesResponse, error) { 35 | return a.a.ListNodes(ctx, in) 36 | } 37 | func (a MockNodeAPI) GetNode(ctx context.Context, in *ppool.GetNodeRequest, opts ...grpc.CallOption) (*ppool.Node, error) { 38 | return a.a.GetNode(ctx, in) 39 | } 40 | func (a MockNodeAPI) ApplyNode(ctx context.Context, in *ppool.ApplyNodeRequest, opts ...grpc.CallOption) (*ppool.Node, error) { 41 | return a.a.ApplyNode(ctx, in) 42 | } 43 | func (a MockNodeAPI) DeleteNode(ctx context.Context, in *ppool.DeleteNodeRequest, opts ...grpc.CallOption) (*empty.Empty, error) { 44 | return a.a.DeleteNode(ctx, in) 45 | } 46 | func (a MockNodeAPI) ScheduleCompute(ctx context.Context, in *ppool.ScheduleComputeRequest, opts ...grpc.CallOption) (*ppool.Node, error) { 47 | return a.a.ScheduleCompute(ctx, in) 48 | } 49 | func (a MockNodeAPI) ReserveCompute(ctx context.Context, in *ppool.ReserveComputeRequest, opts ...grpc.CallOption) (*ppool.Node, error) { 50 | return a.a.ReserveCompute(ctx, in) 51 | } 52 | func (a MockNodeAPI) ReleaseCompute(ctx context.Context, in *ppool.ReleaseComputeRequest, opts ...grpc.CallOption) (*empty.Empty, error) { 53 | return a.a.ReleaseCompute(ctx, in) 54 | } 55 | func (a MockNodeAPI) ScheduleStorage(ctx context.Context, in *ppool.ScheduleStorageRequest, opts ...grpc.CallOption) (*ppool.Node, error) { 56 | return a.a.ScheduleStorage(ctx, in) 57 | } 58 | func (a MockNodeAPI) ReserveStorage(ctx context.Context, in *ppool.ReserveStorageRequest, opts ...grpc.CallOption) (*ppool.Node, error) { 59 | return a.a.ReserveStorage(ctx, in) 60 | } 61 | func (a MockNodeAPI) ReleaseStorage(ctx context.Context, in *ppool.ReleaseStorageRequest, opts ...grpc.CallOption) (*empty.Empty, error) { 62 | return a.a.ReleaseStorage(ctx, in) 63 | } 64 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/node/budget.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/n0stack/n0stack/n0proto.go/budget/v0" 7 | ) 8 | 9 | // TODO: 多分パッケージを切り出す 10 | 11 | func CheckCompute(requestCpus, totalCpus uint32, requestMemory, totalMemory uint64, reserved map[string]*pbudget.Compute) error { 12 | usedCPU := uint32(0) 13 | usedMemory := uint64(0) 14 | 15 | for _, c := range reserved { 16 | usedCPU += c.RequestCpuMilliCore 17 | usedMemory += c.RequestMemoryBytes 18 | } 19 | 20 | if totalCpus < usedCPU+requestCpus { 21 | return fmt.Errorf("parameter='cpu_milli_core', total='%d', used='%d', requested='%d'", totalCpus, usedCPU, requestCpus) 22 | } 23 | if totalMemory < usedMemory+requestMemory { 24 | return fmt.Errorf("parameter='memory_bytes', total='%d', used='%d', requested='%d'", totalMemory, usedMemory, requestMemory) 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func CheckStorage(request, total uint64, reserved map[string]*pbudget.Storage) error { 31 | usedStorage := uint64(0) 32 | 33 | for _, c := range reserved { 34 | usedStorage += c.RequestBytes 35 | } 36 | 37 | if total < usedStorage+request { 38 | return fmt.Errorf("total='%d', used='%d', requested='%d'", total, usedStorage, request) 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/node/connection.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "google.golang.org/grpc" 8 | 9 | ppool "github.com/n0stack/n0stack/n0proto.go/pool/v0" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | func GetConnection(ctx context.Context, api ppool.NodeServiceClient, nodeName string) (*grpc.ClientConn, error) { 14 | n, err := api.GetNode(ctx, &ppool.GetNodeRequest{Name: nodeName}) 15 | if err != nil { 16 | return nil, errors.Wrap(err, "Failed to get node from API") 17 | } 18 | 19 | if n.State != ppool.Node_READY { 20 | return nil, nil 21 | } 22 | 23 | // port を何かから取れるようにする 24 | conn, err := grpc.Dial(fmt.Sprintf("%s:%d", n.Address, 20181), grpc.WithInsecure()) 25 | if err != nil { 26 | return nil, errors.Wrap(err, "Fail to dial to node") 27 | } 28 | 29 | return conn, nil 30 | } 31 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/node/tools.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "github.com/n0stack/n0stack/n0proto.go/pool/v0" 5 | ) 6 | 7 | func IsLockedForDeletion(node *ppool.Node) bool { 8 | for _, c := range node.ReservedComputes { 9 | if _, ok := c.Annotations[AnnotationComputeDisableDeletionLock]; ok { 10 | continue 11 | } 12 | 13 | return true 14 | } 15 | 16 | for _, s := range node.ReservedStorages { 17 | if _, ok := s.Annotations[AnnotationStorageDisableDeletionLock]; ok { 18 | continue 19 | } 20 | 21 | return true 22 | } 23 | 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /n0core/pkg/api/pool/node/tools_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/n0stack/n0stack/n0proto.go/budget/v0" 7 | "github.com/n0stack/n0stack/n0proto.go/pool/v0" 8 | ) 9 | 10 | func TestIsLockedForDeletion(t *testing.T) { 11 | cases := []struct { 12 | Name string 13 | Node *ppool.Node 14 | Res bool 15 | }{ 16 | { 17 | Name: "nothing", 18 | Node: &ppool.Node{}, 19 | Res: false, 20 | }, 21 | { 22 | Name: "not lock by compute", 23 | Node: &ppool.Node{ 24 | ReservedComputes: map[string]*pbudget.Compute{ 25 | "not lock1": &pbudget.Compute{ 26 | Annotations: map[string]string{ 27 | AnnotationComputeDisableDeletionLock: "true", 28 | }, 29 | }, 30 | "not lock2": &pbudget.Compute{ 31 | Annotations: map[string]string{ 32 | AnnotationComputeDisableDeletionLock: "true", 33 | }, 34 | }, 35 | }, 36 | }, 37 | Res: false, 38 | }, 39 | { 40 | Name: "lock by compute", 41 | Node: &ppool.Node{ 42 | ReservedComputes: map[string]*pbudget.Compute{ 43 | "lock": &pbudget.Compute{}, 44 | }, 45 | }, 46 | Res: true, 47 | }, 48 | { 49 | Name: "lock after not lock by compute", 50 | Node: &ppool.Node{ 51 | ReservedComputes: map[string]*pbudget.Compute{ 52 | "not lock": &pbudget.Compute{ 53 | Annotations: map[string]string{ 54 | AnnotationComputeDisableDeletionLock: "true", 55 | }, 56 | }, 57 | "lock": &pbudget.Compute{}, 58 | }, 59 | }, 60 | Res: true, 61 | }, 62 | { 63 | Name: "not lock by storage", 64 | Node: &ppool.Node{ 65 | ReservedStorages: map[string]*pbudget.Storage{ 66 | "not lock1": &pbudget.Storage{ 67 | Annotations: map[string]string{ 68 | AnnotationStorageDisableDeletionLock: "true", 69 | }, 70 | }, 71 | "not lock2": &pbudget.Storage{ 72 | Annotations: map[string]string{ 73 | AnnotationStorageDisableDeletionLock: "true", 74 | }, 75 | }, 76 | }, 77 | }, 78 | Res: false, 79 | }, 80 | { 81 | Name: "lock by storage", 82 | Node: &ppool.Node{ 83 | ReservedStorages: map[string]*pbudget.Storage{ 84 | "lock": &pbudget.Storage{}, 85 | }, 86 | }, 87 | Res: true, 88 | }, 89 | { 90 | Name: "lock after not lock by storage", 91 | Node: &ppool.Node{ 92 | ReservedStorages: map[string]*pbudget.Storage{ 93 | "not lock": &pbudget.Storage{ 94 | Annotations: map[string]string{ 95 | AnnotationStorageDisableDeletionLock: "true", 96 | }, 97 | }, 98 | "lock": &pbudget.Storage{}, 99 | }, 100 | }, 101 | Res: true, 102 | }, 103 | } 104 | 105 | for _, c := range cases { 106 | if IsLockedForDeletion(c.Node) != c.Res { 107 | t.Errorf("[%s] IsLockedForDeletion return value was wrong: Node=%v", c.Name, c.Node) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /n0core/pkg/api/provisioning/README.md: -------------------------------------------------------------------------------- 1 | # Provisioning 2 | 3 | ## Example 4 | 5 | ### BlockStorage 6 | 7 | ``` 8 | grpc_cli call localhost:20180 n0stack.provisioning.BlockStorageService/CreateBlockStorage ' 9 | name: "test-empty-volume" 10 | annotations { 11 | key: "n0core/provisioning/request_node_name" 12 | value: "test" 13 | } 14 | request_bytes: 1024 15 | limit_bytes: 1073741824 16 | ' 17 | ``` 18 | 19 | ``` 20 | grpc_cli call localhost:20183 n0stack.provisioning.BlockStorageService/FetchBlockStorage ' 21 | name: "test-ubuntu-volume" 22 | annotations { 23 | key: "n0core/provisioning/request_node_name" 24 | value: "test" 25 | } 26 | request_bytes: 1073741824 27 | limit_bytes: 10737418240 28 | source_url: "http://cloud-images.ubuntu.com/xenial/current/xenial-server-cloudimg-amd64-disk1.img" 29 | ' 30 | 31 | grpc_cli call localhost:20183 n0stack.provisioning.BlockStorageService/FetchBlockStorage ' 32 | name: "test-ubuntu1804" 33 | annotations { 34 | key: "n0core/provisioning/request_node_name" 35 | value: "test" 36 | } 37 | request_bytes: 1073741824 38 | limit_bytes: 10737418240 39 | source_url: "http://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img" 40 | ' 41 | ``` 42 | 43 | ``` 44 | grpc_cli call localhost:20183 n0stack.provisioning.BlockStorageService/ListBlockStorages '' 45 | ``` 46 | 47 | ``` 48 | grpc_cli call localhost:20183 n0stack.provisioning.BlockStorageService/GetBlockStorage 'name: "test-ubuntu-volume"' 49 | ``` 50 | 51 | ``` 52 | grpc_cli call localhost:20183 n0stack.provisioning.BlockStorageService/SetAvailableBlockStorage 'name: test-ubuntu-volume"' 53 | ``` 54 | 55 | ``` 56 | grpc_cli call localhost:20183 n0stack.provisioning.BlockStorageService/SetInuseBlockStorage 'name: "test-ubuntu-volume"' 57 | ``` 58 | 59 | 60 | ### Virtual machine 61 | 62 | ``` 63 | grpc_cli call localhost:20180 n0stack.provisioning.VirtualMachineService/CreateVirtualMachine ' 64 | name: "test-vm-2" 65 | annotations { 66 | key: "n0core/provisioning/request_node_name" 67 | value: "test" 68 | } 69 | 70 | request_cpu_milli_core: 10 71 | limit_cpu_milli_core: 1000 72 | request_memory_bytes: 1073741824 73 | limit_memory_bytes: 1073741824 74 | 75 | block_storage_names: "test-empty-volume" 76 | nics { 77 | network_name: "test-network" 78 | } 79 | ' 80 | ``` 81 | 82 | ``` 83 | grpc_cli call localhost:20184 n0stack.provisioning.VirtualMachineService/ListVirtualMachines '' 84 | ``` 85 | 86 | ``` 87 | grpc_cli call localhost:20184 n0stack.provisioning.VirtualMachineService/GetVirtualMachine 'name: "test-vm"' 88 | ``` 89 | 90 | ``` 91 | grpc_cli call localhost:20184 n0stack.provisioning.VirtualMachineService/DeleteVirtualMachine 'name: "test-vm"' 92 | ``` 93 | 94 | ``` 95 | grpc_cli call localhost:20184 n0stack.provisioning.VirtualMachineService/BootVirtualMachine 'name: "test-vm"' 96 | ``` 97 | 98 | ``` 99 | grpc_cli call localhost:20184 n0stack.provisioning.VirtualMachineService/RebootVirtualMachine ' 100 | name: "test-vm" 101 | hard: true 102 | ' 103 | ``` 104 | 105 | ``` 106 | grpc_cli call localhost:20184 n0stack.provisioning.VirtualMachineService/ShutdownVirtualMachine ' 107 | name: "test-vm" 108 | hard: true 109 | ' 110 | ``` 111 | -------------------------------------------------------------------------------- /n0core/pkg/api/provisioning/blockstorage/agent.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/n0stack/n0stack/n0core/pkg/api/provisioning/blockstorage;blockstorage"; 4 | 5 | package n0stack.internal.n0core.provisioning.blockstorage; 6 | 7 | import "google/protobuf/empty.proto"; 8 | 9 | service BlockStorageAgentService { 10 | rpc CreateEmptyBlockStorage(CreateEmptyBlockStorageRequest) returns (CreateEmptyBlockStorageResponse) {} 11 | rpc FetchBlockStorage(FetchBlockStorageRequest) returns (FetchBlockStorageResponse) {} 12 | 13 | rpc DeleteBlockStorage(DeleteBlockStorageRequest) returns (google.protobuf.Empty) {} 14 | 15 | rpc ResizeBlockStorage(ResizeBlockStorageRequest) returns (google.protobuf.Empty) {} 16 | 17 | // rpc ResizeBlockStorageAgent(ResizeBlockStorageAgentRequest) returns (BlockStorageAgent) {} 18 | // rpc MigrateBlockStorageAgent(MigrateBlockStorageAgentRequest) returns (BlockStorageAgent) {} 19 | } 20 | 21 | message CreateEmptyBlockStorageRequest { 22 | string name = 1; 23 | uint64 bytes = 2; 24 | } 25 | message CreateEmptyBlockStorageResponse{ 26 | string path = 3; 27 | } 28 | 29 | message FetchBlockStorageRequest { 30 | string name = 1; 31 | uint64 bytes = 2; 32 | 33 | string source_url = 3; 34 | } 35 | message FetchBlockStorageResponse { 36 | string path = 3; 37 | } 38 | 39 | message DeleteBlockStorageRequest { 40 | string path = 3; 41 | } 42 | 43 | message ResizeBlockStorageRequest{ 44 | uint64 bytes = 2; 45 | string path = 3; 46 | } 47 | -------------------------------------------------------------------------------- /n0core/pkg/api/provisioning/blockstorage/agent_mock.go: -------------------------------------------------------------------------------- 1 | package blockstorage 2 | 3 | import ( 4 | "context" 5 | "path/filepath" 6 | 7 | "github.com/golang/protobuf/ptypes/empty" 8 | "google.golang.org/grpc" 9 | ) 10 | 11 | type MockBlockStorageAgentAPI struct{} 12 | 13 | func (a MockBlockStorageAgentAPI) CreateEmptyBlockStorage(ctx context.Context, req *CreateEmptyBlockStorageRequest) (*CreateEmptyBlockStorageResponse, error) { 14 | return &CreateEmptyBlockStorageResponse{ 15 | Path: filepath.Join("/tmp", req.Name), 16 | }, nil 17 | } 18 | func (a MockBlockStorageAgentAPI) FetchBlockStorage(ctx context.Context, req *FetchBlockStorageRequest) (*FetchBlockStorageResponse, error) { 19 | return &FetchBlockStorageResponse{ 20 | Path: filepath.Join("/tmp", req.Name), 21 | }, nil 22 | } 23 | func (a MockBlockStorageAgentAPI) DeleteBlockStorage(ctx context.Context, req *DeleteBlockStorageRequest) (*empty.Empty, error) { 24 | return &empty.Empty{}, nil 25 | } 26 | 27 | func (a MockBlockStorageAgentAPI) ResizeBlockStorage(ctx context.Context, req *ResizeBlockStorageRequest) (*empty.Empty, error) { 28 | return &empty.Empty{}, nil 29 | } 30 | 31 | type MockBlockStorageAgentClient struct { 32 | api BlockStorageAgentServiceServer 33 | } 34 | 35 | func NewMockBlockStorageAgentClient() *MockBlockStorageAgentClient { 36 | return &MockBlockStorageAgentClient{ 37 | api: &MockBlockStorageAgentAPI{}, 38 | } 39 | } 40 | func (a MockBlockStorageAgentClient) CreateEmptyBlockStorage(ctx context.Context, in *CreateEmptyBlockStorageRequest, opts ...grpc.CallOption) (*CreateEmptyBlockStorageResponse, error) { 41 | return a.api.CreateEmptyBlockStorage(ctx, in) 42 | } 43 | func (a MockBlockStorageAgentClient) FetchBlockStorage(ctx context.Context, in *FetchBlockStorageRequest, opts ...grpc.CallOption) (*FetchBlockStorageResponse, error) { 44 | return a.api.FetchBlockStorage(ctx, in) 45 | } 46 | func (a MockBlockStorageAgentClient) DeleteBlockStorage(ctx context.Context, in *DeleteBlockStorageRequest, opts ...grpc.CallOption) (*empty.Empty, error) { 47 | return a.api.DeleteBlockStorage(ctx, in) 48 | } 49 | 50 | func (a MockBlockStorageAgentClient) ResizeBlockStorage(ctx context.Context, in *ResizeBlockStorageRequest, opts ...grpc.CallOption) (*empty.Empty, error) { 51 | return a.api.ResizeBlockStorage(ctx, in) 52 | } 53 | -------------------------------------------------------------------------------- /n0core/pkg/api/provisioning/blockstorage/annotations.go: -------------------------------------------------------------------------------- 1 | package blockstorage 2 | 3 | // Create のときに自動生成、消されると困る 4 | const AnnotationBlockStorageURL = "n0core/provisioning/block_storage/url" 5 | 6 | const AnnotationStorageReservedBy = "n0core/provisioning/block_storage/reserved_by" 7 | 8 | const AnnotationBlockStorageRequestNodeName = "n0core/provisioning/block_storage/request_node_name" 9 | 10 | const AnnotationBlockStorageFetchFrom = "n0core/provisioning/block_storage/fetch_from" 11 | 12 | const AnnotationBlockStorageCopyFrom = "n0core/provisioning/block_storage/copy_from" 13 | -------------------------------------------------------------------------------- /n0core/pkg/api/provisioning/blockstorage/generate_stdapi_test.go: -------------------------------------------------------------------------------- 1 | package blockstorage 2 | 3 | import ( 4 | "testing" 5 | 6 | stdapi "github.com/n0stack/n0stack/n0core/pkg/api/standard_api" 7 | "github.com/n0stack/n0stack/n0core/pkg/util/generator" 8 | ) 9 | 10 | func TestGenerate(t *testing.T) { 11 | g := generator.NewGoCodeGenerator("template_api", "blockstorage") 12 | stdapi.GenerateTemplateAPI(g, "provisioning", "BlockStorage") 13 | 14 | if err := g.WriteAsTemplateFileName(); err != nil { 15 | t.Errorf("err=%s", err.Error()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /n0core/pkg/api/provisioning/virtualmachine/agent.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/n0stack/n0stack/n0core/pkg/api/provisioning/virtualmachine;virtualmachine"; 4 | 5 | package n0stack.internal.n0core.provisioning.virtual_machine; 6 | 7 | import "google/protobuf/empty.proto"; 8 | 9 | 10 | service VirtualMachineAgentService { 11 | rpc BootVirtualMachine(BootVirtualMachineRequest) returns (BootVirtualMachineResponse) {} 12 | rpc RebootVirtualMachine(RebootVirtualMachineRequest) returns (RebootVirtualMachineResponse) {} 13 | rpc ShutdownVirtualMachine(ShutdownVirtualMachineRequest) returns (ShutdownVirtualMachineResponse) {} 14 | 15 | rpc DeleteVirtualMachine(DeleteVirtualMachineRequest) returns (google.protobuf.Empty) {} 16 | } 17 | 18 | 19 | enum VirtualMachineState { 20 | FAILED = 0; 21 | UNKNOWN = 1; 22 | SHUTDOWN = 2; 23 | RUNNING = 3; 24 | PAUSED = 4; 25 | } 26 | message BlockDev { 27 | string name = 1; 28 | string url = 2; 29 | uint32 boot_index = 3; 30 | } 31 | message NetDev { 32 | string name = 1; 33 | string network_name = 2; 34 | string hardware_address = 3; 35 | string ipv4_address_cidr = 4; 36 | string ipv4_gateway = 5; 37 | repeated string nameservers = 6; 38 | } 39 | 40 | message BootVirtualMachineRequest { 41 | string name = 1; 42 | string uuid = 2; 43 | 44 | uint32 vcpus = 3; 45 | uint64 memory_bytes = 4; 46 | 47 | repeated BlockDev blockdevs = 5; 48 | repeated NetDev netdevs = 6; 49 | 50 | string login_username = 7; 51 | repeated string ssh_authorized_keys = 8; 52 | } 53 | message BootVirtualMachineResponse { 54 | VirtualMachineState state = 1; 55 | uint32 websocket_port = 2; 56 | } 57 | 58 | message RebootVirtualMachineRequest { 59 | string name = 1; 60 | 61 | bool hard = 2; 62 | } 63 | message RebootVirtualMachineResponse { 64 | VirtualMachineState state = 1; 65 | } 66 | 67 | message ShutdownVirtualMachineRequest { 68 | string name = 1; 69 | 70 | bool hard = 2; 71 | } 72 | message ShutdownVirtualMachineResponse { 73 | VirtualMachineState state = 1; 74 | } 75 | 76 | message DeleteVirtualMachineRequest { 77 | string name = 1; 78 | 79 | // TODO: netdev の情報を QMP から取るまでは、とりあえず渡してもらう 80 | repeated NetDev netdevs = 8; 81 | } 82 | -------------------------------------------------------------------------------- /n0core/pkg/api/provisioning/virtualmachine/agent_mock.go: -------------------------------------------------------------------------------- 1 | package virtualmachine 2 | 3 | import ( 4 | "context" 5 | 6 | empty "github.com/golang/protobuf/ptypes/empty" 7 | "google.golang.org/grpc" 8 | ) 9 | 10 | type VirtualMachineAgentMock struct{} 11 | 12 | func (a VirtualMachineAgentMock) BootVirtualMachine(ctx context.Context, req *BootVirtualMachineRequest) (*BootVirtualMachineResponse, error) { 13 | return &BootVirtualMachineResponse{ 14 | State: VirtualMachineState_RUNNING, 15 | WebsocketPort: 6900, 16 | }, nil 17 | } 18 | func (a VirtualMachineAgentMock) RebootVirtualMachine(ctx context.Context, req *RebootVirtualMachineRequest) (*RebootVirtualMachineResponse, error) { 19 | return &RebootVirtualMachineResponse{ 20 | State: VirtualMachineState_RUNNING, 21 | }, nil 22 | } 23 | func (a VirtualMachineAgentMock) ShutdownVirtualMachine(ctx context.Context, req *ShutdownVirtualMachineRequest) (*ShutdownVirtualMachineResponse, error) { 24 | return &ShutdownVirtualMachineResponse{ 25 | State: VirtualMachineState_SHUTDOWN, 26 | }, nil 27 | } 28 | func (a VirtualMachineAgentMock) DeleteVirtualMachine(ctx context.Context, req *DeleteVirtualMachineRequest) (*empty.Empty, error) { 29 | return &empty.Empty{}, nil 30 | } 31 | 32 | type MockVirtualMachineAgentClient struct { 33 | api VirtualMachineAgentServiceServer 34 | } 35 | 36 | func NewMockVirtualMachineAgentClientMock() *MockVirtualMachineAgentClient { 37 | return &MockVirtualMachineAgentClient{ 38 | api: VirtualMachineAgentMock{}, 39 | } 40 | } 41 | func (a MockVirtualMachineAgentClient) BootVirtualMachine(ctx context.Context, in *BootVirtualMachineRequest, opts ...grpc.CallOption) (*BootVirtualMachineResponse, error) { 42 | return a.api.BootVirtualMachine(ctx, in) 43 | } 44 | func (a MockVirtualMachineAgentClient) RebootVirtualMachine(ctx context.Context, in *RebootVirtualMachineRequest, opts ...grpc.CallOption) (*RebootVirtualMachineResponse, error) { 45 | return a.api.RebootVirtualMachine(ctx, in) 46 | } 47 | func (a MockVirtualMachineAgentClient) ShutdownVirtualMachine(ctx context.Context, in *ShutdownVirtualMachineRequest, opts ...grpc.CallOption) (*ShutdownVirtualMachineResponse, error) { 48 | return a.api.ShutdownVirtualMachine(ctx, in) 49 | } 50 | func (a MockVirtualMachineAgentClient) DeleteVirtualMachine(ctx context.Context, in *DeleteVirtualMachineRequest, opts ...grpc.CallOption) (*empty.Empty, error) { 51 | return a.api.DeleteVirtualMachine(ctx, in) 52 | } 53 | -------------------------------------------------------------------------------- /n0core/pkg/api/provisioning/virtualmachine/agent_test.go: -------------------------------------------------------------------------------- 1 | package virtualmachine 2 | 3 | // func TestBootVirtualMachineAgent(t *testing.T) { 4 | // a, err := CreateVirtualMachineAgent("./") 5 | // if err != nil { 6 | // t.Fatalf("Failed to create virtual machine agent: err=%s", err.Error()) 7 | // } 8 | 9 | // ctx := context.Background() 10 | // ctx, cancel := context.WithCancel(ctx) 11 | // defer cancel() 12 | 13 | // boot := &BootVirtualMachineRequest{ 14 | // Name: "test-vm", 15 | // Uuid: uuid.NewV4().String(), 16 | 17 | // } 18 | // a.BootVirtualMachine(ctx) 19 | 20 | // } 21 | -------------------------------------------------------------------------------- /n0core/pkg/api/provisioning/virtualmachine/annotations.go: -------------------------------------------------------------------------------- 1 | package virtualmachine 2 | 3 | const AnnotationVirtualMachineVncWebSocketPort = "n0core/provisioning/virtual_machine/vnc_websocket_port" 4 | 5 | const AnnotationComputeReservedBy = "n0core/provisioning/virtual_machine/virtual_machine/reserved_by" 6 | 7 | const AnnotationNetworkInterfaceIsGateway = "n0core/provisioning/virtual_machine/is_gateway" 8 | 9 | const AnnotationVirtualMachineRequestNodeName = "n0core/provisioning/virtual_machine/request_node_name" 10 | 11 | const AnnotationVirtualMachineNICIsGateway = "n0core/provisioning/virtual_machine/is_gateway" 12 | -------------------------------------------------------------------------------- /n0core/pkg/api/provisioning/virtualmachine/n0test.CreateVirtualMachine.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | import numpy.random as nprd 4 | 5 | from n0test.tdt_generator import Generation 6 | 7 | 8 | ENV_KEY = "N0TEST_JSON_CreateVirtualMachine_REQUESTS" 9 | # JSON_FILEPATH = "n0test.CreateVirtualMachine.json" 10 | JSON_FILEPATH = "/tmp/n0test.selected.json" 11 | JSON_SEED_FILEPATH = "n0test.CreateVirtualMachine.seed.json" 12 | GENERATION_CASES = 512 13 | 14 | 15 | class CreateVirtualMachineGeneration(Generation): 16 | def run_get_proc(self, request_list): 17 | p = subprocess.Popen([ 18 | "virtualmachine.test", 19 | "-test.coverprofile=/dev/stderr" 20 | ], env={ 21 | ENV_KEY: json.dumps(request_list), 22 | }, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 23 | 24 | return p 25 | 26 | def run_get_score(self, out, err, chosen): 27 | if "PASS" in out: 28 | lines = err.split('\n') 29 | coverage = 0 30 | 31 | for l in lines: 32 | if len(l) == 0: 33 | continue 34 | 35 | if l[-1] == '1': 36 | coverage += 1 37 | 38 | return coverage 39 | 40 | elif "N0TEST_OMIT" in out: 41 | return -1 42 | 43 | print("out={}\nerr={}\nrequest={}".format(out.decode('utf-8'), err.decode('utf-8'), json.dumps(request, indent=2))) 44 | 45 | # TODO: crash report, 保存したい 46 | return -1 47 | 48 | def select(self, select_num=256): 49 | filtered = filter(lambda x: x["score"] != -1, self._result) 50 | # selected = sorted(filtered, key=lambda x: x["distance"], reverse=True)[:select_num] 51 | # self._selected = list(map(lambda x: x["case"], selected)) + self._persistence 52 | self._selected = list(map(lambda x: x["case"], filtered)) + self._persistence 53 | # self._selected = list(map(lambda x: x["case"], selected)) 54 | 55 | return self._selected 56 | 57 | 58 | if __name__ == "__main__": 59 | # with open(JSON_FILEPATH) as f: 60 | # prev = json.load(f) 61 | prev = None 62 | 63 | with open(JSON_SEED_FILEPATH) as f: 64 | seed = json.load(f) 65 | 66 | if prev: 67 | gen = CreateVirtualMachineGeneration(seed=seed, list_operate_distance=3, previous=prev) 68 | else: 69 | gen = CreateVirtualMachineGeneration(seed=seed, list_operate_distance=3) 70 | 71 | for i in range(500): 72 | while gen.len < GENERATION_CASES: 73 | if nprd.rand() < 0.75: 74 | gen.mutate(nprd.randint(1, 10) * (1 if nprd.rand() < 0.5 else -1)) 75 | gen.mutate(nprd.randint(1, 10) * (1 if nprd.rand() < 0.5 else -1)) 76 | else: 77 | gen.cross() 78 | 79 | gen.run() 80 | gen.persistent() 81 | gen.select(int(GENERATION_CASES * 0.75)) 82 | 83 | print(json.dumps({ 84 | "score(coverage)": gen.max_score, 85 | "persistenced": len(gen.persistence), 86 | "average_distance": gen.average_distance, 87 | })) 88 | 89 | with open(JSON_FILEPATH, mode='w') as f: 90 | json.dump(gen.persistence, f, indent=2) 91 | 92 | gen = CreateVirtualMachineGeneration(previous_generation=gen) -------------------------------------------------------------------------------- /n0core/pkg/api/provisioning/virtualmachine/n0test.CreateVirtualMachine.seed.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "debug", 3 | "annotations": { 4 | "n0core/provisioning/virtual_machine/request_node_name": "mocked" 5 | }, 6 | "request_cpu_milli_core": 10, 7 | "limit_cpu_milli_core": 1000, 8 | "request_memory_bytes": 536870912, 9 | "limit_memory_bytes": 536870912, 10 | "block_storage_names": [ 11 | "factory-blockstorage1" 12 | ], 13 | "nics": [ 14 | { 15 | "network_name": "factory-network1", 16 | "ipv4_address": "10.0.1.1" 17 | } 18 | ], 19 | "uuid": "056d2ccd-0c4c-44dc-a2c8-39a9d394b51f", 20 | "login_username": "test", 21 | "ssh_authorized_keys": [ 22 | "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBITowPn2Ol1eCvXN5XV+Lb6jfXzgDbXyEdtayadDUJtFrcN2m2mjC1B20VBAoJcZtSYkmjrllS06Q26Te5sTYvE= testkey" 23 | ] 24 | } -------------------------------------------------------------------------------- /n0core/pkg/api/provisioning/virtualmachine/n0test.CreateVirtualMachine_test.go: -------------------------------------------------------------------------------- 1 | package virtualmachine 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "testing" 11 | 12 | "github.com/golang/protobuf/jsonpb" 13 | "github.com/n0stack/n0stack/n0core/pkg/datastore/memory" 14 | "github.com/n0stack/n0stack/n0proto.go/provisioning/v0" 15 | ) 16 | 17 | func TestCreateVirtualMachineOnN0test(t *testing.T) { 18 | raw := os.Getenv("N0TEST_JSON_CreateVirtualMachine_REQUESTS") 19 | if raw == "" { 20 | // b, err := ioutil.ReadFile("CreateVirtualMachine.n0test.json") 21 | b, err := ioutil.ReadFile("n0test.CreateVirtualMachine.json") 22 | if err != nil { 23 | t.Fatalf("Failed to read CreateVirtualMachine.n0test.json: err=%s", err.Error()) 24 | } 25 | 26 | raw = string(b) 27 | } 28 | 29 | var rawList []interface{} 30 | if err := json.Unmarshal([]byte(raw), &rawList); err != nil { 31 | t.Fatalf("Failed to parse N0TEST_JSON_CreateVirtualMachine_REQUESTS by JSON: err=%s", err.Error()) 32 | } 33 | 34 | ctx := context.Background() 35 | ctx, cancel := context.WithCancel(ctx) 36 | defer cancel() 37 | 38 | m := memory.NewMemoryDatastore() 39 | vma := NewMockVirtualMachineAPI(m) 40 | 41 | mnode, err := vma.NodeAPI.SetupMockNode(ctx) 42 | if err != nil { 43 | t.Fatalf("Failed to set up mocked node: err=%s", err.Error()) 44 | } 45 | 46 | _, err = vma.NetworkAPI.FactoryNetwork(ctx) 47 | if err != nil { 48 | t.Fatalf("Failed to factory network: err='%s'", err.Error()) 49 | } 50 | 51 | _, err = vma.BlockStorageAPI.FactoryBlockStorage(ctx, mnode.Name) 52 | if err != nil { 53 | t.Fatalf("Failed to factory blockstorage: err='%s'", err.Error()) 54 | } 55 | 56 | baseDatastore := vma.api.dataStore 57 | for i, r := range rawList { 58 | vma.api.dataStore = baseDatastore.AddPrefix(fmt.Sprintf("test%d", i)) 59 | 60 | b, _ := json.Marshal(r) 61 | reader := bytes.NewReader(b) 62 | req := &pprovisioning.CreateVirtualMachineRequest{} 63 | 64 | if err := jsonpb.Unmarshal(reader, req); err != nil { 65 | t.Fatalf("[N0TEST_OMIT] Failed to parse N0TEST_JSON_CreateVirtualMachine_REQUESTS from JSON to pb") 66 | } 67 | 68 | vma.CreateVirtualMachine(ctx, req) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /n0core/pkg/api/provisioning/virtualmachine/n0test.md: -------------------------------------------------------------------------------- 1 | ``` 2 | go test -coverpkg='github.com/n0stack/n0stack/n0core/...' -run '^(TestCreateVirtualMachineOnN0test)$' -c . 3 | python n0test.CreateVirtualMachine.py 4 | ``` 5 | -------------------------------------------------------------------------------- /n0core/pkg/api/standard_api/error.go: -------------------------------------------------------------------------------- 1 | package stdapi 2 | 3 | import ( 4 | "google.golang.org/grpc/codes" 5 | 6 | grpcutil "github.com/n0stack/n0stack/n0core/pkg/util/grpc" 7 | ) 8 | 9 | func LockError() error { 10 | return grpcutil.WrapGrpcErrorf(codes.FailedPrecondition, "this is locked, wait a moment") 11 | } 12 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/README.md: -------------------------------------------------------------------------------- 1 | # Datastore 2 | 3 | 標準メソッドとカスタムメソッドのデータの永続化に関する共通部分を抽象化するものである。 4 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/docs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Example 3 | 4 | func SomeEndpoint(ctx context.COntext, req SomeRequest) error { 5 | // validation 6 | 7 | datastore.Lock(req.Key) 8 | defer datastore.Unlock(req.Key) 9 | 10 | tx := transaction.Begin() 11 | defer tx.Rollback() 12 | 13 | // API Process 14 | 15 | tx.Done() 16 | 17 | return nil 18 | } 19 | */ 20 | package datastore 21 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/embed/datastore.go: -------------------------------------------------------------------------------- 1 | package embed 2 | 3 | import ( 4 | "github.com/n0stack/n0stack/n0core/pkg/datastore" 5 | "github.com/n0stack/n0stack/n0core/pkg/datastore/lock" 6 | "github.com/n0stack/n0stack/n0core/pkg/datastore/store/leveldb" 7 | 8 | "github.com/golang/protobuf/proto" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type EmbedDatastore struct { 13 | db *leveldb.LeveldbStore 14 | mutex lock.MutexTable 15 | } 16 | 17 | func NewEmbedDatastore(dbDirectory string) (*EmbedDatastore, error) { 18 | l, err := leveldb.NewLeveldbStore(dbDirectory) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return &EmbedDatastore{ 24 | db: l, 25 | 26 | // TODO: prefix で同期されない 27 | mutex: lock.NewMemoryMutexTable(10000), 28 | }, nil 29 | } 30 | 31 | func (ds *EmbedDatastore) Close() error { 32 | return ds.db.Close() 33 | } 34 | 35 | func (m *EmbedDatastore) AddPrefix(prefix string) datastore.Datastore { 36 | return &EmbedDatastore{ 37 | db: m.db.AddPrefix(prefix), 38 | mutex: lock.NewMemoryMutexTable(10000), 39 | } 40 | } 41 | 42 | func (ds EmbedDatastore) List(f func(length int) []proto.Message) error { 43 | b, err := ds.db.List() 44 | if err != nil { 45 | return errors.Wrap(err, "failed to list by snapshot") 46 | } 47 | 48 | pb := f(len(b)) 49 | for i, v := range b { 50 | if err := proto.Unmarshal(v, pb[i]); err != nil { 51 | return err 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func (ds EmbedDatastore) Get(key string, pb proto.Message) error { 59 | v, err := ds.db.Get(key) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | if err := proto.Unmarshal(v, pb); err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (ds *EmbedDatastore) Apply(key string, pb proto.Message) error { 72 | if !ds.mutex.IsLocked(key) { 73 | return errors.New("key is not locked") 74 | } 75 | 76 | s, err := proto.Marshal(pb) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | if err := ds.db.Apply(key, s); err != nil { 82 | return errors.Wrap(err, "failed to apply for log") 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func (ds *EmbedDatastore) Delete(key string) error { 89 | if !ds.mutex.IsLocked(key) { 90 | return errors.New("key is not locked") 91 | } 92 | 93 | if err := ds.db.Delete(key); err != nil { 94 | return err 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (ds *EmbedDatastore) Lock(key string) bool { 101 | return ds.mutex.Lock(key) 102 | } 103 | func (ds *EmbedDatastore) Unlock(key string) bool { 104 | return ds.mutex.Unlock(key) 105 | } 106 | func (ds *EmbedDatastore) IsLocked(key string) bool { 107 | return ds.mutex.IsLocked(key) 108 | } 109 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/errors.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import ( 4 | fmt "fmt" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | type NotFoundError interface { 10 | IsNotFound() bool 11 | 12 | Error() string 13 | } 14 | 15 | type NotFound struct { 16 | key string 17 | } 18 | 19 | func NewNotFound(key string) *NotFound { 20 | return &NotFound{ 21 | key: key, 22 | } 23 | } 24 | 25 | func (e NotFound) IsNotFound() bool { 26 | return true 27 | } 28 | 29 | func (e NotFound) Error() string { 30 | return fmt.Sprintf("Key '%s' is not found", e.key) 31 | } 32 | 33 | func IsNotFound(err error) bool { 34 | if e, ok := err.(NotFoundError); ok { 35 | return e.IsNotFound() 36 | } 37 | 38 | return false 39 | } 40 | 41 | // func LockErrorMessage() string { 42 | // return "this is locked, wait a moment" 43 | // } 44 | 45 | func DefaultErrorMessage(err error) string { 46 | return errors.Wrap(err, "Failed to operate on datastore").Error() 47 | } 48 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/interface.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import ( 4 | "github.com/golang/protobuf/proto" 5 | ) 6 | 7 | type Datastore interface { 8 | AddPrefix(prefix string) Datastore 9 | 10 | List(f func(length int) []proto.Message) error 11 | 12 | // if result is empty, set pb as nil. 13 | Get(key string, pb proto.Message) error 14 | 15 | // update process requires locking in advance 16 | Apply(key string, pb proto.Message) error 17 | Delete(key string) error 18 | 19 | Lock(key string) bool 20 | Unlock(key string) bool 21 | IsLocked(key string) bool 22 | } 23 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/lock/interface.go: -------------------------------------------------------------------------------- 1 | package lock 2 | 3 | type MutexTable interface { 4 | // Lock key 5 | // return that lock is succeeded 6 | Lock(key string) bool 7 | 8 | // Unlock key 9 | // return that unlock is succeeded 10 | Unlock(key string) bool 11 | 12 | // IsLocked return that key is locked 13 | IsLocked(key string) bool 14 | } 15 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/lock/lock.go: -------------------------------------------------------------------------------- 1 | package lock 2 | 3 | type MemoryMutexTable struct { 4 | table map[string]bool 5 | request chan mutexRequest 6 | } 7 | 8 | func NewMemoryMutexTable(requestBuffer int) *MemoryMutexTable { 9 | mt := &MemoryMutexTable{ 10 | table: make(map[string]bool), 11 | request: make(chan mutexRequest, requestBuffer), 12 | } 13 | 14 | go mt.mutexThread() 15 | 16 | return mt 17 | } 18 | 19 | func (mt *MemoryMutexTable) Lock(key string) bool { 20 | ch := make(chan mutexResult) 21 | defer close(ch) 22 | 23 | mt.request <- mutexRequest{ 24 | Key: key, 25 | Action: lock, 26 | Result: ch, 27 | } 28 | 29 | for r := range ch { 30 | return r.Succeeded 31 | } 32 | 33 | return false 34 | } 35 | 36 | func (mt *MemoryMutexTable) Unlock(key string) bool { 37 | ch := make(chan mutexResult) 38 | defer close(ch) 39 | 40 | mt.request <- mutexRequest{ 41 | Key: key, 42 | Action: unlock, 43 | Result: ch, 44 | } 45 | 46 | for r := range ch { 47 | return r.Succeeded 48 | } 49 | 50 | return false 51 | } 52 | 53 | func (mt MemoryMutexTable) IsLocked(key string) bool { 54 | ch := make(chan mutexResult) 55 | defer close(ch) 56 | 57 | mt.request <- mutexRequest{ 58 | Key: key, 59 | Action: isLocked, 60 | Result: ch, 61 | } 62 | 63 | for r := range ch { 64 | return r.Locked 65 | } 66 | 67 | return false 68 | } 69 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/lock/lock_test.go: -------------------------------------------------------------------------------- 1 | package lock 2 | 3 | import "testing" 4 | 5 | func TestMemoryMutexTable(t *testing.T) { 6 | mt := NewMemoryMutexTable(10000) 7 | 8 | if mt.IsLocked("test") { 9 | t.Errorf("precondition was locked") 10 | } 11 | if mt.Unlock("test") { 12 | t.Errorf("failed to unlock on precondition") 13 | } 14 | 15 | if !mt.Lock("test") { 16 | t.Errorf("failed to lock") 17 | } 18 | if !mt.IsLocked("test") { 19 | t.Errorf("is not locked after locked") 20 | } 21 | if mt.Lock("test") { 22 | t.Errorf("lock after locked") 23 | } 24 | 25 | if !mt.Unlock("test") { 26 | t.Errorf("failed to unlock") 27 | } 28 | if mt.IsLocked("test") { 29 | t.Errorf("is not unlocked after unlocked") 30 | } 31 | } 32 | 33 | func BenchmarkLock(b *testing.B) { 34 | mt := NewMemoryMutexTable(10000) 35 | 36 | b.ResetTimer() 37 | for i := 0; i < b.N; i++ { 38 | go mt.Lock("test") 39 | go mt.IsLocked("test") 40 | go mt.IsLocked("test") 41 | go mt.IsLocked("test") 42 | go mt.IsLocked("test") 43 | go mt.IsLocked("test") 44 | go mt.Unlock("test") 45 | go mt.IsLocked("test") 46 | go mt.IsLocked("test") 47 | go mt.IsLocked("test") 48 | go mt.IsLocked("test") 49 | go mt.IsLocked("test") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/lock/thread.go: -------------------------------------------------------------------------------- 1 | package lock 2 | 3 | type mutexAction int 4 | 5 | const ( 6 | lock mutexAction = iota 7 | unlock mutexAction = iota 8 | isLocked mutexAction = iota 9 | ) 10 | 11 | type mutexRequest struct { 12 | Key string 13 | Action mutexAction 14 | Result chan mutexResult 15 | } 16 | 17 | type mutexResult struct { 18 | Succeeded bool 19 | Locked bool 20 | } 21 | 22 | func (mt *MemoryMutexTable) mutexThread() { 23 | for req := range mt.request { 24 | succeeded := false 25 | 26 | switch req.Action { 27 | case lock: 28 | succeeded = mt.lock(req.Key) 29 | 30 | case unlock: 31 | succeeded = mt.unlock(req.Key) 32 | 33 | case isLocked: 34 | succeeded = true 35 | } 36 | 37 | req.Result <- mutexResult{ 38 | Succeeded: succeeded, 39 | Locked: mt.isLocked(req.Key), 40 | } 41 | } 42 | } 43 | 44 | func (mt *MemoryMutexTable) lock(key string) bool { 45 | if mt.isLocked(key) { 46 | return false 47 | } 48 | 49 | // raft consensus 50 | mt.table[key] = true 51 | 52 | return true 53 | } 54 | 55 | func (mt *MemoryMutexTable) unlock(key string) bool { 56 | if !mt.isLocked(key) { 57 | return false 58 | } 59 | 60 | // raft consensus 61 | delete(mt.table, key) 62 | 63 | return true 64 | } 65 | 66 | func (mt MemoryMutexTable) isLocked(key string) bool { 67 | if _, ok := mt.table[key]; ok { 68 | return true 69 | } 70 | 71 | return false 72 | } 73 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/lock/util.go: -------------------------------------------------------------------------------- 1 | package lock 2 | 3 | import "time" 4 | 5 | func WaitUntilLock(mutex MutexTable, key string, timeout, sleep time.Duration) bool { 6 | over := time.After(timeout) 7 | 8 | for { 9 | select { 10 | case <-over: 11 | return false 12 | 13 | default: 14 | if mutex.Lock(key) { 15 | return true 16 | } 17 | time.Sleep(sleep) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/lock/util_test.go: -------------------------------------------------------------------------------- 1 | package lock 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestWaitUntilLock(t *testing.T) { 9 | m := NewMemoryMutexTable(100) 10 | 11 | if !WaitUntilLock(m, "test", 50*time.Millisecond, 300*time.Millisecond) { 12 | t.Errorf("failed to lock") 13 | } 14 | if WaitUntilLock(m, "test", 50*time.Millisecond, 300*time.Millisecond) { 15 | t.Errorf("locked after locked") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/memory/store.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/pkg/errors" 7 | 8 | "github.com/golang/protobuf/proto" 9 | "github.com/n0stack/n0stack/n0core/pkg/datastore" 10 | "github.com/n0stack/n0stack/n0core/pkg/datastore/lock" 11 | ) 12 | 13 | type MemoryDatastore struct { 14 | // 本当は `proto.Message` を入れたいが、何故か中身がなかったのでとりあえずシリアライズする 15 | Data map[string][]byte 16 | 17 | mutex lock.MutexTable 18 | prefix string 19 | } 20 | 21 | func NewMemoryDatastore() *MemoryDatastore { 22 | return &MemoryDatastore{ 23 | Data: map[string][]byte{}, 24 | mutex: lock.NewMemoryMutexTable(10000), 25 | } 26 | } 27 | 28 | func (m *MemoryDatastore) AddPrefix(prefix string) datastore.Datastore { 29 | return &MemoryDatastore{ 30 | Data: m.Data, 31 | prefix: m.prefix + prefix + "/", 32 | mutex: lock.NewMemoryMutexTable(10000), 33 | } 34 | } 35 | 36 | func (m MemoryDatastore) List(f func(length int) []proto.Message) error { 37 | l := 0 38 | for k, _ := range m.Data { 39 | if strings.HasPrefix(k, m.prefix) { 40 | l++ 41 | } 42 | } 43 | 44 | pb := f(l) 45 | i := 0 46 | for k, v := range m.Data { 47 | if !strings.HasPrefix(k, m.prefix) { 48 | continue 49 | } 50 | 51 | err := proto.Unmarshal(v, pb[i]) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | i++ 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (m MemoryDatastore) Get(key string, pb proto.Message) error { 63 | v, ok := m.Data[m.prefix+key] 64 | if !ok { 65 | pb = nil 66 | return datastore.NewNotFound(key) 67 | } 68 | 69 | err := proto.Unmarshal(v, pb) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func (m *MemoryDatastore) Apply(key string, pb proto.Message) error { 78 | if !m.mutex.IsLocked(m.getKey(key)) { 79 | return errors.New("key is not locked") 80 | } 81 | 82 | s, err := proto.Marshal(pb) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | m.Data[m.getKey(key)] = s 88 | 89 | return nil 90 | } 91 | 92 | func (m *MemoryDatastore) Delete(key string) error { 93 | if !m.mutex.IsLocked(m.getKey(key)) { 94 | return errors.New("key is not locked") 95 | } 96 | 97 | var ok bool 98 | _, ok = m.Data[m.getKey(key)] 99 | if ok { 100 | delete(m.Data, m.getKey(key)) 101 | return nil 102 | } 103 | 104 | return datastore.NewNotFound(key) 105 | } 106 | 107 | func (m MemoryDatastore) getKey(key string) string { 108 | return m.prefix + key 109 | } 110 | 111 | func (m *MemoryDatastore) Lock(key string) bool { 112 | return m.mutex.Lock(m.getKey(key)) 113 | } 114 | func (m *MemoryDatastore) Unlock(key string) bool { 115 | return m.mutex.Unlock(m.getKey(key)) 116 | } 117 | func (m *MemoryDatastore) IsLocked(key string) bool { 118 | return m.mutex.IsLocked(m.getKey(key)) 119 | } 120 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/store/README.md: -------------------------------------------------------------------------------- 1 | # store 2 | 3 | ## benchmark 4 | 5 | - sqliteは過去の履歴にアクセスできるように実装したが,それにしても他の実装と比較して遅いことがわかる 6 | - leveldb は平均的に read, write の性能ともに良い 7 | - 耐障害性がよくわからないため,第十日どうか判断できていないがとりあえずleveldbを利用することにする 8 | 9 | ### memory read 10 | 11 | ``` 12 | goos: linux 13 | goarch: amd64 14 | pkg: github.com/n0stack/n0stack/n0core/pkg/datastore/store/memory 15 | BenchmarkMemoryStoreGet-8 20000000 59.2 ns/op 0 B/op 0 allocs/op 16 | PASS 17 | ok github.com/n0stack/n0stack/n0core/pkg/datastore/store/memory 5.438s 18 | Success: Benchmarks passed. 19 | ``` 20 | 21 | ### memory write 22 | 23 | ``` 24 | goos: linux 25 | goarch: amd64 26 | pkg: github.com/n0stack/n0stack/n0core/pkg/datastore/store/memory 27 | BenchmarkMemoryStoreApply-8 20000000 76.8 ns/op 7 B/op 1 allocs/op 28 | PASS 29 | ok github.com/n0stack/n0stack/n0core/pkg/datastore/store/memory 3.630s 30 | Success: Benchmarks passed. 31 | ``` 32 | 33 | ### sqlite read 34 | 35 | ``` 36 | goos: linux 37 | goarch: amd64 38 | pkg: github.com/n0stack/n0stack/n0core/pkg/datastore/store/sqlite 39 | BenchmarkSqliteStoreGet-8 30000 64989 ns/op 9757 B/op 198 allocs/op 40 | PASS 41 | ok github.com/n0stack/n0stack/n0core/pkg/datastore/store/sqlite 81.512s 42 | Success: Benchmarks passed. 43 | ``` 44 | 45 | ### sqlite write 46 | 47 | 48 | ``` 49 | goos: linux 50 | goarch: amd64 51 | pkg: github.com/n0stack/n0stack/n0core/pkg/datastore/store/sqlite 52 | BenchmarkSqliteStoreApply-8 1000 2184621 ns/op 7940 B/op 168 allocs/op 53 | PASS 54 | ok github.com/n0stack/n0stack/n0core/pkg/datastore/store/sqlite 2.421s 55 | Success: Benchmarks passed. 56 | ``` 57 | 58 | ### leveldb read 59 | 60 | ``` 61 | goos: linux 62 | goarch: amd64 63 | pkg: github.com/n0stack/n0stack/n0core/pkg/datastore/store/leveldb 64 | BenchmarkSqliteStoreApply-8 1000000 3615 ns/op 580 B/op 10 allocs/op 65 | PASS 66 | ok github.com/n0stack/n0stack/n0core/pkg/datastore/store/leveldb 7.887s 67 | Success: Benchmarks passed. 68 | ``` 69 | 70 | ### leveldb write 71 | 72 | ``` 73 | goos: linux 74 | goarch: amd64 75 | pkg: github.com/n0stack/n0stack/n0core/pkg/datastore/store/leveldb 76 | BenchmarkSqliteStoreApply-8 300000 4857 ns/op 287 B/op 4 allocs/op 77 | PASS 78 | ok github.com/n0stack/n0stack/n0core/pkg/datastore/store/leveldb 3.239s 79 | Success: Benchmarks passed. 80 | ``` -------------------------------------------------------------------------------- /n0core/pkg/datastore/store/errors.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type NotFoundError interface { 8 | IsNotFound() bool 9 | 10 | Error() string 11 | } 12 | 13 | type NotFound struct { 14 | key string 15 | } 16 | 17 | func NewNotFound(key string) *NotFound { 18 | return &NotFound{ 19 | key: key, 20 | } 21 | } 22 | 23 | func (e NotFound) IsNotFound() bool { 24 | return true 25 | } 26 | 27 | func (e NotFound) Error() string { 28 | return fmt.Sprintf("Key '%s' is not found", e.key) 29 | } 30 | 31 | func IsNotFound(err error) bool { 32 | if e, ok := err.(NotFoundError); ok { 33 | return e.IsNotFound() 34 | } 35 | 36 | return false 37 | } 38 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/store/interface.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | type Store interface { 4 | List() ([][]byte, error) 5 | Get(key string) ([]byte, error) 6 | Apply(key string, value []byte) error 7 | Delete(key string) error 8 | } 9 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/store/leveldb/store.go: -------------------------------------------------------------------------------- 1 | package leveldb 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/n0stack/n0stack/n0core/pkg/datastore/store" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/syndtr/goleveldb/leveldb" 10 | lerrors "github.com/syndtr/goleveldb/leveldb/errors" 11 | "github.com/syndtr/goleveldb/leveldb/util" 12 | ) 13 | 14 | type LeveldbStore struct { 15 | db *leveldb.DB 16 | 17 | prefix string 18 | } 19 | 20 | func NewLeveldbStore(directory string) (*LeveldbStore, error) { 21 | db, err := leveldb.OpenFile(directory, nil) 22 | if err != nil { 23 | return nil, errors.Wrap(err, "failed to connect database") 24 | } 25 | 26 | return &LeveldbStore{ 27 | db: db, 28 | }, nil 29 | } 30 | 31 | func (ds *LeveldbStore) Close() error { 32 | return ds.db.Close() 33 | } 34 | 35 | func (ds *LeveldbStore) AddPrefix(prefix string) *LeveldbStore { 36 | return &LeveldbStore{ 37 | db: ds.db, 38 | prefix: filepath.Join(ds.prefix, prefix), 39 | } 40 | } 41 | 42 | func (ds *LeveldbStore) List() ([][]byte, error) { 43 | res := make([][]byte, 0) 44 | 45 | iter := ds.db.NewIterator(util.BytesPrefix([]byte(ds.prefix)), nil) 46 | for iter.Next() { 47 | // NOTE: iter.Value() doesn't allocate new address for retrieved value, 48 | // so we need to copy it into new array 49 | v := make([]byte, len(iter.Value()), len(iter.Value())) 50 | copy(v, iter.Value()) 51 | res = append(res, v) 52 | } 53 | iter.Release() 54 | 55 | if err := iter.Error(); err != nil { 56 | return nil, err 57 | } 58 | 59 | return res, nil 60 | } 61 | 62 | func (ds *LeveldbStore) Get(key string) ([]byte, error) { 63 | v, err := ds.db.Get(ds.getKey(key), nil) 64 | if err != nil { 65 | if err == lerrors.ErrNotFound { 66 | return nil, store.NewNotFound(key) 67 | } 68 | 69 | return nil, err 70 | } 71 | 72 | return v, nil 73 | } 74 | 75 | func (ds *LeveldbStore) Apply(key string, value []byte) error { 76 | return ds.db.Put(ds.getKey(key), value, nil) 77 | } 78 | 79 | func (ds *LeveldbStore) Delete(key string) error { 80 | return ds.db.Delete(ds.getKey(key), nil) 81 | } 82 | 83 | func (ds LeveldbStore) getKey(key string) []byte { 84 | return []byte(filepath.Join(ds.prefix, key)) 85 | } 86 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/store/memory/store.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/n0stack/n0stack/n0core/pkg/datastore/store" 7 | ) 8 | 9 | var data map[string]map[string][]byte 10 | 11 | func init() { 12 | data = make(map[string]map[string][]byte) 13 | data[""] = make(map[string][]byte) 14 | } 15 | 16 | type MemoryStore struct { 17 | prefix string 18 | } 19 | 20 | func NewMemoryStore() *MemoryStore { 21 | return &MemoryStore{} 22 | } 23 | 24 | func (m *MemoryStore) AddPrefix(prefix string) *MemoryStore { 25 | p := filepath.Join(m.prefix, prefix) 26 | data[p] = make(map[string][]byte) 27 | 28 | return &MemoryStore{ 29 | prefix: p, 30 | } 31 | } 32 | 33 | func (m MemoryStore) List() ([][]byte, error) { 34 | l := make([][]byte, len(data[m.prefix])) 35 | i := 0 36 | for _, v := range data[m.prefix] { 37 | l[i] = v 38 | i++ 39 | } 40 | 41 | return l, nil 42 | } 43 | 44 | func (m MemoryStore) Get(key string) ([]byte, error) { 45 | v, ok := data[m.prefix][key] 46 | if !ok { 47 | return nil, store.NewNotFound(key) 48 | } 49 | 50 | return v, nil 51 | } 52 | 53 | func (m *MemoryStore) Apply(key string, value []byte) error { 54 | data[m.prefix][key] = value 55 | 56 | return nil 57 | } 58 | 59 | func (m *MemoryStore) Delete(key string) error { 60 | if !m.IsExisting(key) { 61 | return store.NewNotFound(key) 62 | } 63 | 64 | delete(data[m.prefix], key) 65 | return nil 66 | } 67 | 68 | func (m MemoryStore) IsExisting(key string) bool { 69 | _, ok := data[m.prefix][key] 70 | return ok 71 | } 72 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/store/sqlite/store.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "path/filepath" 5 | "time" 6 | 7 | "github.com/n0stack/n0stack/n0core/pkg/datastore/store" 8 | 9 | "github.com/jinzhu/gorm" 10 | _ "github.com/mattn/go-sqlite3" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type LogType int 15 | 16 | const ( 17 | LOGTYPE_APPLY LogType = iota 18 | LOGTYPE_DELETE 19 | ) 20 | 21 | type Log struct { 22 | ID uint `gorm:"primary_key"` 23 | CreatedAt time.Time `gorm:"index"` 24 | 25 | Type LogType 26 | Prefix string `gorm:"index:idx_prefix_key"` 27 | Key string `gorm:"index:idx_prefix_key"` 28 | Value []byte 29 | } 30 | 31 | type SqliteStore struct { 32 | db *gorm.DB 33 | 34 | prefix string 35 | } 36 | 37 | func NewSqliteStore(file string) (*SqliteStore, error) { 38 | db, err := gorm.Open("sqlite3", file) 39 | if err != nil { 40 | return nil, errors.Wrap(err, "failed to connect database") 41 | } 42 | 43 | db.AutoMigrate(&Log{}) 44 | 45 | return &SqliteStore{ 46 | db: db, 47 | }, nil 48 | } 49 | 50 | func (ds *SqliteStore) Close() error { 51 | return ds.db.Close() 52 | } 53 | 54 | func (ds *SqliteStore) AddPrefix(prefix string) *SqliteStore { 55 | return &SqliteStore{ 56 | db: ds.db, 57 | prefix: filepath.Join(ds.prefix, prefix), 58 | } 59 | } 60 | 61 | func (ds *SqliteStore) List() ([][]byte, error) { 62 | return nil, nil 63 | } 64 | 65 | func (ds *SqliteStore) Get(key string) ([]byte, error) { 66 | l := &Log{} 67 | if err := ds.db.Last(&l).Error; err != nil { 68 | if gorm.IsRecordNotFoundError(err) { 69 | return nil, store.NewNotFound(key) 70 | } 71 | 72 | return nil, err 73 | } 74 | 75 | if l.Type == LOGTYPE_DELETE { 76 | return nil, store.NewNotFound(key) 77 | } 78 | 79 | return l.Value, nil 80 | } 81 | 82 | func (ds *SqliteStore) Apply(key string, value []byte) error { 83 | return ds.db.Create(&Log{ 84 | Type: LOGTYPE_APPLY, 85 | Prefix: ds.prefix, 86 | Key: key, 87 | Value: value, 88 | }).Error 89 | } 90 | 91 | func (ds *SqliteStore) Delete(key string) error { 92 | return ds.db.Create(&Log{ 93 | Type: LOGTYPE_DELETE, 94 | Prefix: ds.prefix, 95 | Key: key, 96 | }).Error 97 | } 98 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/store/sqlite/store_test.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/n0stack/n0stack/n0core/pkg/datastore/store" 8 | ) 9 | 10 | const dbFile = "test.db" 11 | 12 | func TestSqliteStore(t *testing.T) { 13 | ds, err := NewSqliteStore("test.db") 14 | if err != nil { 15 | t.Fatalf("failed to generate sqlite datastore: %s", err.Error()) 16 | } 17 | defer os.Remove(dbFile) 18 | 19 | k := "key" 20 | v := []byte("value") 21 | 22 | if _, err := ds.Get(k); err == nil { 23 | t.Errorf("Get() does not return error, want NotFound") 24 | } else if !store.IsNotFound(err) { 25 | t.Errorf("Get() return wrong error, want NotFound: %s", err.Error()) 26 | } 27 | 28 | if err := ds.Apply(k, v); err != nil { 29 | t.Fatalf("failed to apply data: %s", err.Error()) 30 | } 31 | 32 | if b, err := ds.Get(k); err != nil { 33 | t.Errorf("failed to get stored data: %s", err.Error()) 34 | } else if string(v) != string(b) { 35 | t.Errorf("Get result is wrong: want=%s, have=%s", string(v), string(b)) 36 | } 37 | 38 | if err := ds.Delete(k); err != nil { 39 | t.Errorf("failed to delete data: %s", err.Error()) 40 | } 41 | if _, err := ds.Get(k); err == nil { 42 | t.Errorf("Get() does not return error, want NotFound") 43 | } else if !store.IsNotFound(err) { 44 | t.Errorf("Get() return wrong error, want NotFound: %s", err.Error()) 45 | } 46 | 47 | if err := ds.Close(); err != nil { 48 | t.Errorf("failed to close db: %s", err.Error()) 49 | } 50 | } 51 | 52 | func BenchmarkSqliteStoreApply(b *testing.B) { 53 | m, err := NewSqliteStore("test.db") 54 | if err != nil { 55 | b.Fatalf("failed to generate sqlite datastore: %s", err.Error()) 56 | } 57 | defer os.Remove(dbFile) 58 | 59 | k := "key" 60 | v := []byte("value") 61 | 62 | b.ResetTimer() 63 | for i := 0; i < b.N; i++ { 64 | key := k + string(i) 65 | m.Apply(key, v) 66 | } 67 | } 68 | 69 | func BenchmarkSqliteStoreDeleteAfterApply(b *testing.B) { 70 | m, err := NewSqliteStore("test.db") 71 | if err != nil { 72 | b.Fatalf("failed to generate sqlite datastore: %s", err.Error()) 73 | } 74 | defer os.Remove(dbFile) 75 | 76 | k := "key" 77 | v := []byte("value") 78 | 79 | b.ResetTimer() 80 | for i := 0; i < b.N; i++ { 81 | key := k + string(i) 82 | m.Apply(key, v) 83 | m.Delete(key) 84 | } 85 | } 86 | 87 | func BenchmarkSqliteStoreGet(b *testing.B) { 88 | m, err := NewSqliteStore("test.db") 89 | if err != nil { 90 | b.Fatalf("failed to generate sqlite datastore: %s", err.Error()) 91 | } 92 | defer os.Remove(dbFile) 93 | 94 | k := "key" 95 | v := []byte("value") 96 | 97 | for i := 0; i < b.N; i++ { 98 | key := k + string(i) 99 | m.Apply(key, v) 100 | } 101 | 102 | b.ResetTimer() 103 | for i := 0; i < b.N; i++ { 104 | key := k + string(i) 105 | m.Get(key) 106 | } 107 | } 108 | 109 | func BenchmarkSqliteStoreList(b *testing.B) { 110 | m, err := NewSqliteStore("test.db") 111 | if err != nil { 112 | b.Fatalf("failed to generate sqlite datastore: %s", err.Error()) 113 | } 114 | defer os.Remove(dbFile) 115 | 116 | k := "key" 117 | v := []byte("value") 118 | 119 | for i := 0; i < 100; i++ { 120 | key := k + string(i) 121 | m.Apply(key, v) 122 | } 123 | 124 | b.ResetTimer() 125 | for i := 0; i < b.N; i++ { 126 | m.List() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/test.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: pkg/datastore/test.proto 3 | 4 | package datastore 5 | 6 | import ( 7 | fmt "fmt" 8 | proto "github.com/golang/protobuf/proto" 9 | math "math" 10 | ) 11 | 12 | // Reference imports to suppress errors if they are not otherwise used. 13 | var _ = proto.Marshal 14 | var _ = fmt.Errorf 15 | var _ = math.Inf 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the proto package it is being compiled against. 19 | // A compilation error at this line likely means your copy of the 20 | // proto package needs to be updated. 21 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 22 | 23 | type Test struct { 24 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 25 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 26 | XXX_unrecognized []byte `json:"-"` 27 | XXX_sizecache int32 `json:"-"` 28 | } 29 | 30 | func (m *Test) Reset() { *m = Test{} } 31 | func (m *Test) String() string { return proto.CompactTextString(m) } 32 | func (*Test) ProtoMessage() {} 33 | func (*Test) Descriptor() ([]byte, []int) { 34 | return fileDescriptor_0f5896dd61bbe524, []int{0} 35 | } 36 | 37 | func (m *Test) XXX_Unmarshal(b []byte) error { 38 | return xxx_messageInfo_Test.Unmarshal(m, b) 39 | } 40 | func (m *Test) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 41 | return xxx_messageInfo_Test.Marshal(b, m, deterministic) 42 | } 43 | func (m *Test) XXX_Merge(src proto.Message) { 44 | xxx_messageInfo_Test.Merge(m, src) 45 | } 46 | func (m *Test) XXX_Size() int { 47 | return xxx_messageInfo_Test.Size(m) 48 | } 49 | func (m *Test) XXX_DiscardUnknown() { 50 | xxx_messageInfo_Test.DiscardUnknown(m) 51 | } 52 | 53 | var xxx_messageInfo_Test proto.InternalMessageInfo 54 | 55 | func (m *Test) GetName() string { 56 | if m != nil { 57 | return m.Name 58 | } 59 | return "" 60 | } 61 | 62 | func init() { 63 | proto.RegisterType((*Test)(nil), "n0stack.internal.n0core.datastore.Test") 64 | } 65 | 66 | func init() { proto.RegisterFile("pkg/datastore/test.proto", fileDescriptor_0f5896dd61bbe524) } 67 | 68 | var fileDescriptor_0f5896dd61bbe524 = []byte{ 69 | // 135 bytes of a gzipped FileDescriptorProto 70 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x28, 0xc8, 0x4e, 0xd7, 71 | 0x4f, 0x49, 0x2c, 0x49, 0x2c, 0x2e, 0xc9, 0x2f, 0x4a, 0xd5, 0x2f, 0x49, 0x2d, 0x2e, 0xd1, 0x2b, 72 | 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x52, 0xcc, 0x33, 0x28, 0x2e, 0x49, 0x4c, 0xce, 0xd6, 0xcb, 0xcc, 73 | 0x2b, 0x49, 0x2d, 0xca, 0x4b, 0xcc, 0xd1, 0xcb, 0x33, 0x48, 0xce, 0x2f, 0x4a, 0xd5, 0x83, 0xab, 74 | 0x56, 0x92, 0xe2, 0x62, 0x09, 0x49, 0x2d, 0x2e, 0x11, 0x12, 0xe2, 0x62, 0xc9, 0x4b, 0xcc, 0x4d, 75 | 0x95, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x02, 0xb3, 0x9d, 0xac, 0xa3, 0x2c, 0xd3, 0x33, 0x4b, 76 | 0x32, 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0xa1, 0x66, 0x21, 0xd1, 0x20, 0xa3, 0xf4, 0x51, 77 | 0x2c, 0xb7, 0x86, 0xb3, 0x92, 0xd8, 0xc0, 0x4e, 0x30, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x3a, 78 | 0x5c, 0xf6, 0xf1, 0x9e, 0x00, 0x00, 0x00, 79 | } 80 | -------------------------------------------------------------------------------- /n0core/pkg/datastore/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/n0stack/n0stack/n0core/pkg/datastore;datastore"; 4 | 5 | package n0stack.internal.n0core.datastore; 6 | 7 | message Test { 8 | string name = 1; 9 | } 10 | -------------------------------------------------------------------------------- /n0core/pkg/deploy/agent.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/coreos/go-systemd/unit" 7 | ) 8 | 9 | func (d LocalDeployer) CreateAgentUnit(command string) []byte { 10 | u := []*unit.UnitOption{ 11 | { 12 | Section: "Unit", 13 | Name: "Description", 14 | Value: "n0core agent: The n0stack cluster node", 15 | }, 16 | { 17 | Section: "Unit", 18 | Name: "Documentation", 19 | Value: "https://github.com/n0stack/n0stack", 20 | }, 21 | // { 22 | // Section: "Service", 23 | // Name: "Environment", 24 | // Value: "", 25 | // }, 26 | { 27 | Section: "Service", 28 | Name: "ExecStart", 29 | Value: command, 30 | }, 31 | { 32 | Section: "Service", 33 | Name: "Restart", 34 | Value: "always", 35 | }, 36 | { 37 | Section: "Service", 38 | Name: "StartLimitInterval", 39 | Value: "0", 40 | }, 41 | { 42 | Section: "Service", 43 | Name: "RestartSec", 44 | Value: "10", 45 | }, 46 | { 47 | Section: "Service", 48 | Name: "KillMode", 49 | Value: "process", 50 | }, 51 | { 52 | Section: "Service", 53 | Name: "TasksMax", 54 | Value: "infinity", 55 | }, 56 | { 57 | Section: "Install", 58 | Name: "WantedBy", 59 | Value: "multi-user.target", 60 | }, 61 | } 62 | 63 | buf := new(bytes.Buffer) 64 | buf.ReadFrom(unit.Serialize(u)) 65 | return buf.Bytes() 66 | } 67 | -------------------------------------------------------------------------------- /n0core/pkg/deploy/agent_test.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | func TestCreateAgentUnit(t *testing.T) { 10 | d := &LocalDeployer{ 11 | targetDirectory: "/var/lib/n0core", 12 | } 13 | 14 | have := string(d.CreateAgentUnit("/var/lib/n0core/n0core agent hogehoge")) 15 | want := `[Unit] 16 | Description=n0core agent: The n0stack cluster node 17 | Documentation=https://github.com/n0stack/n0stack 18 | 19 | [Service] 20 | ExecStart=/var/lib/n0core/n0core agent hogehoge 21 | Restart=always 22 | StartLimitInterval=0 23 | RestartSec=10 24 | KillMode=process 25 | TasksMax=infinity 26 | 27 | [Install] 28 | WantedBy=multi-user.target 29 | ` 30 | 31 | if diff := cmp.Diff(have, want); diff != "" { 32 | t.Errorf("CreateAgentUnit response is wrong: diff=(-want +have)\n%s", diff) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /n0core/pkg/deploy/local_test.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestLocalDeployer(t *testing.T) { 9 | t.Skip() // OS依存で少し面倒なため 10 | 11 | d, err := NewLocalDeployer(".") 12 | if err != nil { 13 | t.Fatalf("Failed to create new local deployer ") 14 | } 15 | 16 | if err := d.InstallPackages([]string{"nano"}, os.Stdout, os.Stderr); err != nil { 17 | t.Errorf("Failed to InstallPackages: err='%s'", err.Error()) 18 | } 19 | 20 | testPath := "test" 21 | if err := d.SaveFile([]byte("test"), testPath, 0644); err != nil { 22 | t.Errorf("Failed to SaveFile: err='%s'", err.Error()) 23 | } else { 24 | if err := os.Remove(testPath); err != nil { 25 | t.Fatalf("Failed to Remove test environment: err='%s'", err.Error()) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /n0core/pkg/deploy/remote_test.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import "testing" 4 | 5 | func TestReadSelf(t *testing.T) { 6 | d := &RemoteDeployer{ 7 | targetDirectory: "/var/lib/n0core", 8 | } 9 | 10 | if buf, err := d.ReadSelf(); err != nil { 11 | t.Errorf("Failed to read self: err=%s'", err.Error()) 12 | } else if len(buf) < 1 { 13 | t.Errorf("Length of buffer is not large, maybe wrong: len=%d'", len(buf)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /n0core/pkg/driver/README.md: -------------------------------------------------------------------------------- 1 | # Driver 2 | 3 | - 副作用を管理 4 | - ロジックは可能な限り除く 5 | 6 | ## Test 7 | 8 | - 正常系のみを行う 9 | - 原状復帰すること 10 | - `Create -> Delete` みたいな 11 | -------------------------------------------------------------------------------- /n0core/pkg/driver/cloudinit/README.md: -------------------------------------------------------------------------------- 1 | # Cloud-init 2 | 3 | 6 | 7 | ## Dependency packages 8 | 9 | - genisoimage 10 | 11 | ```sh 12 | apt install -y \ 13 | genisoimage 14 | ``` 15 | 16 | ## Support distributions 17 | 18 | - Ubuntu 18.04 19 | - CentOS 7 20 | -------------------------------------------------------------------------------- /n0core/pkg/driver/cloudinit/configdrive/config_test.go: -------------------------------------------------------------------------------- 1 | package configdrive 2 | 3 | import ( 4 | "io/ioutil" 5 | "net" 6 | "strings" 7 | "testing" 8 | 9 | img "github.com/n0stack/n0stack/n0core/pkg/driver/qemu_img" 10 | netutil "github.com/n0stack/n0stack/n0core/pkg/util/net" 11 | 12 | "golang.org/x/crypto/ssh" 13 | ) 14 | 15 | func TestGenerateISO(t *testing.T) { 16 | key, _, _, _, _ := ssh.ParseAuthorizedKey([]byte("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBITowPn2Ol1eCvXN5XV+Lb6jfXzgDbXyEdtayadDUJtFrcN2m2mjC1B20VBAoJcZtSYkmjrllS06Q26Te5sTYvE= testkey")) 17 | i := netutil.ParseCIDR("192.168.122.10/24") 18 | hw, _ := net.ParseMAC("52:54:00:78:71:f1") 19 | e := &CloudConfigEthernet{ 20 | MacAddress: hw, 21 | Address4: i, 22 | Gateway4: net.ParseIP("192.168.122.1"), 23 | NameServers: []net.IP{ 24 | net.ParseIP("192.168.122.1"), 25 | }, 26 | } 27 | c := StructConfig("user", "host", []ssh.PublicKey{key}, []*CloudConfigEthernet{e}) 28 | 29 | f, err := c.Generate(".") 30 | if err != nil { 31 | t.Errorf("Failed to generate iso: err='%s'", err.Error()) 32 | } 33 | 34 | _, err = img.OpenQemuImg(f) 35 | if err != nil { 36 | t.Errorf("Failed to open qemu image, maybe this is not valid image: path='%s', err='%s'", f, err.Error()) 37 | } 38 | 39 | if err := c.Delete(); err != nil { 40 | t.Errorf("Failed to delete: err='%s'", err.Error()) 41 | } 42 | } 43 | 44 | func TestTRUNCFile(t *testing.T) { 45 | key, _, _, _, _ := ssh.ParseAuthorizedKey([]byte("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBITowPn2Ol1eCvXN5XV+Lb6jfXzgDbXyEdtayadDUJtFrcN2m2mjC1B20VBAoJcZtSYkmjrllS06Q26Te5sTYvE= testkey")) 46 | i := netutil.ParseCIDR("192.168.122.10/24") 47 | hw, _ := net.ParseMAC("52:54:00:78:71:f1") 48 | e := &CloudConfigEthernet{ 49 | MacAddress: hw, 50 | Address4: i, 51 | Gateway4: net.ParseIP("192.168.122.1"), 52 | NameServers: []net.IP{ 53 | net.ParseIP("192.168.122.1"), 54 | }, 55 | } 56 | c := StructConfig("user", "host", []ssh.PublicKey{key}, []*CloudConfigEthernet{e}) 57 | 58 | padding := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 59 | 60 | err := ioutil.WriteFile("meta-data", []byte(padding), 0664) 61 | if err != nil { 62 | t.Fatalf("Failed to write precondition: err='%s'", err.Error()) 63 | } 64 | 65 | if err := c.GenerateMetadataFile("."); err != nil { 66 | t.Fatalf("Failed to generate metadata file: err='%s'", err.Error()) 67 | } 68 | defer c.Delete() 69 | 70 | data, _ := ioutil.ReadFile("meta-data") 71 | if len(data) != 39 { 72 | t.Errorf("GenerateMetadataFile was wrong") 73 | } 74 | } 75 | 76 | func TestGenerateNetworkConfigFileAboutIPAddressIsMissing(t *testing.T) { 77 | key, _, _, _, _ := ssh.ParseAuthorizedKey([]byte("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBITowPn2Ol1eCvXN5XV+Lb6jfXzgDbXyEdtayadDUJtFrcN2m2mjC1B20VBAoJcZtSYkmjrllS06Q26Te5sTYvE= testkey")) 78 | hw, _ := net.ParseMAC("52:54:00:78:71:f1") 79 | e := &CloudConfigEthernet{ 80 | MacAddress: hw, 81 | } 82 | c := StructConfig("user", "host", []ssh.PublicKey{key}, []*CloudConfigEthernet{e}) 83 | 84 | if err := c.GenerateNetworkConfigFile("."); err != nil { 85 | t.Fatalf("Failed to generate network config file: err='%s'", err.Error()) 86 | } 87 | defer c.Delete() 88 | 89 | data, _ := ioutil.ReadFile("network-config") 90 | if strings.Contains(string(data), "null") { 91 | t.Errorf("Failed to valid network file, subnets have null") 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /n0core/pkg/driver/cloudinit/configdrive/test_id_ecdsa: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIBAQh+adEg/rjqj9qLE0jI4EqV8kZFDzWTASAwvx6HWdoAoGCCqGSM49 3 | AwEHoUQDQgAEhOjA+fY6XV4K9c3ldX4tvqN9fOANtfIR21rJp0NQm0Wtw3abaaML 4 | UHbRUECglxm1JiSaOuWVLTpDbpN7mxNi8Q== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /n0core/pkg/driver/cloudinit/configdrive/test_id_ecdsa.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBITowPn2Ol1eCvXN5XV+Lb6jfXzgDbXyEdtayadDUJtFrcN2m2mjC1B20VBAoJcZtSYkmjrllS06Q26Te5sTYvE= testkey 2 | -------------------------------------------------------------------------------- /n0core/pkg/driver/dnsmasq/README.md: -------------------------------------------------------------------------------- 1 | # Dnsmasq 2 | 3 | 6 | 7 | ## Dependency packages 8 | 9 | - dnsmasq 10 | 11 | ```sh 12 | apt install -y \ 13 | dnsmasq 14 | ``` 15 | -------------------------------------------------------------------------------- /n0core/pkg/driver/iproute2/README.md: -------------------------------------------------------------------------------- 1 | # QEMU 2 | 3 | ## Dependency packages 4 | 5 | - iproute2 6 | 7 | ```sh 8 | apt install -y \ 9 | iproute2 10 | ``` 11 | -------------------------------------------------------------------------------- /n0core/pkg/driver/iproute2/bridge_test.go: -------------------------------------------------------------------------------- 1 | // +build medium 2 | // +build !without_root 3 | 4 | package iproute2 5 | 6 | import "testing" 7 | 8 | func TestBridge(t *testing.T) { 9 | b, err := NewBridge("test") 10 | if err != nil { 11 | t.Fatalf("Failed to create bridge: err='%s'", err.Error()) 12 | } 13 | 14 | if err := b.Up(); err != nil { 15 | t.Errorf("Failed to up bridge: err='%s'", err.Error()) 16 | } 17 | 18 | if err := b.SetAddress("10.255.255.1/24"); err != nil { 19 | t.Errorf("Failed to set address: err='%s'", err.Error()) 20 | } 21 | 22 | if _, err := b.GetIPv4(); err != nil { 23 | t.Errorf("Failed to get address: err='%s'", err.Error()) 24 | } 25 | 26 | if err := b.Delete(); err != nil { 27 | t.Errorf("Failed to delete bridge: err='%s'", err.Error()) 28 | } 29 | } 30 | 31 | func TestExistingBridge(t *testing.T) { 32 | b, err := NewBridge("test") 33 | if err != nil { 34 | t.Fatalf("Failed to create bridge: err='%s'", err.Error()) 35 | } 36 | defer b.Delete() 37 | 38 | if _, err := NewBridge("test"); err != nil { 39 | t.Fatalf("Failed to find existing bridge: err='%s'", err.Error()) 40 | } 41 | } 42 | 43 | func TestListSlaves(t *testing.T) { 44 | b, err := NewBridge("br-test") 45 | if err != nil { 46 | t.Fatalf("Failed to create bridge: err='%s'", err.Error()) 47 | } 48 | defer b.Delete() 49 | 50 | nt, err := NewTap("tap-test") 51 | if err != nil { 52 | t.Fatalf("Failed to create tap: err='%s'", err.Error()) 53 | } 54 | defer nt.Delete() 55 | 56 | if err := nt.SetMaster(b); err != nil { 57 | t.Errorf("Failed to set master: err='%s'", err.Error()) 58 | } 59 | 60 | if l, err := b.ListSlaves(); err != nil { 61 | t.Errorf("Failed to list slaves: err='%s'", err.Error()) 62 | } else if len(l) != 1 { 63 | t.Errorf("Wrong the number of slaves: want='%d', have='%d'", 1, len(l)) 64 | } else if l[0] != nt.Name() { 65 | t.Errorf("Got wrong link about index: want='%s', have='%s'", nt.Name(), l[0]) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /n0core/pkg/driver/iproute2/interface.go: -------------------------------------------------------------------------------- 1 | package iproute2 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/vishvananda/netlink" 7 | ) 8 | 9 | type Interface struct { 10 | name string 11 | link netlink.Link // TODO 12 | } 13 | 14 | func GetInterface(name string) (*Interface, error) { 15 | i := &Interface{ 16 | name: name, 17 | } 18 | 19 | var err error 20 | i.link, err = netlink.LinkByName(name) 21 | if err != nil { 22 | return nil, fmt.Errorf("Failed to find interface: name='%s', err='%s'", i.name, err.Error()) 23 | } 24 | 25 | return i, nil 26 | } 27 | 28 | func (i Interface) Name() string { 29 | return i.name 30 | } 31 | 32 | func (i Interface) Up() error { 33 | if err := netlink.LinkSetUp(i.link); err != nil { 34 | return fmt.Errorf("Failed 'ip link set dev %s up': err='%s'", i.name, err.Error()) 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (i *Interface) SetMaster(b *Bridge) error { 41 | if err := netlink.SetPromiscOn(i.link); err != nil { 42 | return fmt.Errorf("Failed 'ip link set dev %s promisc on': err='%s'", i.name, err.Error()) 43 | } 44 | 45 | if err := netlink.LinkSetMaster(i.link, b.link); err != nil { 46 | return fmt.Errorf("Failed ip link set dev %s master %s': err='%s'", i.name, b.name, err.Error()) 47 | } 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /n0core/pkg/driver/iproute2/tap.go: -------------------------------------------------------------------------------- 1 | package iproute2 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/vishvananda/netlink" 7 | ) 8 | 9 | type Tap struct { 10 | name string 11 | link netlink.Link // TODO 12 | } 13 | 14 | func NewTap(name string) (*Tap, error) { 15 | t := &Tap{ 16 | name: name, 17 | } 18 | 19 | var err error 20 | t.link, err = netlink.LinkByName(name) 21 | if err != nil { 22 | if err := t.createTap(); err != nil { 23 | return nil, err 24 | } 25 | } 26 | 27 | if t.link.Type() != "tun" && t.link.Type() != "tuntap" { 28 | return nil, fmt.Errorf("Interface '%s' is not 'tun', but '%s'", name, t.link.Type()) 29 | } 30 | 31 | return t, nil 32 | } 33 | 34 | func (t *Tap) createTap() error { 35 | l := netlink.NewLinkAttrs() 36 | l.Name = t.name 37 | t.link = &netlink.Tuntap{ 38 | LinkAttrs: l, 39 | Mode: netlink.TUNTAP_MODE_TAP, 40 | } 41 | 42 | if err := netlink.LinkAdd(t.link); err != nil { 43 | return fmt.Errorf("Failed 'ip tuntap add name %s mode tap': err='%s'", t.name, err.Error()) 44 | } 45 | 46 | if err := t.Up(); err != nil { 47 | return err 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (t Tap) Name() string { 54 | return t.name 55 | } 56 | 57 | func (t *Tap) Up() error { 58 | if err := netlink.LinkSetUp(t.link); err != nil { 59 | return fmt.Errorf("Failed 'ip link set dev %s up': err='%s'", t.name, err.Error()) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (t *Tap) SetMaster(b *Bridge) error { 66 | if err := netlink.LinkSetMaster(t.link, b.link); err != nil { 67 | return fmt.Errorf("Failed ip link set dev %s master %s': err='%s'", t.name, b.name, err.Error()) 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (t *Tap) Delete() error { 74 | if err := netlink.LinkDel(t.link); err != nil { 75 | return fmt.Errorf("Failed 'ip link del %s type bridge': err='%s'", t.name, err.Error()) 76 | } 77 | 78 | t.link = nil 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /n0core/pkg/driver/iproute2/tap_test.go: -------------------------------------------------------------------------------- 1 | // +build medium 2 | // +build !without_root 3 | 4 | package iproute2 5 | 6 | import "testing" 7 | 8 | func TestTap(t *testing.T) { 9 | nt, err := NewTap("test") 10 | if err != nil { 11 | t.Fatalf("Failed to create tap: err='%s'", err.Error()) 12 | } 13 | 14 | if err := nt.Up(); err != nil { 15 | t.Errorf("Failed to up tap: err='%s'", err.Error()) 16 | } 17 | 18 | if err := nt.Delete(); err != nil { 19 | t.Errorf("Failed to delete tap: err='%s'", err.Error()) 20 | } 21 | } 22 | 23 | func TestExistingTap(t *testing.T) { 24 | nt, err := NewTap("test") 25 | if err != nil { 26 | t.Fatalf("Failed to create tap: err='%s'", err.Error()) 27 | } 28 | defer nt.Delete() 29 | 30 | if _, err := NewTap("test"); err != nil { 31 | t.Errorf("Failed to find existing tap: err='%s'", err.Error()) 32 | } 33 | } 34 | 35 | func TestTapSetMasterAsBridge(t *testing.T) { 36 | b, err := NewBridge("br-test") 37 | if err != nil { 38 | t.Fatalf("Failed to create bridge: err='%s'", err.Error()) 39 | } 40 | defer b.Delete() 41 | 42 | nt, err := NewTap("tap-test") 43 | if err != nil { 44 | t.Fatalf("Failed to create tap: err='%s'", err.Error()) 45 | } 46 | defer nt.Delete() 47 | 48 | if err := nt.SetMaster(b); err != nil { 49 | t.Errorf("Failed to set master: err='%s'", err.Error()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /n0core/pkg/driver/iproute2/vlan.go: -------------------------------------------------------------------------------- 1 | package iproute2 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/vishvananda/netlink" 7 | ) 8 | 9 | type Vlan struct { 10 | name string 11 | id int 12 | i *Interface 13 | 14 | link netlink.Link 15 | } 16 | 17 | func NewVlan(i *Interface, id int) (*Vlan, error) { 18 | v := &Vlan{ 19 | name: fmt.Sprintf("%s.%d", i.Name(), id), 20 | id: id, 21 | i: i, 22 | } 23 | 24 | l, err := netlink.LinkByName(v.name) 25 | if err != nil { 26 | if err := v.createVlan(); err != nil { 27 | return nil, err 28 | } 29 | 30 | return v, nil 31 | } 32 | 33 | v.link = l 34 | 35 | return v, nil 36 | } 37 | 38 | // ip link add $name type vlan id $id 39 | func (v *Vlan) createVlan() error { 40 | l := netlink.NewLinkAttrs() 41 | l.Name = v.name 42 | l.ParentIndex = v.i.link.Attrs().Index 43 | v.link = &netlink.Vlan{ 44 | LinkAttrs: l, 45 | VlanId: v.id, 46 | } 47 | 48 | if err := netlink.LinkAdd(v.link); err != nil { 49 | return fmt.Errorf("Failed to command 'ip link add link %s name %s type vlan id %d': err='%s'", v.i.Name(), v.name, v.id, err.Error()) 50 | } 51 | 52 | if err := v.Up(); err != nil { 53 | return err 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func (v *Vlan) Name() string { 60 | return v.name 61 | } 62 | 63 | // ip link set dev $name up 64 | func (v *Vlan) Up() error { 65 | if err := netlink.LinkSetUp(v.link); err != nil { 66 | return fmt.Errorf("Failed to command 'ip link set dev %s up': err='%s'", v.name, err.Error()) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func (v *Vlan) SetMaster(b *Bridge) error { 73 | if err := netlink.LinkSetMaster(v.link, b.link); err != nil { 74 | return fmt.Errorf("Failed to command 'ip link set dev %s master %s': err='%s'", v.name, b.name, err.Error()) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func (v *Vlan) Delete() error { 81 | if err := netlink.LinkDel(v.link); err != nil { 82 | return fmt.Errorf("Failed 'ip link del %s type bridge': err='%s'", v.name, err.Error()) 83 | } 84 | 85 | v.link = nil 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /n0core/pkg/driver/iproute2/vlan_test.go: -------------------------------------------------------------------------------- 1 | package iproute2 2 | 3 | // TODO: テスト環境に大きく依存するため、テストがかけない 4 | -------------------------------------------------------------------------------- /n0core/pkg/driver/iptables/masquerade.go: -------------------------------------------------------------------------------- 1 | package iptables 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/coreos/go-iptables/iptables" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func structMasqueradeRule(bridgeName string, network *net.IPNet) (string, string, []string) { 11 | return "nat", "POSTROUTING", []string{ 12 | "!", 13 | "-o", 14 | bridgeName, 15 | "-s", 16 | network.String(), 17 | "-j", 18 | "MASQUERADE", 19 | } 20 | } 21 | 22 | func CreateMasqueradeRule(bridgeName string, network *net.IPNet) error { 23 | ipt, err := iptables.New() 24 | if err != nil { 25 | return errors.Wrapf(err, "Failed to create iptables instance") 26 | } 27 | 28 | // iptables -t nat -A POSTROUTING -o br0 -s 192.168.0.0/24 -j MASQUERADE 29 | table, chain, rule := structMasqueradeRule(bridgeName, network) 30 | 31 | if exists, err := ipt.Exists(table, chain, rule...); err != nil { 32 | return errors.Wrapf(err, "Failed to check rule") 33 | } else if exists { 34 | return nil 35 | } 36 | 37 | if err := ipt.Insert(table, chain, 1, rule...); err != nil { 38 | return errors.Wrapf(err, "Failed to create iptables instance: rule='%s'", rule) 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func DeleteMasqueradeRule(bridgeName string, network *net.IPNet) error { 45 | ipt, err := iptables.New() 46 | if err != nil { 47 | return errors.Wrapf(err, "Failed to create iptables instance") 48 | } 49 | 50 | // iptables -t nat -A POSTROUTING -o br0 -s 192.168.0.0/24 -j MASQUERADE 51 | table, chain, rule := structMasqueradeRule(bridgeName, network) 52 | 53 | if exists, err := ipt.Exists(table, chain, rule...); err != nil { 54 | return errors.Wrapf(err, "Failed to check rule") 55 | } else if !exists { 56 | return nil 57 | } 58 | 59 | if err := ipt.Delete(table, chain, rule...); err != nil { 60 | return errors.Wrapf(err, "Failed to delete iptables instance: rule='%s'", rule) 61 | } 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /n0core/pkg/driver/iptables/masquerade_test.go: -------------------------------------------------------------------------------- 1 | // +build medium 2 | 3 | package iptables 4 | 5 | import ( 6 | "net" 7 | "testing" 8 | 9 | "github.com/n0stack/n0stack/n0core/pkg/driver/iproute2" 10 | ) 11 | 12 | func TestMasquerade(t *testing.T) { 13 | b, err := iproute2.NewBridge("masq-br") 14 | if err != nil { 15 | t.Fatalf("failed to create bridge: err='%s'", err.Error()) 16 | } 17 | defer b.Delete() 18 | 19 | if err := b.SetAddress("172.31.255.254/24"); err != nil { 20 | t.Fatalf("failed to set address to bridge: err='%s'", err.Error()) 21 | } 22 | 23 | _, n, _ := net.ParseCIDR("172.31.255.0/24") 24 | if err := CreateMasqueradeRule(b.Name(), n); err != nil { 25 | t.Errorf("Failed to create masquerade rule: err='%s'", err.Error()) 26 | } 27 | 28 | if err := DeleteMasqueradeRule(b.Name(), n); err != nil { 29 | t.Errorf("Failed to delete masquerade rule: err='%s'", err.Error()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /n0core/pkg/driver/n0deploy/local.go: -------------------------------------------------------------------------------- 1 | package n0deploy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | func NewLocalParser() *Parser { 12 | return &Parser{ 13 | NewCopyInstruction: NewLocalCopyInstruction, 14 | NewRunInstruction: NewLocalRunInstruction, 15 | } 16 | } 17 | 18 | type LocalRunInstruction struct { 19 | command string 20 | } 21 | 22 | func NewLocalRunInstruction(line string) (Instruction, error) { 23 | return &LocalRunInstruction{ 24 | command: strings.TrimPrefix(line, "RUN "), 25 | }, nil 26 | } 27 | 28 | func (i LocalRunInstruction) Do(ctx context.Context, out io.Writer) error { 29 | cmd := exec.CommandContext(ctx, "sh", "-c", i.command) 30 | cmd.Stdout = out 31 | cmd.Stderr = out 32 | 33 | if err := cmd.Run(); err != nil { 34 | return err 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (i LocalRunInstruction) String() string { 41 | return fmt.Sprintf("RUN %s", i.command) 42 | } 43 | 44 | type LocalCopyInstruction struct { 45 | src string 46 | dst string 47 | } 48 | 49 | func NewLocalCopyInstruction(src, dst string) (Instruction, error) { 50 | return &LocalCopyInstruction{ 51 | src: src, 52 | dst: dst, 53 | }, nil 54 | } 55 | 56 | func (i LocalCopyInstruction) Do(ctx context.Context, out io.Writer) error { 57 | cmd := exec.CommandContext(ctx, "cp", i.src, i.dst) 58 | cmd.Stdout = out 59 | cmd.Stderr = out 60 | 61 | if err := cmd.Run(); err != nil { 62 | return err 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (i LocalCopyInstruction) String() string { 69 | return fmt.Sprintf("COPY %s %s", i.src, i.dst) 70 | } 71 | -------------------------------------------------------------------------------- /n0core/pkg/driver/n0deploy/parse.go: -------------------------------------------------------------------------------- 1 | package n0deploy 2 | 3 | import ( 4 | "context" 5 | "encoding/csv" 6 | "fmt" 7 | "io" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // ValidateCopy return source and destination to copy file 15 | func ValidateCopy(line string) (string, string, error) { 16 | r := csv.NewReader(strings.NewReader(line)) 17 | r.Comma = ' ' 18 | args, err := r.Read() 19 | if err != nil { 20 | return "", "", errors.Wrap(err, "Failed to parse as csv, csv package is used for splitting a string at Space except inside quotation marks") // https://stackoverflow.com/questions/47489745/splitting-a-string-at-space-except-inside-quotation-marks-go 21 | } 22 | 23 | if len(args) != 3 { 24 | return "", "", fmt.Errorf("COPY takes 2 arguments") 25 | } 26 | if strings.HasPrefix(filepath.Clean(args[1]), "..") { 27 | return "", "", fmt.Errorf("COPY source must not refer to the parent directory") 28 | } 29 | 30 | if !filepath.IsAbs(args[2]) { 31 | return "", "", fmt.Errorf("Set an absolute path for COPY destination") 32 | } 33 | 34 | return args[1], args[2], nil 35 | } 36 | 37 | type N0deploy struct { 38 | Bootstrap []Instruction 39 | Deploy []Instruction 40 | } 41 | 42 | type BlockType int 43 | 44 | const ( 45 | BlockType_BOOTSTRAP BlockType = iota 46 | BlockType_DEPLOY 47 | ) 48 | 49 | type Instruction interface { 50 | Do(ctx context.Context, out io.Writer) error 51 | String() string 52 | } 53 | 54 | type Parser struct { 55 | NewRunInstruction func(command string) (Instruction, error) 56 | NewCopyInstruction func(src, dst string) (Instruction, error) 57 | } 58 | 59 | func (p Parser) Parse(src string) (*N0deploy, error) { 60 | src = strings.Replace(src, "\\\n", "", -1) 61 | lines := strings.Split(src, "\n") 62 | 63 | ins := map[BlockType][]Instruction{ 64 | BlockType_BOOTSTRAP: make([]Instruction, 0, len(lines)), 65 | BlockType_DEPLOY: make([]Instruction, 0, len(lines)), 66 | } 67 | block := BlockType_BOOTSTRAP 68 | 69 | for _, l := range lines { 70 | l = strings.TrimSpace(l) 71 | l = strings.Trim(l, "\t") 72 | 73 | if len(l) == 0 { 74 | continue 75 | } 76 | 77 | switch { 78 | case strings.HasPrefix(l, "RUN"): 79 | i, err := p.NewRunInstruction(strings.TrimPrefix(l, "RUN ")) 80 | if err != nil { 81 | return nil, errors.Wrapf(err, "Failed to parse '%s'", l) 82 | } 83 | 84 | ins[block] = append(ins[block], i) 85 | 86 | case strings.HasPrefix(l, "COPY"): 87 | src, dst, err := ValidateCopy(l) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | i, err := p.NewCopyInstruction(src, dst) 93 | if err != nil { 94 | return nil, errors.Wrapf(err, "Failed to parse '%s'", l) 95 | } 96 | 97 | ins[block] = append(ins[block], i) 98 | 99 | case strings.HasPrefix(l, "BOOTSTRAP"): 100 | if len(ins[BlockType_BOOTSTRAP]) != 0 { 101 | return nil, errors.New("BOOTSTAP block is duplicated") 102 | } 103 | 104 | block = BlockType_BOOTSTRAP 105 | 106 | case strings.HasPrefix(l, "DEPLOY"): 107 | if len(ins[BlockType_DEPLOY]) != 0 { 108 | return nil, errors.New("DEPLOY block is duplicated") 109 | } 110 | 111 | block = BlockType_DEPLOY 112 | 113 | default: 114 | return nil, fmt.Errorf("Failed to parse '%s': the instruction does not exist", l) 115 | } 116 | } 117 | 118 | return &N0deploy{ 119 | Bootstrap: ins[BlockType_BOOTSTRAP], 120 | Deploy: ins[BlockType_DEPLOY], 121 | }, nil 122 | } 123 | -------------------------------------------------------------------------------- /n0core/pkg/driver/n0deploy/parse_test.go: -------------------------------------------------------------------------------- 1 | package n0deploy 2 | 3 | import "testing" 4 | 5 | func TestParse(t *testing.T) { 6 | parser := NewLocalParser() 7 | 8 | n0dep, err := parser.Parse(` 9 | RUN git clone ... \ 10 | && apt update && apt install -y ... 11 | COPY ./src /dst 12 | 13 | DEPLOY 14 | COPY ./src /dst 15 | `) 16 | if err != nil { 17 | t.Errorf("Parse() got error: err=%s", err.Error()) 18 | } 19 | 20 | if n0dep.Bootstrap[0].String() != "RUN git clone ... && apt update && apt install -y ..." { 21 | t.Errorf("Parse() return wrong bootstrap[0]:\n have=%s\n want=%s", n0dep.Bootstrap[0].String(), "RUN git clone ... && apt update && apt install -y ...") 22 | } 23 | if n0dep.Bootstrap[1].String() != "COPY ./src /dst" { 24 | t.Errorf("Parse() return wrong bootstrap[1]:\n have=%s\n want=%s", n0dep.Bootstrap[1].String(), "COPY ./src /dst") 25 | } 26 | if n0dep.Deploy[0].String() != "COPY ./src /dst" { 27 | t.Errorf("Parse() return wrong deploy[0]:\n have=%s\n want=%s", n0dep.Bootstrap[1].String(), "COPY ./src /dst") 28 | } 29 | } 30 | 31 | func TestValidateCopy(t *testing.T) { 32 | _, _, err := ValidateCopy("COPY ./hoge/../../bar /tmp") 33 | if err == nil { 34 | t.Errorf("ValidateCopy(\"COPY ./hoge/../../bar /tmp\") do not return error") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /n0core/pkg/driver/n0deploy/ssh.go: -------------------------------------------------------------------------------- 1 | package n0deploy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | "github.com/pkg/sftp" 12 | "golang.org/x/crypto/ssh" 13 | ) 14 | 15 | func NewSshParser(s *ssh.Client) *Parser { 16 | return &Parser{ 17 | NewCopyInstruction: NewSshCopyInstruction(s), 18 | NewRunInstruction: NewSshRunInstruction(s), 19 | } 20 | } 21 | 22 | type SshRunInstruction struct { 23 | command string 24 | sshClinet *ssh.Client 25 | } 26 | 27 | func NewSshRunInstruction(s *ssh.Client) func(string) (Instruction, error) { 28 | return func(line string) (Instruction, error) { 29 | return &SshRunInstruction{ 30 | command: strings.TrimPrefix(line, "RUN "), 31 | sshClinet: s, 32 | }, nil 33 | } 34 | } 35 | 36 | func (i SshRunInstruction) Do(ctx context.Context, out io.Writer) error { 37 | sess, err := i.sshClinet.NewSession() 38 | if err != nil { 39 | return errors.Wrap(err, "Failed to create new session") 40 | } 41 | defer sess.Close() 42 | 43 | sess.Stdout = out 44 | sess.Stderr = out 45 | 46 | if err := sess.Run(i.command); err != nil { // sh -c が必要かどうかわからない 47 | if ee, ok := err.(*ssh.ExitError); ok { 48 | return fmt.Errorf("'%s' exit status is not 0: code=%d", i.command, ee.ExitStatus()) 49 | } 50 | 51 | return errors.Wrapf(err, "Failed to command '%s'", i.command) 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (i SshRunInstruction) String() string { 58 | return fmt.Sprintf("RUN %s", i.command) 59 | } 60 | 61 | type SshCopyInstruction struct { 62 | src string 63 | dst string 64 | sshClinet *ssh.Client 65 | } 66 | 67 | func NewSshCopyInstruction(s *ssh.Client) func(string, string) (Instruction, error) { 68 | return func(src, dst string) (Instruction, error) { 69 | return &SshCopyInstruction{ 70 | src: src, 71 | dst: dst, 72 | sshClinet: s, 73 | }, nil 74 | } 75 | } 76 | 77 | func (i SshCopyInstruction) Do(ctx context.Context, out io.Writer) error { 78 | client, err := sftp.NewClient(i.sshClinet) 79 | if err != nil { 80 | return errors.Wrap(err, "Failed to create new sftp client") 81 | } 82 | defer client.Close() 83 | 84 | srcFile, err := os.Open(i.src) 85 | if err != nil { 86 | return errors.Wrap(err, "Failed to open localfile") 87 | } 88 | defer srcFile.Close() 89 | 90 | // ディレクトリの処理がうまくできていない 91 | dstFile, err := client.Create(i.dst) 92 | if err != nil { 93 | return errors.Wrap(err, "Failed to create remote file") 94 | } 95 | defer dstFile.Close() 96 | 97 | _, err = io.Copy(dstFile, srcFile) 98 | if err != nil { 99 | return errors.Wrap(err, "Failed to copy file") 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func (i SshCopyInstruction) String() string { 106 | return fmt.Sprintf("COPY %s %s", i.src, i.dst) 107 | } 108 | -------------------------------------------------------------------------------- /n0core/pkg/driver/qemu/README.md: -------------------------------------------------------------------------------- 1 | # QEMU 2 | 3 | ## Features 4 | 5 | - CPU は `host` で渡している 6 | - Ballooning はしない 7 | - 基本的にはvirtioで接続 8 | - SCSIコントローラを作成している 9 | 10 | ## Dependency packages 11 | 12 | - qemu-kvm 13 | 14 | 15 | ```sh 16 | apt install -y \ 17 | qemu-kvm 18 | ``` 19 | 20 | ## Test parameters 21 | 22 | ### DISABLE_KVM 23 | 24 | When setting environment variable, enable KVM. 25 | 26 | ```sh 27 | DISABLE_KVM=1 make test-small 28 | ``` 29 | -------------------------------------------------------------------------------- /n0core/pkg/driver/qemu/misc_test.go: -------------------------------------------------------------------------------- 1 | package qemu 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | func TestGetParsedOptionValueById(t *testing.T) { 10 | Cases := []struct { 11 | name string 12 | key string 13 | id string 14 | req []string 15 | args []string 16 | kwds map[string]string 17 | ok bool 18 | }{ 19 | { 20 | "with arg", 21 | "-chardev", 22 | "charmonitor", 23 | []string{"-chardev", "socket,id=charmonitor,path=monitor.sock,server,nowait"}, 24 | []string{"socket", "server", "nowait"}, 25 | map[string]string{ 26 | "id": "charmonitor", 27 | "path": "monitor.sock", 28 | }, 29 | true, 30 | }, 31 | { 32 | "without arg", 33 | "-mon", 34 | "monitor", 35 | []string{"-mon", "chardev=charmonitor,id=monitor,mode=control"}, 36 | []string{}, 37 | map[string]string{ 38 | "id": "monitor", 39 | "chardev": "charmonitor", 40 | "mode": "control", 41 | }, 42 | true, 43 | }, 44 | { 45 | "no match", 46 | "-foo", 47 | "aa", 48 | []string{"-mon", "chardev=charmonitor,id=monitor,mode=control"}, 49 | nil, 50 | nil, 51 | false, 52 | }, 53 | } 54 | 55 | for _, c := range Cases { 56 | a := ParseQemuArgs(c.req) 57 | 58 | args, kwds, ok := a.GetParsedOptionValueById(c.key, c.id) 59 | 60 | if c.ok != ok { 61 | t.Errorf("[%s] ok is wrong", c.name) 62 | } 63 | if diff := cmp.Diff(c.args, args); diff != "" { 64 | t.Errorf("[%s] args is wrong: diff=(-want +got)\n%s", c.name, diff) 65 | } 66 | if diff := cmp.Diff(c.kwds, kwds); diff != "" { 67 | t.Errorf("[%s] kwds is wrong: diff=(-want +got)\n%s", c.name, diff) 68 | } 69 | } 70 | } 71 | 72 | func TestGetTopParsedOptionValue(t *testing.T) { 73 | Cases := []struct { 74 | name string 75 | key string 76 | req []string 77 | args []string 78 | kwds map[string]string 79 | ok bool 80 | }{ 81 | { 82 | "with arg", 83 | "-chardev", 84 | []string{"-chardev", "socket,id=charmonitor,path=monitor.sock,server,nowait"}, 85 | []string{"socket", "server", "nowait"}, 86 | map[string]string{ 87 | "id": "charmonitor", 88 | "path": "monitor.sock", 89 | }, 90 | true, 91 | }, 92 | { 93 | "without arg", 94 | "-mon", 95 | []string{"-mon", "chardev=charmonitor,id=monitor,mode=control"}, 96 | []string{}, 97 | map[string]string{ 98 | "id": "monitor", 99 | "chardev": "charmonitor", 100 | "mode": "control", 101 | }, 102 | true, 103 | }, 104 | { 105 | "no match", 106 | "-foo", 107 | []string{"-mon", "chardev=charmonitor,id=monitor,mode=control"}, 108 | nil, 109 | nil, 110 | false, 111 | }, 112 | } 113 | 114 | for _, c := range Cases { 115 | a := ParseQemuArgs(c.req) 116 | 117 | args, kwds, ok := a.GetTopParsedOptionValue(c.key) 118 | 119 | if c.ok != ok { 120 | t.Errorf("[%s] ok is wrong", c.name) 121 | } 122 | if diff := cmp.Diff(c.args, args); diff != "" { 123 | t.Errorf("[%s] args is wrong: diff=(-want +got)\n%s", c.name, diff) 124 | } 125 | if diff := cmp.Diff(c.kwds, kwds); diff != "" { 126 | t.Errorf("[%s] kwds is wrong: diff=(-want +got)\n%s", c.name, diff) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /n0core/pkg/driver/qemu/network.go: -------------------------------------------------------------------------------- 1 | package qemu 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | ) 8 | 9 | // (QEMU) netdev_add id=tap0 type=tap vhost=true ifname=tap0 script=no downscript=no 10 | // (QEMU) device_add driver=virtio-net-pci netdev=tap0 id=test0 mac=52:54:00:df:89:29 bus=pci.0 11 | // まだべき等ではない 12 | // TODO: 13 | // - すでにアタッチされていた場合、エラー処理を文字列で判定する必要がある 14 | // - MACアドレスを変更する 15 | func (q Qemu) AttachTap(label, tap string, mac net.HardwareAddr) error { 16 | netdevID := fmt.Sprintf("netdev-%s", label) 17 | devID := fmt.Sprintf("virtio-net-%s", label) 18 | 19 | // check to create netdev 20 | 21 | if err := q.tapNetdevAdd(netdevID, tap); err != nil { 22 | return fmt.Errorf("Failed to run netdev_add: err='%s'", err.Error()) 23 | } 24 | 25 | if err := q.virtioNetPCIAdd(devID, netdevID, mac); err != nil { 26 | return fmt.Errorf("Failed to create virtio network device: err='%s'", err.Error()) 27 | } 28 | 29 | return nil 30 | } 31 | 32 | func (q *Qemu) tapNetdevAdd(id, ifname string) error { 33 | cmd := struct { 34 | ID string `json:"id"` 35 | Type string `json:"type"` 36 | Ifname string `json:"ifname"` 37 | Vhost bool `json:"vhost"` 38 | Script string `json:"script"` 39 | Downscript string `json:"downscript"` 40 | }{ 41 | id, 42 | "tap", 43 | ifname, 44 | true, 45 | "no", 46 | "no", 47 | } 48 | 49 | bs, err := json.Marshal(map[string]interface{}{ 50 | "execute": "netdev_add", 51 | "arguments": cmd, 52 | }) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | _, err = q.qmp.Run(bs) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | return err 63 | } 64 | 65 | func (q *Qemu) virtioNetPCIAdd(devID, netdevID string, mac net.HardwareAddr) error { 66 | cmd := struct { 67 | Driver string `json:"driver"` 68 | ID string `json:"id"` 69 | Netdev string `json:"netdev"` 70 | Bus string `json:"bus"` 71 | Mac string `json:"mac"` 72 | }{ 73 | "virtio-net-pci", 74 | devID, 75 | netdevID, 76 | "pci.0", 77 | mac.String(), 78 | } 79 | 80 | bs, err := json.Marshal(map[string]interface{}{ 81 | "execute": "device_add", 82 | "arguments": cmd, 83 | }) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | _, err = q.qmp.Run(bs) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /n0core/pkg/driver/qemu/network_test.go: -------------------------------------------------------------------------------- 1 | // +build medium 2 | // +build !without_root 3 | 4 | package qemu 5 | 6 | import ( 7 | "net" 8 | "os" 9 | "testing" 10 | 11 | "code.cloudfoundry.org/bytefmt" 12 | "github.com/n0stack/n0stack/n0core/pkg/driver/iproute2" 13 | uuid "github.com/satori/go.uuid" 14 | ) 15 | 16 | func TestAttachTap(t *testing.T) { 17 | b, err := iproute2.NewBridge("br-test") 18 | if err != nil { 19 | t.Fatalf("Failed to create bridge: err='%s'", err.Error()) 20 | } 21 | defer b.Delete() 22 | 23 | i, err := iproute2.NewTap("tap-test") 24 | if err != nil { 25 | t.Fatalf("Failed to create tap: err='%s'", err.Error()) 26 | } 27 | defer i.Delete() 28 | 29 | i.SetMaster(b) 30 | 31 | id, _ := uuid.FromString("5fd6c569-172f-4b25-84cd-b76cc336cfdd") 32 | q, err := OpenQemu("test") 33 | if err != nil { 34 | t.Fatalf("Failed to open qemu: err='%s'", err.Error()) 35 | } 36 | defer q.Delete() 37 | 38 | if _, ok := os.LookupEnv("DISABLE_KVM"); ok { 39 | q.isKVM = false 40 | } 41 | 42 | m, _ := bytefmt.ToBytes("512M") 43 | if err := q.Start(id, "monitor.sock", 10000, 1, m); err != nil { 44 | t.Fatalf("Failed to start process: err='%s'", err.Error()) 45 | } 46 | 47 | hw, _ := net.ParseMAC("52:54:01:23:45:67") 48 | if err := q.AttachTap("test", i.Name(), hw); err != nil { 49 | t.Errorf("Failed to attach tap: err='%s'", err.Error()) 50 | } 51 | 52 | if err := q.Boot(); err != nil { 53 | t.Errorf("Failed to boot: err='%s'", err.Error()) 54 | } 55 | 56 | s, err := q.Status() 57 | if err != nil { 58 | t.Errorf("Failed to get status: err='%s'", err.Error()) 59 | } 60 | if s != StatusRunning { 61 | t.Errorf("Status is mismatch: want='%v', have='%v'", StatusRunning, s) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /n0core/pkg/driver/qemu_img/README.md: -------------------------------------------------------------------------------- 1 | # QEMU 2 | 3 | ## Dependency packages 4 | 5 | - qemu-utils 6 | 7 | ```sh 8 | apt install -y \ 9 | qemu-utils 10 | ``` 11 | -------------------------------------------------------------------------------- /n0core/pkg/driver/qemu_img/download.go: -------------------------------------------------------------------------------- 1 | package img 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | ) 10 | 11 | func (q *QemuImg) Download(src *url.URL) error { 12 | if q.IsExists() { 13 | return fmt.Errorf("Already exists") // TODO 14 | } 15 | 16 | res, err := http.Get(src.String()) 17 | if err != nil { 18 | return fmt.Errorf("Failed to get file from http: url='%s', err='%s'", src.String(), err.Error()) 19 | } 20 | defer res.Body.Close() 21 | 22 | if res.StatusCode != http.StatusOK { 23 | return fmt.Errorf("Failed to get file from http: url='%s', status='%s'", src.String(), res.Status) 24 | } 25 | 26 | f, err := os.Create(q.path) 27 | if err != nil { 28 | return fmt.Errorf("Failed to open file: path='%s', err='%s'", q.path, err.Error()) 29 | } 30 | defer f.Close() 31 | 32 | if _, err := io.Copy(f, res.Body); err != nil { 33 | return fmt.Errorf("Failed to copy file: path='%s', err='%s'", q.path, err.Error()) 34 | } 35 | 36 | if err := q.updateInfo(); err != nil { 37 | return fmt.Errorf("Failed to update info: err='%s'", err.Error()) 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /n0core/pkg/driver/qemu_img/download_test.go: -------------------------------------------------------------------------------- 1 | // +build medium 2 | 3 | package img 4 | 5 | import ( 6 | "net/url" 7 | "testing" 8 | ) 9 | 10 | func TestDownloadQcow2(t *testing.T) { 11 | p := "test.qcow2" 12 | 13 | i, err := OpenQemuImg(p) 14 | if err != nil { 15 | t.Fatalf("Cannot open '%s': err='%s'", p, err.Error()) 16 | } 17 | if i.IsExists() { 18 | t.Errorf("Test environment is invalid, image is already existing: path='%s'", p) 19 | } 20 | 21 | u, err := url.Parse("http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img") 22 | if err := i.Download(u); err != nil { 23 | t.Errorf("Failed to download image: err='%s'", err.Error()) 24 | } 25 | if !i.IsExists() { 26 | t.Errorf("Failed to download image: image is not existing yet") 27 | } 28 | 29 | if err := i.Delete(); err != nil { 30 | t.Errorf("Failed to delete image: err='%s'", err.Error()) 31 | } 32 | if i.IsExists() { 33 | t.Errorf("Failed to delete image: image is existing yet") 34 | } 35 | } 36 | 37 | func TestDownloadISO(t *testing.T) { 38 | p := "mini.iso" 39 | 40 | i, err := OpenQemuImg(p) 41 | if err != nil { 42 | t.Fatalf("Cannot open '%s': err='%s'", p, err.Error()) 43 | } 44 | if i.IsExists() { 45 | t.Errorf("Test environment is invalid, image is already existing: path='%s'", p) 46 | } 47 | 48 | u, err := url.Parse("http://archive.ubuntu.com/ubuntu/dists/bionic-updates/main/installer-amd64/current/images/netboot/mini.iso") 49 | if err := i.Download(u); err != nil { 50 | t.Errorf("Failed to download image: err='%s'", err.Error()) 51 | } 52 | if !i.IsExists() { 53 | t.Errorf("Failed to download image: image is not existing yet") 54 | } 55 | 56 | if err := i.Delete(); err != nil { 57 | t.Errorf("Failed to delete image: err='%s'", err.Error()) 58 | } 59 | if i.IsExists() { 60 | t.Errorf("Failed to delete image: image is existing yet") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /n0core/pkg/util/generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/format" 7 | "io/ioutil" 8 | ) 9 | 10 | // referenced github.com/golang/tools/blob/master/cmd/stringer 11 | type GoCodeGenerator struct { 12 | code bytes.Buffer 13 | 14 | generator string 15 | pkg string 16 | } 17 | 18 | func NewGoCodeGenerator(generator string, pkg string) *GoCodeGenerator { 19 | g := &GoCodeGenerator{ 20 | generator: generator, 21 | pkg: pkg, 22 | } 23 | 24 | g.Printf("// Code generated by \"%s\"; DO NOT EDIT.\n", g.generator) 25 | g.Printf("\n") 26 | g.Printf("package %s\n", g.pkg) 27 | g.Printf("\n") 28 | 29 | return g 30 | } 31 | 32 | func (g *GoCodeGenerator) Printf(format string, a ...interface{}) (int, error) { 33 | return g.code.WriteString(fmt.Sprintf(format, a...)) 34 | } 35 | 36 | func (g GoCodeGenerator) FormattedCode() ([]byte, error) { 37 | return format.Source(g.code.Bytes()) 38 | } 39 | 40 | func (g GoCodeGenerator) WriteFile(filename string) error { 41 | src, err := g.FormattedCode() 42 | if err != nil { 43 | return err 44 | } 45 | 46 | err = ioutil.WriteFile(filename, src, 0644) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (g GoCodeGenerator) WriteAsTemplateFileName() error { 55 | return g.WriteFile(fmt.Sprintf("%s.generated.go", g.generator)) 56 | } 57 | -------------------------------------------------------------------------------- /n0core/pkg/util/generator/generator_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestGoCodeGenerator(t *testing.T) { 9 | g := NewGoCodeGenerator("test_generator", "generator") 10 | if err := g.WriteAsTemplateFileName(); err != nil { 11 | t.Errorf("err=%s, src=\n%s", err.Error(), g.code.String()) 12 | } 13 | defer os.Remove("test_generator.generated.go") 14 | } 15 | -------------------------------------------------------------------------------- /n0core/pkg/util/grpc/error.go: -------------------------------------------------------------------------------- 1 | package grpcutil 2 | 3 | import ( 4 | "log" 5 | 6 | "google.golang.org/grpc" 7 | "google.golang.org/grpc/codes" 8 | ) 9 | 10 | // WrapGrpcErrorf returns grpc.Errorf 11 | // in the case of 'Internal', logging message because the server has failed 12 | func WrapGrpcErrorf(c codes.Code, format string, a ...interface{}) error { 13 | err := grpc.Errorf(c, format, a...) 14 | 15 | if c == codes.Internal { 16 | log.Printf("[WARNING] "+format, a...) 17 | } 18 | 19 | return err 20 | } 21 | -------------------------------------------------------------------------------- /n0core/pkg/util/net/ip.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | func NextIP(ip net.IP) net.IP { 9 | res := net.ParseIP(ip.String()) 10 | for i := len(res) - 1; i >= 0; i-- { 11 | res[i]++ 12 | 13 | if res[i] > 0 { 14 | break 15 | } 16 | } 17 | 18 | return res 19 | } 20 | 21 | func GetEndIP(ipn *net.IPNet) net.IP { 22 | // next network address 23 | ip := NextIP(ipn.IP) 24 | 25 | for { 26 | // before broadcast address 27 | if !ipn.Contains(NextIP(NextIP(ip))) { 28 | return ip 29 | } 30 | 31 | ip = NextIP(ip) 32 | } 33 | } 34 | 35 | type IPv4Cidr struct { 36 | ip net.IP 37 | network *net.IPNet 38 | } 39 | 40 | func ParseCIDR(cidr string) *IPv4Cidr { 41 | ip, ipn, err := net.ParseCIDR(cidr) 42 | if err != nil { 43 | return nil 44 | } 45 | 46 | return &IPv4Cidr{ 47 | ip: ip, 48 | network: ipn, 49 | } 50 | } 51 | 52 | func (c *IPv4Cidr) String() string { 53 | if c == nil { 54 | return "" 55 | } 56 | 57 | return fmt.Sprintf("%s/%d", c.ip.String(), c.SubnetMaskBits()) 58 | } 59 | 60 | func (c IPv4Cidr) IP() net.IP { 61 | return c.ip 62 | } 63 | 64 | func (c IPv4Cidr) Next() *IPv4Cidr { 65 | next := NextIP(c.ip) 66 | if !c.network.Contains(next) { 67 | return nil 68 | } 69 | 70 | return &IPv4Cidr{ 71 | ip: next, 72 | network: c.network, 73 | } 74 | } 75 | 76 | func (c IPv4Cidr) Network() *net.IPNet { 77 | net := *c.network 78 | 79 | return &net 80 | } 81 | 82 | func (c IPv4Cidr) SubnetMaskBits() int { 83 | m, _ := c.network.Mask.Size() 84 | 85 | return m 86 | } 87 | 88 | func (c IPv4Cidr) SubnetMaskIP() net.IP { 89 | return net.IPv4(c.network.Mask[0], c.network.Mask[1], c.network.Mask[2], c.network.Mask[3]) 90 | } 91 | 92 | func IsConflicting(a, b *IPv4Cidr) bool { 93 | if a.Network().Contains(b.IP()) || b.Network().Contains(a.IP()) { 94 | return true 95 | } 96 | 97 | return false 98 | } 99 | -------------------------------------------------------------------------------- /n0core/pkg/util/net/ip_test.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func TestGetEndIP(t *testing.T) { 9 | _, ipn, _ := net.ParseCIDR("192.168.0.0/24") 10 | if ip := GetEndIP(ipn); ip.String() != "192.168.0.254" { 11 | t.Errorf("Failed to get end of ip: got='%s', want='%s'", ip.String(), "192.168.0.254") 12 | } 13 | } 14 | 15 | func TestIPv4Cidr(t *testing.T) { 16 | cidr := ParseCIDR("aa") 17 | if cidr != nil { 18 | t.Errorf("ParseCIDR() do not return nil when over: have=%v", cidr) 19 | } 20 | 21 | cidr = ParseCIDR("192.168.0.2/30") 22 | if cidr.String() != "192.168.0.2/30" { 23 | t.Errorf("String() is wrong: have='%s', want='%s'", cidr.String(), "192.168.0.2/30") 24 | } 25 | if cidr.Next().String() != "192.168.0.3/30" { 26 | t.Errorf("Next().String() is wrong: have='%s', want='%s'", cidr.Next().String(), "192.168.0.3/30") 27 | } 28 | if n := cidr.Next().Next(); n != nil { 29 | t.Errorf("Next() do not return nil when over: have=%v", n) 30 | } 31 | 32 | if cidr.IP().String() != "192.168.0.2" { 33 | t.Errorf("IP() is wrong: have='%s', want='%s'", cidr.IP().String(), "192.168.0.2/30") 34 | } 35 | if cidr.Network().String() != "192.168.0.0/30" { 36 | t.Errorf("Network() is wrong: have='%s', want='%s'", cidr.Network().String(), "192.168.0.0/30") 37 | } 38 | if cidr.SubnetMaskBits() != 30 { 39 | t.Errorf("SubnetMaskBits() is wrong: have='%d', want='%d'", cidr.SubnetMaskBits(), 30) 40 | } 41 | if cidr.SubnetMaskIP().String() != "255.255.255.252" { 42 | t.Errorf("SubnetMaskIP() is wrong: have='%s', want='%s'", cidr.SubnetMaskIP().String(), "192.168.0.2") 43 | } 44 | } 45 | 46 | func TestIsConflicting(t *testing.T) { 47 | cases := []struct { 48 | name string 49 | inputA *IPv4Cidr 50 | inputB *IPv4Cidr 51 | result bool 52 | }{ 53 | { 54 | "not conflicting", 55 | ParseCIDR("192.168.0.0/24"), 56 | ParseCIDR("192.168.1.0/24"), 57 | false, 58 | }, 59 | { 60 | "contain", 61 | ParseCIDR("192.168.0.0/23"), 62 | ParseCIDR("192.168.1.0/24"), 63 | true, 64 | }, 65 | { 66 | "contain", 67 | ParseCIDR("192.168.0.0/20"), 68 | ParseCIDR("192.168.1.0/24"), 69 | true, 70 | }, 71 | } 72 | 73 | for _, c := range cases { 74 | have := IsConflicting(c.inputA, c.inputB) 75 | 76 | if have != c.result { 77 | t.Errorf("[%s] Result has mismatch: want=%v, have=%v", c.name, c.result, have) 78 | } 79 | } 80 | } 81 | 82 | func TestNilWithString(t *testing.T) { 83 | var ip *IPv4Cidr = nil 84 | 85 | // when ip is nil, return "" 86 | if ip.String() != "" { 87 | t.Errorf("return value of ip.String() is wrong, require ''") 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /n0core/pkg/util/net/linux.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "fmt" 5 | "hash/crc32" 6 | "math" 7 | ) 8 | 9 | const MaxLengthLinuxNetworkDeviceName = 15 10 | const ChecksumLength = 5 11 | 12 | // TrimNetdevName trim network device name because Linux network device can use 15 characters. 13 | // コンフリクトを抑制するために末尾 4 bytes を乱数にする 14 | func StructLinuxNetdevName(name string) string { 15 | cs := crc32.Checksum([]byte(name), crc32.IEEETable) % uint32(math.Pow(0x10, 4)) 16 | 17 | if len(name)+ChecksumLength > MaxLengthLinuxNetworkDeviceName { 18 | return fmt.Sprintf("%s-%02x", name[:10], cs) 19 | } 20 | 21 | return fmt.Sprintf("%s-%02x", name, cs) 22 | } 23 | -------------------------------------------------------------------------------- /n0core/pkg/util/net/linux_test.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import "testing" 4 | 5 | func TestStructLinuxNetdevName(t *testing.T) { 6 | cases := []struct { 7 | name string 8 | arg string 9 | ret string 10 | }{ 11 | { 12 | "simple", 13 | "aa", 14 | "aa-19d7", 15 | }, 16 | { 17 | "trim", 18 | "aaaaaaaaaaaaaaaa", 19 | "aaaaaaaaaa-68d5", 20 | }, 21 | { 22 | "just", 23 | "aaaaaaaaaa", 24 | "aaaaaaaaaa-cdf0", 25 | }, 26 | { 27 | "just+1", 28 | "aaaaaaaaaaa", 29 | "aaaaaaaaaa-5d92", 30 | }, 31 | } 32 | 33 | for _, c := range cases { 34 | ret := StructLinuxNetdevName(c.arg) 35 | if ret != c.ret { 36 | t.Errorf("Get wrong return, have='%s', want='%s'", ret, c.ret) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /n0core/pkg/util/net/mac.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "hash/crc32" 7 | "net" 8 | ) 9 | 10 | func GenerateHardwareAddress(id string) net.HardwareAddr { 11 | cs := crc32.Checksum([]byte(id), crc32.IEEETable) 12 | b, _ := hex.DecodeString(fmt.Sprintf("5254%08x", cs)) 13 | return net.HardwareAddr(b) 14 | } 15 | -------------------------------------------------------------------------------- /n0core/pkg/util/net/mac_test.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import "testing" 4 | 5 | func TestGenerateHardwareAddress(t *testing.T) { 6 | result := "52:54:f5:8c:a4:f2" 7 | hw := GenerateHardwareAddress("hogehoge") 8 | if hw.String() != result { 9 | t.Errorf("Wrong hardware address\n\thave:%s\n\twant:%s", hw, result) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /n0core/pkg/util/race/io.go: -------------------------------------------------------------------------------- 1 | package raceutil 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | ) 7 | 8 | // https://www.reddit.com/r/golang/comments/6bpmtj/writing_to_a_file_on_multiple_threads/ 9 | type LockedWriter struct { 10 | m *sync.Mutex 11 | w io.Writer 12 | } 13 | 14 | func NewLockedWriter(w io.Writer) *LockedWriter { 15 | return &LockedWriter{ 16 | m: &sync.Mutex{}, 17 | w: w, 18 | } 19 | } 20 | 21 | func (lw *LockedWriter) Write(b []byte) (n int, err error) { 22 | lw.m.Lock() 23 | defer lw.m.Unlock() 24 | return lw.w.Write(b) 25 | } 26 | -------------------------------------------------------------------------------- /n0core/pkg/util/string/name.go: -------------------------------------------------------------------------------- 1 | package stringutil 2 | 3 | import ( 4 | "fmt" 5 | "hash/crc32" 6 | "math" 7 | ) 8 | 9 | const SuffixLength = 5 10 | 11 | func StringWithChecksumSuffix(name string, size int) string { 12 | cs := crc32.Checksum([]byte(name), crc32.IEEETable) % uint32(math.Pow(0x10, 4)) 13 | 14 | if len(name)+SuffixLength > size { 15 | return fmt.Sprintf("%s-%02x", name[:size-SuffixLength], cs) 16 | } 17 | 18 | return fmt.Sprintf("%s-%02x", name, cs) 19 | } 20 | -------------------------------------------------------------------------------- /n0core/pkg/util/string/name_test.go: -------------------------------------------------------------------------------- 1 | package stringutil 2 | 3 | import "testing" 4 | 5 | func TestStringWithChecksumSuffix(t *testing.T) { 6 | cases := []struct { 7 | name string 8 | arg string 9 | size int 10 | ret string 11 | }{ 12 | { 13 | "simple", 14 | "aa", 15 | 15, 16 | "aa-19d7", 17 | }, 18 | { 19 | "trim", 20 | "aaaaaaaaaaaaaaaa", 21 | 15, 22 | "aaaaaaaaaa-68d5", 23 | }, 24 | { 25 | "just", 26 | "aaaaaaaaaa", 27 | 15, 28 | "aaaaaaaaaa-cdf0", 29 | }, 30 | { 31 | "just+1", 32 | "aaaaaaaaaaa", 33 | 15, 34 | "aaaaaaaaaa-5d92", 35 | }, 36 | { 37 | "just+1", 38 | "aaaaaaaaaaa", 39 | 10, 40 | "aaaaa-5d92", 41 | }, 42 | } 43 | 44 | for _, c := range cases { 45 | ret := StringWithChecksumSuffix(c.arg, c.size) 46 | if ret != c.ret { 47 | t.Errorf("Get wrong return, have='%s', want='%s'", ret, c.ret) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /n0proto.go/pkg/transaction/docs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Example 3 | 4 | func SomeEndpoint(ctx context.COntext, req SomeRequest) error { 5 | // validation 6 | 7 | tx := transaction.Begin() 8 | defer tx.Rollback() 9 | 10 | // API Process 11 | if err := Process(); err != nil { 12 | return err 13 | } 14 | tx.PushRollback("Process", func() error { 15 | return InverseProcess() 16 | }) 17 | 18 | tx.Done() 19 | 20 | return nil 21 | } 22 | */ 23 | package transaction 24 | -------------------------------------------------------------------------------- /n0proto.go/pkg/transaction/transaction.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/cenkalti/backoff" 8 | ) 9 | 10 | type RollbackTask struct { 11 | Name string 12 | Func func() error 13 | } 14 | 15 | type Transaction struct { 16 | stack []*RollbackTask 17 | } 18 | 19 | func Begin() *Transaction { 20 | t := &Transaction{} 21 | t.stack = make([]*RollbackTask, 0) 22 | 23 | return &Transaction{} 24 | } 25 | 26 | func (tx *Transaction) PushRollback(name string, f func() error) { 27 | tx.stack = append(tx.stack, &RollbackTask{ 28 | Name: name, 29 | Func: f, 30 | }) 31 | } 32 | 33 | func (tx *Transaction) PopRollback() *RollbackTask { 34 | l := len(tx.stack) 35 | if l == 0 { 36 | return nil 37 | } 38 | 39 | ret := tx.stack[l-1] 40 | tx.stack = tx.stack[:l-1] 41 | 42 | return ret 43 | } 44 | 45 | func (tx *Transaction) Rollback() error { 46 | errMes := "" 47 | 48 | for r := tx.PopRollback(); r != nil; r = tx.PopRollback() { 49 | b := backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 5) 50 | err := backoff.Retry(r.Func, b) 51 | if err != nil { 52 | errMes = fmt.Sprintf(" [%s] %s\n%s", r.Name, err.Error(), errMes) 53 | } 54 | } 55 | 56 | if errMes != "" { 57 | return fmt.Errorf(errMes) 58 | } 59 | return nil 60 | } 61 | 62 | func (tx *Transaction) RollbackWithLog() { 63 | if err := tx.Rollback(); err != nil { 64 | log.Printf("[CRITICAL] Failed to rollback: err=\n%s", err.Error()) 65 | } 66 | } 67 | 68 | func (tx *Transaction) Commit() { 69 | tx.stack = make([]*RollbackTask, 0) 70 | } 71 | -------------------------------------------------------------------------------- /n0proto.go/pkg/transaction/transaction_test.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | import "testing" 4 | 5 | func TestTransaction(t *testing.T) { 6 | tx := Begin() 7 | 8 | called := false 9 | tx.PushRollback("test", func() error { 10 | called = true 11 | return nil 12 | }) 13 | 14 | if err := tx.Rollback(); err != nil { 15 | t.Errorf("Make err nil: err=%s", err.Error()) 16 | } 17 | if !called { 18 | t.Errorf("Rollback is not called") 19 | } 20 | } 21 | 22 | func TestTransactionManyTimes(t *testing.T) { 23 | tx := Begin() 24 | 25 | called := 0 26 | for i := 0; i < 10; i++ { 27 | tx.PushRollback("test", func() error { 28 | called++ 29 | return nil 30 | }) 31 | } 32 | 33 | if err := tx.Rollback(); err != nil { 34 | t.Errorf("Make err nil: err=%s", err.Error()) 35 | } 36 | if called != 10 { 37 | t.Errorf("Rollback was not called 10 times") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /n0proto/README.md: -------------------------------------------------------------------------------- 1 | # n0proto 2 | 3 | Protobuf definitions for all of n0stack services. 4 | 5 | ## Overview 6 | 7 | see also [docs](https://docs.n0st.ac/en/latest/user/overview_n0proto.html). 8 | 9 | ## How to build 10 | 11 | - Required Docker 12 | - Generating Golang and Python files on n0proto.go and n0proto.py 13 | 14 | ``` 15 | cd .. 16 | make build-n0proto-on-docker 17 | ``` 18 | 19 | ## Principles 20 | 21 | - Do not define variables that change with implementation, such values ​​should be placed in "annotations". 22 | - e.g. VLAN ID and VXLAN ID 23 | 24 | ### Standard fields 25 | 26 | - Metadata (1 ~ 9) 27 | - Spec (10 ~ 49) 28 | - Status (50 ~) 29 | 30 | ```pb 31 | // Name is a unique field. 32 | string name = 1; 33 | // string namespace = 2; 34 | 35 | // Annotations can store metadata used by the system for control. 36 | // In particular, implementation-dependent fields that can not be set as protobuf fields are targeted. 37 | // The control specified by n0stack may delete metadata specified by the user. 38 | map annotations = 3; 39 | 40 | // Labels stores user-defined metadata. 41 | // The n0stack system must not rewrite this value. 42 | map labels = 4; 43 | ``` 44 | -------------------------------------------------------------------------------- /n0proto/n0stack/budget/README.md: -------------------------------------------------------------------------------- 1 | # Resource 2 | 3 | - クラスタ上のリソースの管理の一元化 4 | - 実態の操作は行わない 5 | - pool にAPIの実装を行う 6 | -------------------------------------------------------------------------------- /n0proto/n0stack/budget/v0/compute.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/n0stack/n0stack/n0proto.go/budget/v0;pbudget"; 4 | 5 | package n0stack.budget.v0; 6 | 7 | import "protoc-gen-swagger/options/annotations.proto"; 8 | 9 | option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { 10 | info: { 11 | title: "n0stack"; 12 | contact: { 13 | name: "n0stack"; 14 | url: "https://github.com/n0stack/n0stack"; 15 | }; 16 | }; 17 | 18 | schemes: HTTP; 19 | schemes: HTTPS; 20 | consumes: "application/json"; 21 | produces: "application/json"; 22 | }; 23 | 24 | message Compute { 25 | map annotations = 1; 26 | 27 | uint32 request_cpu_milli_core = 2; 28 | uint32 limit_cpu_milli_core = 3; 29 | 30 | uint64 request_memory_bytes = 4; 31 | uint64 limit_memory_bytes = 5; 32 | } 33 | -------------------------------------------------------------------------------- /n0proto/n0stack/budget/v0/network_interface.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/n0stack/n0stack/n0proto.go/budget/v0;pbudget"; 4 | 5 | package n0stack.budget.v0; 6 | 7 | import "protoc-gen-swagger/options/annotations.proto"; 8 | 9 | option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { 10 | info: { 11 | title: "n0stack"; 12 | contact: { 13 | name: "n0stack"; 14 | url: "https://github.com/n0stack/n0stack"; 15 | }; 16 | }; 17 | 18 | schemes: HTTP; 19 | schemes: HTTPS; 20 | consumes: "application/json"; 21 | produces: "application/json"; 22 | }; 23 | 24 | message NetworkInterface { 25 | map annotations = 1; 26 | 27 | string hardware_address = 2; 28 | 29 | string ipv4_address = 3; 30 | string ipv6_address = 4; 31 | } 32 | -------------------------------------------------------------------------------- /n0proto/n0stack/budget/v0/storage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/n0stack/n0stack/n0proto.go/budget/v0;pbudget"; 4 | 5 | package n0stack.budget.v0; 6 | 7 | import "protoc-gen-swagger/options/annotations.proto"; 8 | 9 | option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { 10 | info: { 11 | title: "n0stack"; 12 | contact: { 13 | name: "n0stack"; 14 | url: "https://github.com/n0stack/n0stack"; 15 | }; 16 | }; 17 | 18 | schemes: HTTP; 19 | schemes: HTTPS; 20 | consumes: "application/json"; 21 | produces: "application/json"; 22 | }; 23 | 24 | 25 | message Storage { 26 | map annotations = 1; 27 | 28 | uint64 request_bytes = 2; 29 | uint64 limit_bytes = 3; 30 | } 31 | -------------------------------------------------------------------------------- /n0proto/n0stack/deployment/README.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | - Provisionin を抽象化し、宣言的に定義できるようにする 4 | - よって非同期も可 5 | - この層は immutable なリソースを扱う 6 | 7 | ## immutable であるということは 8 | 9 | - 宣言的に定義する 10 | - よってインターフェイスは ARD 11 | - Apply, Read, Delete 12 | -------------------------------------------------------------------------------- /n0proto/n0stack/iam/v0/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/n0stack/n0stack/n0proto.go/iam/v0;piam"; 4 | 5 | package n0stack.iam.v0; 6 | 7 | import "google/api/annotations.proto"; 8 | import "google/protobuf/empty.proto"; 9 | // import "google/protobuf/timestamp.proto"; 10 | // import "n0stack/provisioning/v0/block_storage.proto"; 11 | import "protoc-gen-swagger/options/annotations.proto"; 12 | 13 | option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { 14 | info: { 15 | title: "n0stack"; 16 | contact: { 17 | name: "n0stack"; 18 | url: "https://github.com/n0stack/n0stack"; 19 | } 20 | } 21 | 22 | schemes: HTTP; 23 | schemes: HTTPS; 24 | consumes: "application/json"; 25 | produces: "application/json"; 26 | }; 27 | 28 | 29 | message User { 30 | // Name is a unique field. 31 | string name = 1; 32 | // string namespace = 2; 33 | 34 | // Annotations can store metadata used by the system for control. 35 | // In particular, implementation-dependent fields that can not be set as protobuf fields are targeted. 36 | // The control specified by n0stack may delete metadata specified by the user. 37 | map annotations = 3; 38 | 39 | // Labels stores user-defined metadata. 40 | // The n0stack system must not rewrite this value. 41 | map labels = 4; 42 | 43 | map ssh_public_keys = 10; 44 | 45 | enum UserState { 46 | USER_UNSPECIFIED = 0; 47 | 48 | // working API 49 | PENDING = 1; 50 | 51 | AVAILABLE = 2; 52 | } 53 | UserState state = 50; 54 | } 55 | 56 | 57 | service UserService { 58 | rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) { 59 | option (google.api.http) = { 60 | get: "/api/v0/user" 61 | }; 62 | } 63 | rpc GetUser(GetUserRequest) returns (User) { 64 | option (google.api.http) = { 65 | get: "/api/v0/user/{name}" 66 | }; 67 | } 68 | rpc CreateUser(CreateUserRequest) returns (User) { 69 | option (google.api.http) = { 70 | post: "/api/v0/user/{name}" 71 | body: "*" 72 | }; 73 | } 74 | rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty) { 75 | option (google.api.http) = { 76 | delete: "/api/v0/user/{name}" 77 | }; 78 | } 79 | 80 | rpc AddSshPublicKey(AddSshPublicKeyRequest) returns (User) { 81 | option (google.api.http) = { 82 | post: "/api/v0/user/{user_name}/ssh_public_key/{ssh_public_key_name}" 83 | body: "*" 84 | }; 85 | } 86 | rpc DeleteSshPublicKey(DeleteSshPublicKeyRequest) returns (User) { 87 | option (google.api.http) = { 88 | delete: "/api/v0/user/{user_name}/ssh_public_key/{ssh_public_key_name}" 89 | }; 90 | } 91 | } 92 | 93 | 94 | message ListUsersRequest {} 95 | message ListUsersResponse { 96 | repeated User users = 1; 97 | } 98 | 99 | message GetUserRequest { 100 | string name = 1; 101 | } 102 | 103 | message CreateUserRequest { 104 | string name = 1; 105 | map annotations = 3; 106 | map labels = 4; 107 | } 108 | 109 | message DeleteUserRequest { 110 | string name = 1; 111 | } 112 | 113 | message AddSshPublicKeyRequest { 114 | string user_name = 1; 115 | string ssh_public_key_name = 2; 116 | string ssh_public_key = 3; 117 | } 118 | message DeleteSshPublicKeyRequest { 119 | string user_name = 1; 120 | string ssh_public_key_name = 2; 121 | } 122 | -------------------------------------------------------------------------------- /n0proto/n0stack/pool/README.md: -------------------------------------------------------------------------------- 1 | # Pool 2 | 3 | - リソースを宣言的におこなえるようにする 4 | - この層は immutable なリソースを扱う (?) 5 | 6 | ## immutable であるということは 7 | 8 | - 宣言的に定義する 9 | - よってインターフェイスは ARD 10 | - Apply, Read, Delete 11 | -------------------------------------------------------------------------------- /n0proto/n0stack/provisioning/README.md: -------------------------------------------------------------------------------- 1 | # Provisioning 2 | 3 | - リソースの仮想化を行う実体を管理する 4 | - この層は mutable なリソースを扱う 5 | 6 | ## mutable であるということは 7 | 8 | - 最初のリクエストが重要な意味を持つ、更新できるフィールドは非常に制限される 9 | - よってインターフェイスは CRUD 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | class Packages(): 5 | def __init__(self): 6 | self._package = [] 7 | self._package_dir = {} 8 | 9 | @property 10 | def package_dir(self): 11 | return self._package_dir 12 | 13 | @property 14 | def package(self): 15 | return self._package 16 | 17 | def add_package(self, package, directory): 18 | self._package_dir[package] = directory 19 | self._package.append(package) 20 | self._package.extend(self.__add_prefix(package, find_packages(directory))) 21 | 22 | @staticmethod 23 | def __add_prefix(prefix, l): 24 | return list(map(lambda x: prefix+"."+x, l)) 25 | 26 | 27 | if __name__ == "__main__": 28 | with open('README.md') as f: 29 | readme = f.read() 30 | 31 | with open('LICENSE') as f: 32 | license = f.read() 33 | 34 | with open('VERSION') as f: 35 | version = f.read() 36 | 37 | packages = Packages() 38 | packages.add_package('n0test', 'build/n0test') 39 | packages.add_package('n0proto', 'n0proto.py') 40 | 41 | print(packages.package) 42 | 43 | setup( 44 | name='n0stack', 45 | version="0.1."+version, 46 | description='A simple cloud provider using gRPC', 47 | long_description=readme, 48 | author='h-otter', 49 | author_email='h-otter@outlook.jp', 50 | install_requires=['protobuf', 'grpcio-tools', 'numpy'], 51 | url='https://github.com/n0stack/n0stack', 52 | license=license, 53 | packages=packages.package, 54 | package_dir=packages.package_dir, 55 | ) 56 | --------------------------------------------------------------------------------