├── .gitignore ├── .gitreview ├── .travis.yml ├── Documentation ├── ApplicationManagement.md ├── Capstanfile.md ├── Capstanignore.md ├── ConfigurationFiles.md ├── Installation.md ├── OsvFilesystem.md ├── Repository.md ├── RuntimeJava.md ├── RuntimeNative.md ├── RuntimeNode.md ├── RuntimePython.md ├── UnderTheHood.md ├── Volumes.md ├── WalkthroughNodeJS.md └── generated │ └── CLI.md ├── LICENSE ├── README.md ├── capstan.go ├── capstan_test.go ├── cmd ├── build.go ├── compose.go ├── compose_test.go ├── config.go ├── delete.go ├── info.go ├── instance.go ├── package.go ├── package_test.go ├── pull.go ├── run.go ├── runtime.go ├── runtime_test.go ├── stack.go ├── stop.go ├── testdata │ └── hashing │ │ ├── dir1 │ │ ├── dir3 │ │ │ ├── another-file │ │ │ └── file3 │ │ └── file2 │ │ ├── dir2 │ │ └── file-in-dir2 │ │ ├── file1 │ │ ├── file4 │ │ ├── meta │ │ └── package.yaml │ │ └── symlink-to-file1 ├── volume.go └── volume_test.go ├── core ├── capstanignore.go ├── hashcache.go ├── image.go ├── package.go ├── rpm.go ├── template.go ├── template_test.go ├── yaml.go └── yaml_test.go ├── cpio └── cpio.go ├── go.mod ├── go.sum ├── hypervisor ├── default_darwin.go ├── default_freebsd.go ├── default_linux.go ├── default_windows.go ├── gce │ └── gce.go ├── hyperkit │ └── hyperkit.go ├── qemu │ ├── qemu.go │ └── qemu_test.go ├── util.go ├── util_test.go ├── vbox │ └── vbox.go └── vmw │ └── vmw.go ├── image ├── gce │ └── gce.go ├── probe.go ├── qcow2 │ └── qcow2.go ├── vdi │ └── vdi.go └── vmdk │ └── vmdk.go ├── install ├── install.bat ├── make-dist ├── nat └── nat.go ├── provider └── openstack │ ├── openstack.go │ ├── openstack_auth.go │ └── openstack_internal_test.go ├── runtime ├── java.go ├── java_test.go ├── native.go ├── node.go ├── node_test.go ├── parser.go ├── python.go ├── python_test.go └── runtime.go ├── scripts ├── capstan.bash ├── download ├── generate_cli_doc.py └── version ├── test ├── core │ └── capstanignore_test.go ├── runtime │ └── runtime_test.go └── util │ └── parser_test.go ├── testing ├── checkers.go ├── checkers_test.go ├── common.go └── prepare_files.go └── util ├── github_releases_repository.go ├── github_releases_repository_test.go ├── github_repository.go ├── image_util.go ├── mac.go ├── nbd.go ├── package_test.go ├── parser.go ├── parser_test.go ├── remote_repository.go ├── repository.go ├── repository_test.go ├── rofs.go ├── rofs_test.go ├── s3_repository.go ├── s3_repository_test.go ├── termios.go ├── termios_windows.go ├── testdata └── github │ ├── cloudius-systems │ └── osv │ │ └── releases │ │ └── download │ │ ├── v0.51.0 │ │ ├── index.yaml │ │ │ └── payload │ │ ├── osv-loader.qemu │ │ │ └── payload │ │ ├── osv.bootstrap.yaml │ │ │ └── payload │ │ ├── osv.cli.yaml │ │ │ └── payload │ │ ├── osv.httpserver-api.yaml │ │ │ └── payload │ │ ├── osv.httpserver-html5-cli.yaml │ │ │ └── payload │ │ ├── osv.httpserver-html5-gui.yaml │ │ │ └── payload │ │ ├── osv.iperf.yaml │ │ │ └── payload │ │ ├── osv.lighttpd.yaml │ │ │ └── payload │ │ ├── osv.memcached.yaml │ │ │ └── payload │ │ ├── osv.mysql.yaml │ │ │ └── payload │ │ ├── osv.netperf.yaml │ │ │ └── payload │ │ ├── osv.nginx.yaml │ │ │ └── payload │ │ ├── osv.node-js.yaml │ │ │ └── payload │ │ ├── osv.openjdk10-java-base.yaml │ │ │ └── payload │ │ ├── osv.redis-memonly.yaml │ │ │ └── payload │ │ ├── osv.run-go.yaml │ │ │ └── payload │ │ └── osv.run-java.yaml │ │ │ └── payload │ │ ├── v0.52.0 │ │ ├── osv.ffmpeg.yaml │ │ │ └── payload │ │ ├── osv.httpserver-api.yaml │ │ │ └── payload │ │ ├── osv.libz.yaml │ │ │ └── payload │ │ └── osv.python3x.yaml │ │ │ └── payload │ │ ├── v0.53.0 │ │ ├── osv.httpserver-api.mpm │ │ │ └── payload │ │ ├── osv.httpserver-api.yaml │ │ │ └── payload │ │ ├── osv.httpserver-html5-cli.yaml │ │ │ └── payload │ │ ├── osv.httpserver-html5-gui-and-cli.yaml │ │ │ └── payload │ │ ├── osv.httpserver-html5-gui.yaml │ │ │ └── payload │ │ └── osv.python3x.yaml │ │ │ └── payload │ │ ├── v0.54.0 │ │ ├── osv.bootstrap.mpm │ │ │ └── payload │ │ ├── osv.bootstrap.yaml │ │ │ └── payload │ │ ├── osv.cli.yaml │ │ │ └── payload │ │ ├── osv.httpserver-api.yaml │ │ │ └── payload │ │ ├── osv.httpserver-html5-cli.yaml │ │ │ └── payload │ │ ├── osv.httpserver-html5-gui.yaml │ │ │ └── payload │ │ ├── osv.libz.yaml │ │ │ └── payload │ │ ├── osv.run-go.yaml │ │ │ └── payload │ │ └── osv.run-java.yaml │ │ │ └── payload │ │ └── v0.57.0 │ │ ├── osv-loader.qemu.x86_64 │ │ └── payload │ │ ├── osv.bootstrap.mpm.x86_64 │ │ └── payload │ │ └── osv.bootstrap.yaml │ │ └── payload │ └── repos │ └── cloudius-systems │ └── osv │ └── releases │ ├── payload │ └── tags │ ├── v0.51.0 │ └── payload │ ├── v0.53.0 │ └── payload │ └── v0.57.0 │ └── payload ├── util.go ├── util_darwin.go ├── util_freebsd.go ├── util_linux.go ├── util_posix.go ├── util_test.go └── util_win.go /.gitignore: -------------------------------------------------------------------------------- 1 | capstan 2 | .*.sw* 3 | .idea/* 4 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=review.mikelangelo-project.eu 3 | port=29418 4 | project=mikelangelo/capstan 5 | defaultbranch=master 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.14 5 | - 1.15 6 | - tip 7 | 8 | before_install: 9 | - sudo apt-get update -qq 10 | - sudo apt-get install qemu-kvm qemu-utils 11 | - export PATH=/home/travis/gopath/bin:$PATH 12 | - go get gopkg.in/check.v1 13 | 14 | go_import_path: github.com/cloudius-systems/capstan 15 | 16 | install: 17 | - ./install 18 | -------------------------------------------------------------------------------- /Documentation/Capstanfile.md: -------------------------------------------------------------------------------- 1 | # Capstanfile 2 | 3 | The ``Capstanfile`` is a YAML config file for specifying Capstan images. 4 | 5 | An minimal file looks as follows: 6 | 7 | ``` 8 | base: cloudius/osv-base 9 | 10 | cmdline: /tools/hello.so 11 | 12 | build: make 13 | 14 | files: 15 | /tools/hello.so: hello.so 16 | ``` 17 | 18 | ``base`` specifies the base image that is amended with ``files`` use ``capstan images`` (local images) for a list of available images. 19 | 20 | ``cmdline`` is the startup command line passed to OSv. 21 | 22 | ``build`` specifies an optional build command. 23 | 24 | ``files`` is a map of files that are amended to the base image. The left side 25 | specifies the full path of the file as it will appear in the image and the 26 | right side specifies a relative path to current directory of the actual file 27 | that is added to the image. 28 | 29 | File mapping supports basename variable substitution with the "&" character. 30 | You can specify this: 31 | 32 | ``` 33 | files: 34 | /usr/app/app.jar: build/& 35 | ``` 36 | 37 | which expands to the following during build: 38 | 39 | ``` 40 | files: 41 | /usr/app/app.jar: build/app.jar 42 | ``` 43 | 44 | Here ``/usr/app/app.jar`` is the path in the built image and ``build/app.jar`` 45 | is the file that is picked up from the local filesystem. 46 | 47 | ``rootfs`` specifies a directory that is amended to the base image. The directory hierarchy 48 | in the base image will be the same as in the rootfs directory. If rootfs is not specified 49 | in Capstanfile, a default rootfs directory named ROOTFS will be used. If both files and rootfs 50 | are specified, both will be used to amended the base image. 51 | 52 | ## How to create base images 53 | The easiest way to create Capstan base images is to use the OSv shell script [`build-capstan-base-image`](https://github.com/cloudius-systems/osv/blob/master/scripts/build-capstan-base-image). 54 | The aforementioned script accepts list of OSv apps, creates an image and optionally copies 55 | it to the local Capstan repo like so: 56 | ```bash 57 | ./scripts/build-capstan-base-image cloudius/python3 python3x 'OSv base with Python 3' 58 | ``` 59 | 60 | ## RPM support 61 | 62 | Capstan also supports installing RPMs with the ``rpm-base`` option: 63 | 64 | ``` 65 | rpm-base: 66 | name: java-1.7.0-openjdk 67 | version: 1.7.0.60 68 | release: 2.4.5.0.fc19 69 | arch: x86_64 70 | ``` 71 | -------------------------------------------------------------------------------- /Documentation/Capstanignore.md: -------------------------------------------------------------------------------- 1 | # .capstanignore 2 | 3 | When composing a unikernel Capstan uploads **all** files from current directory to the unikernel. For 4 | simple demo applications this is totally acceptable, but sooner or later you'll want to prevent 5 | Capstan from uploading some subfolders (e.g. .idea/) and that's where `.capstanignore` comes in. 6 | 7 | 8 | ## Ignored by default 9 | There are some folders that are ignored (i.e. not copied to the unikernel that you're composing) 10 | even if you don't provide your own `.capstanignore` file: 11 | ``` 12 | /meta 13 | /mpm-pkg 14 | /.git 15 | ``` 16 | These folders do not get uploaded to the unikernel even if they exist in your project folder. Go ahead, 17 | verify by running: 18 | ```bash 19 | $ capstan config print 20 | --- global configuration 21 | CAPSTAN_ROOT: /home/miha/.capstan 22 | CAPSTAN_REPO_URL: https://mikelangelo-capstan.s3.amazonaws.com/ 23 | CAPSTAN_DISABLE_KVM: false 24 | 25 | --- curent directory configuration 26 | CAPSTANIGNORE: 27 | /meta 28 | /mpm-pkg 29 | /.git 30 | ``` 31 | 32 | ## Specify your own .capstanignore 33 | Go ahead, create a new file in your project root directory and name it `.capstanignore`. Below 34 | please find a valid `.capstanignore` file example showing the syntax: 35 | 36 | ``` 37 | # ignores file 'myfile.txt' in project root directory 38 | /myfile.txt 39 | 40 | # ignores file 'myfile.txt' in '/myfolder' directory 41 | /myfolder/myfile.txt 42 | 43 | # ignores any file 'myfile.txt' in whole project (recursive) 44 | /**/myfile.txt 45 | 46 | # ignores folder 'myfolder' and all its content 47 | /myfolder 48 | 49 | # ignores all files and folders inside 'myfolder', but keeps the folder 50 | /myfolder/* 51 | 52 | # ignores any file with '.txt' suffix in project root directory 53 | /*.txt 54 | 55 | # ignores any file with '.txt' suffix in whole project (recursive) 56 | /**/*.txt 57 | 58 | # ignores any file that is inside any first-level directory (e.g. /result/file.txt), 59 | # but keeps the folders 60 | /*/* 61 | ``` 62 | 63 | As you can see the syntax is that of .gitignore only you need to start each pattern with slash `/`. Note 64 | that negation (`!`) is not supported. You can see what files are actually getting excluded in your 65 | case by using `--verbose` flag: 66 | ```bash 67 | $ capstan package collect --verbose 68 | Resolved runtime into: node 69 | Prepending 'node' runtime dependencies to dep list: [app.node-4.4.5] 70 | .capstanignore: ignore /bin 71 | .capstanignore: ignore /bin/osv-launch-services.sh 72 | .capstanignore: ignore /bin/osv-launch-worker.sh 73 | .capstanignore: ignore /bin/upload_batch.sh 74 | .capstanignore: ignore /doc 75 | .capstanignore: ignore /doc/setup-phase.png 76 | .capstanignore: ignore /doc/worker-phase.png 77 | ``` 78 | -------------------------------------------------------------------------------- /Documentation/Installation.md: -------------------------------------------------------------------------------- 1 | # Step-by-step Installation Guide 2 | 3 | You can install Capstan either by downloading pre-built binaries or building it 4 | from sources. You need to install QEMU in both cases. 5 | 6 | 7 | ## Install Prerequisites 8 | 9 | ### QEMU 10 | Capstan needs QEMU hypervisor being installed on your system, even if you don't intend to 11 | run unikernels with QEMU provider. So go ahead and install it: 12 | 13 | On Fedora: 14 | 15 | ``` 16 | $ sudo yum install qemu-system-x86 qemu-img 17 | ``` 18 | 19 | On Ubuntu 20 | 21 | ``` 22 | $ sudo apt-get install qemu-system-x86 qemu-utils 23 | ``` 24 | 25 | On OS X: 26 | 27 | ``` 28 | $ brew install qemu 29 | ``` 30 | 31 | On FreeBSD: 32 | 33 | ``` 34 | $ sudo pkg install qemu 35 | ``` 36 | 37 | ## Install Capstan 38 | Install capstan by manually downloading the relevant binary from 39 | [the latest Github release assets page](https://github.com/cloudius-systems/capstan/releases/latest) 40 | into `$HOME/bin` directory: 41 | 42 | You can then use Capstan tool with `$HOME/bin/capstan --help` or include `$HOME/bin` into `PATH` and 43 | use it simply with `capstan --help`. 44 | 45 | There you go, happy unikernel creating! 46 | 47 | ## Install Capstan from source (advanced) 48 | 49 | ### Install Go 1.11+ 50 | Capstan is a Go project and needs to be compiled first. But heads up, compiling Go project is trivial, 51 | as long as you have Go installed. Consult [official documentation](https://golang.org/doc/install) 52 | to learn how to install Go, or use this bash snippet to do it for you: 53 | ```bash 54 | curl https://dl.google.com/go/go1.13.5.linux-amd64.tar.gz | sudo tar xz -C /usr/local 55 | sudo mv /usr/local/go /usr/local/go1.13 56 | sudo ln -s /usr/local/go1.13 /usr/local/go 57 | 58 | export GOPATH=$HOME/go 59 | export PATH=$GOPATH/bin:$PATH 60 | export PATH=/usr/local/go/bin:$PATH 61 | ``` 62 | 63 | ### Compile Capstan 64 | Since Capstan is hosted on GitHub, the compilation process is as simple as: 65 | ``` 66 | git clone git@github.com:cloudius-systems/capstan.git 67 | cd capstan 68 | export GO111MODULE=on # Needed for Go 1.11, 1.12 69 | go install 70 | ``` 71 | That's it, we have Capstan installed. You should be able to use Capstan immediately because it was installed in `$GOPATH/bin` added to your `$PATH` above. To test that it works, try: 72 | ``` 73 | capstan --help 74 | ``` 75 | 76 | ## Maintain Capstan 77 | Capstan is managed with Go Modules as [recommended](https://blog.golang.org/using-go-modules), while developing on Capstan, you should follow these steps: 78 | 1. Update import path and corresponding code as you might do 79 | 2. Test you changes with `go test ./...` 80 | 3. Clear up `go.mod` and `go.sum` file with `go mod tidy` 81 | 82 | Then you are ready to tag and release new version. To learn more details about maintaining Go project on Module Mode, see [Go Modules Serial Blogs](https://blog.golang.org/using-go-modules) 83 | 84 | ## Configure Capstan (advanced) 85 | Capstan uses optimized default values under the hood. But you are allowed to override them with 86 | your own values and this section describes how. Actually, there are three ways to override them 87 | (first non-empty value is taken), although not every variable can be set using all three ways: 88 | 89 | ### 1) using command-line arguments 90 | You can override some variables using command-line arguments. Please note that you need to repeat 91 | the argument for every command you use, Capstan does not memorize it. Also please pay attention to 92 | the location of the argument. Capstan command must look like this: 93 | ```bash 94 | $ capstan {command-line-configuration} other sub commands and args 95 | # For example: 96 | $ capstan -r v0.53.0 package compose img1 --size 10GB 97 | |- here --| 98 | ``` 99 | 100 | List of supported arguments: 101 | 102 | * `-r ` specifies the OSv github release tag that is used to fetch precompiled 103 | kernel and packages from. 104 | 105 | ### 2) using configuration file 106 | Capstan supports configuration file to permanently override some internal defaults. This file is 107 | located in `$HOME/.capstan/config.yaml`. It is not created by default, so you need to create the file 108 | and folder if they do not exist yet. The file is nothing but a simple yaml containing "key: value" 109 | pairs e.g. 110 | ```yaml 111 | repo_url: https://mikelangelo-capstan.s3.amazonaws.com/ 112 | disable_kvm: false 113 | qemu_aio_type: threads 114 | ``` 115 | List of supported keys: 116 | 117 | * `repo_url` overrides the default remote S3 repository URL that is used to fetch precompiled 118 | packages from if `--s3` flag is enabled. 119 | * `disable_kvm` by default KVM acceleration is turned on to speed up unikernel creation, but in 120 | certain circumstances this results in error. Set this to `true` if you have problems using KVM. 121 | * `qemu_aio_type` by default QEMU aio type is set to `threads` for compatibility reasons. A faster 122 | aio option may be `native` depending on the version of QEMU, but it's not supported on all platforms. 123 | 124 | Please note that if command line argument is used to override the same value (e.g. -u for repository 125 | URL), then the value from configuration file is ignored. 126 | 127 | ### 3) using environment variables 128 | Capstan reads execution environment to override internal variables. Just set environment variable 129 | prior to calling Capstan commands, for example: 130 | ```bash 131 | $ export CAPSTAN_REPO_URL=https://mikelangelo-capstan.s3.amazonaws.com/ 132 | $ capstan package compose ... 133 | ``` 134 | 135 | List of supported environment variables: 136 | 137 | * `CAPSTAN_REPO_URL` overrides the default remote repository URL that is used to fetch precompiled 138 | packages from. 139 | * `DISABLE_KVM` [true|false] 140 | * `QEMU_AIO_TYPE` [threads|native] 141 | 142 | Please note that environment variables have the lowest priority - if same variable is set using either 143 | command-line argument or configuration file, then environment variable is ignored. 144 | 145 | ### Double-check your configuration 146 | There is a Capstan command to double-check which configuration values are eventually used: 147 | ``` 148 | capstan config print 149 | ``` -------------------------------------------------------------------------------- /Documentation/OsvFilesystem.md: -------------------------------------------------------------------------------- 1 | # Filesystems 2 | 3 | OSv unikernel supports multiple filesystems - ZFS (Zeta File System), ROFS (Read-Only File System) and RamFS. 4 | This means that user can build an image, run it and have OSv mount one of these filesystems. 5 | 6 | ## ZFS 7 | ZFS is the original and default filesystem OSv images built by Capstan come with. ZFS is very performant read-write file system 8 | ideal for running stateful apps on OSv when data needs to be changed and persisted. For more details please 9 | read [this](https://github.com/cloudius-systems/osv/wiki/ZFS) and [that](https://github.com/cloudius-systems/osv/wiki/Managing-your-storage). 10 | 11 | ## ROFS 12 | Read-Only Filesystem has been added recently to OSv and requires 13 | [latest version 0.51](https://github.com/cloudius-systems/osv/releases/tag/v0.51.0). As the name suggests ROFS allows only 14 | reading data from files and therefore well suites running stateless applications on OSv when only code has to be accessed. For more details 15 | look at [this commit](https://github.com/cloudius-systems/osv/commit/cd449667b7f86721095ddf4f9f3f8b87c1c414c9) and 16 | [original Python script](https://github.com/cloudius-systems/osv/blob/master/scripts/gen-rofs-img.py). 17 | 18 | ## Composing packages 19 | When composing OSv images using ```capstan package compose```, please select desired filesystem using new ```-fs``` option like so: 20 | ``` 21 | capstan package compose --fs rofs 22 | ``` 23 | -------------------------------------------------------------------------------- /Documentation/Repository.md: -------------------------------------------------------------------------------- 1 | # Capstan Repository 2 | Capstan downloads base unikernel and precompiled packages from OSv Github releases repository 3 | (the default) or remote repository in AWS S3. 4 | If S3 mode selected (--s3), following S3 repository is used by default: 5 | 6 | ``` 7 | https://mikelangelo-capstan.s3.amazonaws.com 8 | ``` 9 | 10 | There is nothing special about S3 repository, it's just a bunch of directories and files that are made 11 | available on the internet. So you can easily create your own and make capstan use it. No authentication is supported, just a simple HTTP/HTTPS download. 12 | 13 | ## OSv Github Repository 14 | By default capstan pulls OSv kernel and any required packages from [OSv Github repository](https://github.com/cloudius-systems/osv/releases). 15 | The global parameter ```--release-tag``` (or ```-r```) can be used to override default behavior and make 16 | capstan pull artifacts published for specific release (for example ```--r v0.53.0```) or the latest release (```--r latest```). 17 | By default capstan would pull first found artifact from the list of assets published for all releases (```--r any```). 18 | 19 | ## S3 Repository Structure 20 | One can use global ```--s3``` parameter to make capstan pull kernel and packages from repository in AWS S3. 21 | The default public repository created as part of MIKELANGELO project is structured as follows: 22 | 23 | ``` 24 | mike 25 | |- osv-loader # 26 | | |- index.yaml # base unikernel (path is hard-coded in Capstan) 27 | | |- osv-loader.qemu.gz # 28 | packages 29 | |- osv.bootstrap.mpm # actual package 30 | |- osv.bootstrap.yaml # metadata describing package 31 | |- erlang-7.0.mpm 32 | |- erlang-7.0.yaml 33 | |- ... 34 | ``` 35 | 36 | As shown above, there are two root directories in the repository: `mike` and `packages`. 37 | 38 | ### `mike` root directory 39 | This directory contains base unikernel that Capstan builds your own unikernel upon. The 40 | "recipe" to prepare the two files is best shown [here](https://github.com/mikelangelo-project/capstan-packages/blob/master/docker_files/capstan-packages.py#L226-L259). 41 | To prepare the `osv-loader.qemu.gz` file we simply checkout latest [OSv master](https://github.com/cloudius-systems/osv) and build it. Then we take the 42 | `$OSV_DIR/build/last/loader.img` file and tar.gz compress it into `osv-loader.qemu.gz`. 43 | 44 | ### `packages` root directory 45 | This directory contains all the Capstan packages. Each package is stored in two files that 46 | only differ in suffix: `{package-name}.mpm` and `{package-name}.yaml`. The former is actually 47 | a tar.gz file containing all the package files, but please make use of `capstan package build` 48 | command to compress it, as can be seen in [this](https://github.com/mikelangelo-project/capstan-packages/blob/master/docker_files/capstan-packages.py#L391-L425) "recipe". 49 | The latter is a simple metafile in yaml format. 50 | 51 | ## Hosting your own repository 52 | It should be very simple to host your own Capstan repository, just make sure that you maintain 53 | the directory structure as described above. 54 | 55 | Please consult [this](./Installation.md#2-using-configuration-file) file to learn how to tell Capstan 56 | what remote repository to connect to. 57 | -------------------------------------------------------------------------------- /Documentation/RuntimeJava.md: -------------------------------------------------------------------------------- 1 | # Runtime `java` 2 | This document describes how to write a valid `meta/run.yaml` configuration file 3 | for running **Java** application. Please note that you needn't require Java 4 | MPM package manually since Capstan will require following package automatically: 5 | 6 | ``` 7 | - openjdk8-zulu-compact1 8 | ``` 9 | 10 | ## Running .class 11 | Following configuration can be used to run a javac-compiled Java application inside OSv: 12 | 13 | ```yaml 14 | # meta/run.yaml 15 | 16 | runtime: java 17 | 18 | config_set: 19 | hello: 20 | main: MyClass 21 | args: 22 | - Johnny 23 | ``` 24 | 25 | Where the content of MyClass.java file is: 26 | 27 | ```java 28 | public class MyClass 29 | { 30 | public static void main(String[] args) 31 | { 32 | System.out.println("Hello:"); 33 | for(String el : args){ 34 | System.out.printf("- %s\n", el); 35 | } 36 | } 37 | } 38 | ``` 39 | Please note that OSv doesn't really need the .java file, but only the .class file that was 40 | produced like this: 41 | 42 | ```bash 43 | $ javac MyClass.java 44 | ``` 45 | 46 | Example: 47 | 48 | ```bash 49 | $ capstan package compose demo 50 | $ capstan run demo --boot hello 51 | Command line will be set based on --boot parameter 52 | Created instance: demo 53 | Setting cmdline: runscript /run/hello 54 | OSv v0.24-448-g829bf76 55 | eth0: 192.168.122.15 56 | java.so: Starting JVM app using: io/osv/nonisolated/RunNonIsolatedJvmApp 57 | java.so: Setting Java system classloader to NonIsolatingOsvSystemClassLoader 58 | Hello: 59 | - Johnny 60 | ``` 61 | 62 | ## Running .jar 63 | Following configuration can be used to run .jar file inside OSv: 64 | 65 | ```yaml 66 | # meta/run.yaml 67 | 68 | runtime: java 69 | 70 | config_set: 71 | hello: 72 | main: /MyClass.jar 73 | args: 74 | - Johnny 75 | ``` 76 | The MyClass.jar was prepared out of the very same MyClass.java file as provided above by using the following 77 | set of commands: 78 | 79 | ```bash 80 | $ javac MyClass.java 81 | $ jar cfe MyClass.jar MyClass MyClass.class 82 | ``` 83 | 84 | Example: 85 | 86 | ```bash 87 | $ capstan package compose demo 88 | $ capstan run demo --boot hello 89 | Command line will be set based on --boot parameter 90 | Created instance: demo 91 | Setting cmdline: runscript /run/hello 92 | OSv v0.24-448-g829bf76 93 | eth0: 192.168.122.15 94 | java.so: Starting JVM app using: io/osv/nonisolated/RunNonIsolatedJvmApp 95 | java.so: Setting Java system classloader to NonIsolatingOsvSystemClassLoader 96 | Hello: 97 | - Johnny 98 | ``` 99 | -------------------------------------------------------------------------------- /Documentation/RuntimeNative.md: -------------------------------------------------------------------------------- 1 | # Runtime `native` 2 | This document describes how to write a valid `meta/run.yaml` configuration file 3 | for running **native Linux** application. This is the most basic runtime that does not depend 4 | on any intermediate runtime MPM package (Java, Python, etc), but rather launches the corresponding 5 | native ELF x86_64 file directly. 6 | 7 | ## Running Linux x86_64 applications 8 | Bear in mind that you need to either use the application binaries from the Linux host or compile it from 9 | the source code on your own, Capstan won't do it for you. 10 | 11 | Using pre-built binaries from Linux host is the easiest way - you just need to identify all the binaries 12 | and any libraries it depends on and relevant configuration files and copy all those to your capstan 13 | project directory. Also make sure that the Linux binaries are standard Linux position-independent executables ("PIE") 14 | or position-dependent executables (non-relocatable dynamically linked executable) or shared libraries - 15 | (for details please see [OSv Linux compatibility doc](https://github.com/cloudius-systems/osv/wiki/OSv-Linux-ABI-Compatibility)). 16 | One way to automate this process is to use [this OSv shell script](https://github.com/cloudius-systems/osv/blob/master/scripts/manifest_from_host.sh) like 17 | so: 18 | ```bash 19 | $ ./scripts/manifest_from_host.sh -w ls 20 | $ ./scripts/build -j4 --append-manifest export=selected usrskel=none 21 | ``` 22 | 23 | The resulting files from `build/export` directory have to be copied to the capstan project directory. 24 | Once all application binaries and configuration files are placed in the capstan project directory, 25 | you need to create `meta/run.yaml` to specify the application boot command: 26 | 27 | ```yaml 28 | # meta/run.yaml 29 | runtime: native 30 | config_set: 31 | default: 32 | bootcmd: "/ls -l" 33 | config_set_default: default 34 | ``` 35 | 36 | and compose the unikernel image like so: 37 | ```bash 38 | $ capstan package compose demo 39 | $ capstan run demo 40 | ``` 41 | 42 | If you decide to compile it, for the best results the system that you're compiling your application on 43 | should be compatible with the system the base unikernel (mike/osv-loader) was built on, notice the "Platform" column: 44 | 45 | ```bash 46 | $ capstan images 47 | Name Description Version Created Platform 48 | mike/osv-loader OSv Bootloader d9a8771 2017-11-16 07:46 Ubuntu-14.04 49 | ^ 50 | ``` 51 | 52 | This basically means that you should compile your application on Ubuntu 14.04 based on this example. If compiling, say, on Ubuntu 16.04, 53 | then application may crash on OSv. Also, you need to use `-fPIC -shared` or `-fpie -pie` flags when compiling to create a 54 | shared library or standard Linux position-independent executables ("PIE") and position-dependent executables (non-relocatable dynamically linked executable) - 55 | (for details please see [OSv Linux compatibility doc](https://github.com/cloudius-systems/osv/wiki/OSv-Linux-ABI-Compatibility)). Please 56 | navigate [here](https://github.com/mikelangelo-project/osv-utils-xlab) to see some example C applications (echo.c, 57 | sleep.c, cat.c, etc.) as well as corresponding Makefile. 58 | 59 | Once application is compiled, you only need to copy the resulting `.so` in Capstan project directory and provide the boot 60 | command to run it. Following configuration can be used to run your .so application inside OSv, `echo.so` in this example: 61 | 62 | ```yaml 63 | # meta/run.yaml 64 | 65 | runtime: native 66 | 67 | config_set: 68 | say_hey: 69 | bootcmd: /echo.so Hey hey! 70 | ``` 71 | 72 | Example: 73 | 74 | ```bash 75 | $ capstan package compose demo 76 | $ capstan run demo --boot say_hey 77 | Command line will be set based on --boot parameter 78 | Created instance: demo 79 | Setting cmdline: runscript /run/echo 80 | OSv v0.24-434-gf4d1dfb 81 | eth0: 192.168.122.15 82 | Hey hey! 83 | ``` -------------------------------------------------------------------------------- /Documentation/RuntimeNode.md: -------------------------------------------------------------------------------- 1 | # Runtime `node` 2 | This document describes how to write a valid `meta/run.yaml` configuration file 3 | for running **Node.js** application. Please note that you needn't require Node 4 | MPM package manually since Capstan will require require following package automatically: 5 | 6 | ``` 7 | - node-4.4.5 8 | ``` 9 | 10 | ## Interactive node interpreter 11 | Following configuration can be used to run interactive Node.js interpreter inside OSv: 12 | 13 | ```yaml 14 | # meta/run.yaml 15 | 16 | runtime: node 17 | 18 | config_set: 19 | interpreter: 20 | shell: true 21 | ``` 22 | 23 | Example: 24 | 25 | ```bash 26 | $ capstan package compose demo 27 | $ capstan run demo --boot interpreter 28 | Command line will be set based on --boot parameter 29 | Created instance: demo 30 | Setting cmdline: runscript /run/interpreter 31 | OSv v0.24-434-gf4d1dfb 32 | eth0: 192.168.122.15 33 | > console.log("Hello World from Node.js interactive interpreter!") 34 | Hello World from Node.js interactive interpreter! 35 | undefined 36 | > 37 | ``` 38 | 39 | ## Node.js script 40 | Following configuration can be used to run Node.js script inside OSv: 41 | 42 | ```yaml 43 | # meta/run.yaml 44 | 45 | runtime: node 46 | 47 | config_set: 48 | hello: 49 | main: /greeting.js 50 | args: 51 | - "Johnny" 52 | ``` 53 | Note that /greeting.js script mentioned in the snippet above is a simple script that we've 54 | implemented for the sake of demo. It prints node arguments to the console and then 55 | exits. 56 | 57 | Example: 58 | 59 | ```bash 60 | $ capstan package compose demo 61 | $ capstan run demo --boot hello 62 | Command line will be set based on --boot parameter 63 | Created instance: demo 64 | Setting cmdline: runscript /run/hello 65 | OSv v0.24-434-gf4d1dfb 66 | eth0: 192.168.122.15 67 | Hello to: 68 | - Johnny 69 | ``` 70 | -------------------------------------------------------------------------------- /Documentation/RuntimePython.md: -------------------------------------------------------------------------------- 1 | # Runtime `python` 2 | This document describes how to write a valid `meta/run.yaml` configuration file 3 | for running **Python** applications. 4 | 5 | # Newer `native` method 6 | Require python: 7 | ```yaml 8 | require: 9 | - osv.python3x 10 | ``` 11 | 12 | And set `bootcmd` to `python3`: 13 | ```yaml 14 | # meta/run.yaml 15 | runtime: native 16 | 17 | config_set: 18 | default: 19 | bootcmd: python3 20 | ``` 21 | 22 | # Deprecated `python` method 23 | Note that you needn't require Python MPM package manually since Capstan will require following package automatically: 24 | ``` 25 | - python-2.7 26 | ``` 27 | 28 | The following configuration runs an interactive Python interpreter using the python runtime: 29 | ```yaml 30 | # meta/run.yaml 31 | 32 | runtime: python 33 | 34 | config_set: 35 | interpreter: 36 | shell: true 37 | ``` 38 | 39 | Example: 40 | 41 | ```bash 42 | $ capstan package compose demo 43 | $ capstan run demo --boot interpreter 44 | Command line will be set based on --boot parameter 45 | Created instance: demo 46 | Setting cmdline: runscript /run/interpreter 47 | OSv v0.24-448-g829bf76 48 | eth0: 192.168.122.15 49 | Python 2.7.13+ (heads/2.7:883520a, Aug 17 2017, 08:15:22) 50 | [GCC 4.8.4] on linux2 51 | Type "help", "copyright", "credits" or "license" for more information. 52 | >>> 53 | ``` 54 | 55 | ## Python script 56 | Following configuration can be used to run Python script inside OSv: 57 | 58 | ```yaml 59 | # meta/run.yaml 60 | 61 | runtime: python 62 | 63 | config_set: 64 | hello: 65 | main: /script.py 66 | args: 67 | - Johnny 68 | ``` 69 | Note that /script.py script mentioned in the snippet above is a simple script that we've 70 | implemented for the sake of demo. It prints python arguments to the console: 71 | 72 | ```python 73 | import sys 74 | 75 | print 'Hello:' 76 | for el in sys.argv[1:]: 77 | print '- %s' % el 78 | ``` 79 | 80 | Example: 81 | 82 | ```bash 83 | $ capstan package compose demo 84 | $ capstan run demo --boot hello 85 | Command line will be set based on --boot parameter 86 | Created instance: demo 87 | Setting cmdline: runscript /run/hello 88 | OSv v0.24-448-g829bf76 89 | eth0: 192.168.122.15 90 | Hello: 91 | - Johnny 92 | ``` 93 | -------------------------------------------------------------------------------- /Documentation/UnderTheHood.md: -------------------------------------------------------------------------------- 1 | # Under the Hood 2 | 3 | Capstan is a tool that protects user from the complexity of compiling unikernel from scratch. 4 | Here we briefly describe how is this possible and why is OSv unikernel the only one among unikernels 5 | that can make use of precompiled packages. 6 | 7 | Information provided here is not needed to use Capstan. 8 | 9 | 10 | ## Why can Capstan use precompiled packages? 11 | TODO 12 | 13 | ## Implementation details 14 | Please find Capstan implementation documentation here: 15 | [![GoDoc](https://godoc.org/github.com/cloudius-systems/capstan?status.svg)](https://godoc.org/github.com/cloudius-systems/capstan) 16 | -------------------------------------------------------------------------------- /Documentation/Volumes.md: -------------------------------------------------------------------------------- 1 | # Attaching volumes 2 | Capstan supports attaching any number of pre-prepared volumes to the instance. Currenly, you need to prepare 3 | a volume manually i.e. with external tools, and then export it into a file on your host. During the `capstan run` 4 | you then provide path to this file together with some metadata. 5 | 6 | NOTE: volumes are only supported for QEMU hipervisor at the moment. 7 | 8 | ## Creating a new volume 9 | You can create a new volume with Capstan: 10 | 11 | ```bash 12 | $ capstan volume create volume1 13 | ``` 14 | 15 | The command above will create a volume of size 1GB in RAW format by default. Consult `capstan volume create --help` 16 | to learn how other size or other formats can be used. The volume will be created as `./volumes/volume1`. 17 | 18 | ## Attaching volumes 19 | Tell Capstan where your volume is and it will attach it to the instance on run: 20 | 21 | ```bash 22 | $ capstan run demo --volume ./volumes/volume1 23 | ``` 24 | 25 | The volume will get attached as `/dev/vblk1` into the unikernel. The `--volume` argument can be repeated 26 | to attach multiple volumes: 27 | 28 | ```bash 29 | $ capstan run demo --volume ./volume1 --volume ./volume2 --volume ./volume3 30 | ``` 31 | 32 | Volumes will get attached as `/dev/vblk1`, `/dev/vblk2`, `/dev/vblk3` in the same order as provided (left-to-right). 33 | 34 | ### Volume metadata 35 | You can provide volume metadata for Capstan to be able to attach it as desired: 36 | 37 | ```bash 38 | $ capstan run demo --volume ./volume1:format=qcow2:aio=threads 39 | ``` 40 | 41 | | KEY | DEFAULT VALUE | VALUES | 42 | |-------|---------------------|------------| 43 | | format | raw | raw,qcow2,vdi,vmdk| 44 | | aio | native | native/threads| 45 | | cache | none | none/writeback/writethrough/directsync/unsafe| 46 | 47 | *Consult [QEMU documentation](https://qemu.weilnetz.de/doc/qemu-doc.html#Block-device-options) for more details.* 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014-2016 Cloudius Systems, Ltd. 2 | Copyright (C) 2015-2017 XLAB, Ltd. (modifications and additions as noted in 3 | individual source files) 4 | 5 | Parts are copyright by other contributors. Please refer to copyright notices 6 | in the individual source files, and to the git commit log, for a more accurate 7 | list of copyright holders. 8 | 9 | Capstan is open-source software, distributed under the 3-clause BSD license: 10 | 11 | Redistribution and use in source and binary forms, with or without 12 | modification, are permitted provided that the following conditions are met: 13 | 14 | * Redistributions of source code must retain the above copyright notice, 15 | this list of conditions and the following disclaimer. 16 | 17 | * Redistributions in binary form must reproduce the above copyright notice, 18 | this list of conditions and the following disclaimer in the documentation 19 | and/or other materials provided with the distribution. 20 | 21 | * Neither the name of the Cloudius Systems, Ltd. nor the names of its 22 | contributors may be used to endorse or promote products derived from this 23 | software without specific prior written permission. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 26 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 27 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 29 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 30 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 31 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 32 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 33 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 34 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capstan 2 | 3 | Capstan is a command-line tool for rapidly running your application on [OSv unikernel](http://osv.io). 4 | It focuses on improving user experience when building the unikernel and attempts to support 5 | not only a variety of runtimes (C, C++, Java, Node.js etc.), but also a variety of ready-to-run 6 | applications (Hadoop HDFS, MySQL, SimpleFOAM etc.). 7 | 8 | ## Philosophy 9 | Building unikernels is generally a nightmare! It is a non-trivial task that requires deep 10 | knowledge of unikernel implementation. It depends on numerous installation tools and takes 11 | somewhat 10 minutes to prepare each unikernel once configured correctly. 12 | But an application-oriented developer is not willing to take a load of new knowledge about unikernel 13 | specifics, nor wait long minutes to compile! And that's where Capstan comes in. 14 | 15 | Capstan tends to be a tool that one configures with *application-oriented settings* 16 | (Where is application entry point? What environment variables to pass? etc.) and then 17 | runs a command or two to quickly boot up a new unikernel with application. Measured in seconds. 18 | 19 | Capstan has been designed to operate in two distinct modes: 20 | - a mode based on **Capstanfile** (conceptually similar to Dockerfile) where the Capstanfile specifies base OSv image, 21 | optional build command, list of files to append to the image and the boot command line 22 | (for details look at [Capstanfile specification](Documentation/Capstanfile.md)) 23 | - a mode based on **packages** (conceptually similar to Docker compose), where Capstan 24 | uses **prebuilt** artifacts: precompiled OSv kernel, Java runtime from the Linux host, 25 | precompiled MySQL, and many more. 26 | 27 | Either way, all you have to do is to write a Capstanfile or identify what prebuilt packages you want 28 | to have available in your unikernel and that's it. 29 | 30 | ## Features 31 | Capstan is designed to prepare and run OSv unikernel for you. 32 | With Capstan it is possible to: 33 | 34 | * prepare OSv unikernel without compiling anything but your application, in seconds 35 | * build OSv image using Capstanfile (ala Dockerfile) or compose it from pre-built packages 36 | * use any prebuilt package from the OSv github releases repository or S3 package repository, or a combination thereof 37 | * build your own packages or base images 38 | * set arbitrary size of the target unikernel filesystem 39 | * run OSv unikernel one one of the supported hypervisors 40 | 41 | But Capstan is not a magic tool that could solve all the problems for you. 42 | Capstan does **not**: 43 | 44 | * compile your application. If you have Java application, you need to use `javac` compiler and compile 45 | the application yourself prior using Capstan tool! 46 | * inspect your application. Capstan does nothing with your application but copies it into the unikernel 47 | * overcome OSv unikernel limits. Consult OSv documentation about what these limits are since they 48 | all still apply. Most notably, you can only run single process inside unikernel (forks are forbidden). 49 | 50 | ## Getting started 51 | Capstan can be installed using precompiled binary or compiled from source. 52 | [Step-by-step Capstan Installation Guide](Documentation/Installation.md) 53 | 54 | Using Capstan is rather simple: open up your project directory and create 55 | [Capstan configuration files](Documentation/ConfigurationFiles.md) 56 | there: 57 | ``` 58 | $ cd $PROJECT_DIR 59 | $ capstan package init --name {name} --title {title} --author {author} 60 | $ capstan runtime init --runtime {runtime} 61 | # edit meta/run.yaml to match your application structure 62 | ``` 63 | Add your application files to the project root directory and then use Capstan command to create unikernel image 64 | (consult [CLI Reference](Documentation/generated/CLI.md) for a list of available arguments): 65 | ``` 66 | $ capstan package compose {unikernel-image-name} 67 | ``` 68 | At this point, you have your unikernel image built. It contains all your project files plus all the 69 | precompiled artifacts that you asked for. In other words, the unikernel image contains everything and is 70 | ready to be started! As you might have expected, there is Capstan command to run unikernel for you 71 | (using KVM/QEMU hypervisor on Linux): 72 | ``` 73 | $ capstan run {unikernel-image-name} 74 | ``` 75 | Congratulations, your unikernel is up-and-running! Press CTRL + C to stop it. 76 | 77 | ## Documentation 78 | 79 | * [Step-by-step Capstan Installation Guide](Documentation/Installation.md) 80 | * [Capstanfile](Documentation/Capstanfile.md) 81 | * [Guide to Build Apps from Packages](Documentation/ApplicationManagement.md) 82 | * [Running My First Application Inside Unikernel](Documentation/WalkthroughNodeJS.md) 83 | * [Configuration Files](Documentation/ConfigurationFiles.md) 84 | * [Native Runtime](Documentation/RuntimeNative.md) 85 | * [Java Runtime](Documentation/RuntimeJava.md) 86 | * [Node.js Runtime](Documentation/RuntimeNode.md) 87 | * [Python Runtime](Documentation/RuntimePython.md) 88 | * [.capstanignore](Documentation/Capstanignore.md) 89 | * [Attaching Volumes](Documentation/Volumes.md) 90 | * [Packages Repository](Documentation/Repository.md) 91 | * [CLI Reference](Documentation/generated/CLI.md) 92 | * [OSv Filesystem](Documentation/OsvFilesystem.md) 93 | 94 | ## License 95 | Capstan is distributed under the 3-clause BSD license. 96 | 97 | ## Acknowledgements 98 | This code has been developed within the [MIKELANGELO project](https://www.mikelangelo-project.eu) 99 | (no. 645402), started in January 2015, and co-funded by the European Commission under the 100 | H2020-ICT-07-2014: Advanced Cloud Infrastructures and Services programme. 101 | -------------------------------------------------------------------------------- /capstan_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * Modifications copyright (C) 2015 XLAB, Ltd. 4 | * 5 | * This work is open source software, licensed under the terms of the 6 | * BSD license as described in the LICENSE file in the top-level directory. 7 | */ 8 | 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "io/ioutil" 14 | "os" 15 | "os/exec" 16 | "regexp" 17 | "strings" 18 | "testing" 19 | ) 20 | 21 | func runCapstan(command []string, root string) *exec.Cmd { 22 | c := exec.Command("capstan", command...) 23 | c.Env = append(os.Environ(), fmt.Sprintf("CAPSTAN_ROOT=%s", root)) 24 | return c 25 | } 26 | 27 | func TestCommandErrorCodes(t *testing.T) { 28 | root, err := ioutil.TempDir("", "capstan-root") 29 | if err != nil { 30 | t.Errorf("capstan: %v", err) 31 | } 32 | defer os.RemoveAll(root) 33 | 34 | m := []struct { 35 | cmd string 36 | errCode int 37 | errMsg string 38 | }{ 39 | {"build foo", 65, "open Capstanfile: no such file or directory\n"}, 40 | {"build", 64, "usage: capstan build [image-name]\n"}, 41 | {"pull", 64, "usage: capstan pull [image-name]\n"}, 42 | {"import", 64, "usage: capstan import [image-name] [image-file]\n"}, 43 | {"import foo", 64, "usage: capstan import [image-name] [image-file]\n"}, 44 | {"rmi", 64, "usage: capstan rmi [image-name]\n"}, 45 | {"run foo", 65, "Command line will be set to default boot\nfoo: no such image at: " + root + "/repository/foo/foo.qemu\n"}, 46 | {"run", 65, "Missing Capstanfile or package metadata\n"}, 47 | {"package help compose", 0, "capstan package compose - composes the package and all its dependencies into OSv imag"}, 48 | } 49 | for _, args := range m { 50 | cmd := runCapstan(strings.Fields(args.cmd), root) 51 | out, err := cmd.CombinedOutput() 52 | if (args.errCode == 0) && (err != nil) || 53 | (args.errCode != 0) && (err == nil) || 54 | (err != nil) && !strings.Contains(err.Error(), fmt.Sprintf("%d", args.errCode)) { 55 | t.Errorf("capstan %s: %v", args.cmd, err) 56 | } 57 | if g, e := string(out), args.errMsg; !strings.Contains(g, e) { 58 | t.Errorf("capstan %s: want %q, got %q", args.cmd, e, g) 59 | } 60 | } 61 | } 62 | 63 | func TestImportCommand(t *testing.T) { 64 | root, err := ioutil.TempDir("", "capstan-root") 65 | if err != nil { 66 | t.Errorf("capstan: %v", err) 67 | } 68 | defer os.RemoveAll(root) 69 | defer os.Remove("example.qcow2") 70 | 71 | cmd := exec.Command("qemu-img", "create", "-f", "qcow2", "example.qcow2", "128M") 72 | out, err := cmd.Output() 73 | if err != nil { 74 | t.Errorf("capstan: %v", err) 75 | } 76 | 77 | cmd = runCapstan([]string{"import", "example", "example.qcow2"}, root) 78 | out, err = cmd.Output() 79 | if err != nil { 80 | t.Errorf("capstan: %v", err) 81 | } 82 | expectedMessage := fmt.Sprintf("Importing example...\nImporting into %s/repository/example/example.qemu\n", root) 83 | if g := string(out); g != expectedMessage { 84 | t.Errorf("capstan: want %q, got %q", expectedMessage, g) 85 | } 86 | 87 | cmd = runCapstan([]string{"images"}, root) 88 | out, err = cmd.Output() 89 | if err != nil { 90 | t.Errorf("capstan: %v", err) 91 | } 92 | outLines := strings.Split(string(out), "\n") 93 | if g, e := outLines[1], "example .*\n"; regexp.MustCompile(e).MatchString(g) { 94 | t.Errorf("capstan: want prefix %q, got %q", e, g) 95 | } 96 | } 97 | 98 | func TestRmiCommand(t *testing.T) { 99 | root, err := ioutil.TempDir("", "capstan-root") 100 | if err != nil { 101 | t.Errorf("capstan: %v", err) 102 | } 103 | defer os.RemoveAll(root) 104 | defer os.Remove("example.qcow2") 105 | 106 | cmd := exec.Command("qemu-img", "create", "-f", "qcow2", "example.qcow2", "128M") 107 | out, err := cmd.Output() 108 | if err != nil { 109 | t.Errorf("capstan: %v", err) 110 | } 111 | 112 | cmd = runCapstan([]string{"import", "example1", "example.qcow2"}, root) 113 | out, err = cmd.Output() 114 | if err != nil { 115 | t.Errorf("capstan: %v", err) 116 | } 117 | e := fmt.Sprintf("Importing example1...\nImporting into %s/repository/example1/example1.qemu\n", root) 118 | if g := string(out); g != e { 119 | t.Errorf("capstan: want %q, got %q", e, g) 120 | } 121 | 122 | cmd = runCapstan([]string{"import", "example2", "example.qcow2"}, root) 123 | out, err = cmd.Output() 124 | if err != nil { 125 | t.Errorf("capstan: %v", err) 126 | } 127 | e = fmt.Sprintf("Importing example2...\nImporting into %s/repository/example2/example2.qemu\n", root) 128 | if g := string(out); g != e { 129 | t.Errorf("capstan: want %q, got %q", e, g) 130 | } 131 | 132 | cmd = runCapstan([]string{"images"}, root) 133 | out, err = cmd.Output() 134 | if err != nil { 135 | t.Errorf("capstan: %v", err) 136 | } 137 | outLines := strings.Split(string(out), "\n") 138 | if g, e := outLines[1]+"\n"+outLines[2], "example1 .*\nexample2 .*\n"; regexp.MustCompile(e).MatchString(g) { 139 | t.Errorf("capstan: want %q, got %q", e, g) 140 | } 141 | 142 | cmd = runCapstan([]string{"rmi", "example1"}, root) 143 | out, err = cmd.Output() 144 | if err != nil { 145 | t.Errorf("capstan: %v", err) 146 | } 147 | if g, e := string(out), "Removing example1...\n"; g != e { 148 | t.Errorf("capstan: want %q, got %q", e, g) 149 | 150 | } 151 | 152 | cmd = runCapstan([]string{"images"}, root) 153 | out, err = cmd.Output() 154 | if err != nil { 155 | t.Errorf("capstan: %v", err) 156 | } 157 | outLines = strings.Split(string(out), "\n") 158 | if g, e := outLines[1], "example2 .*\n"; regexp.MustCompile(e).MatchString(g) { 159 | t.Errorf("capstan: want %q, got %q", e, g) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /cmd/compose_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package cmd_test 9 | 10 | import ( 11 | "github.com/cloudius-systems/capstan/cmd" 12 | . "gopkg.in/check.v1" 13 | "io/ioutil" 14 | "os" 15 | "path/filepath" 16 | ) 17 | 18 | type TestingComposeSuite struct { 19 | capstanBinary string 20 | } 21 | 22 | func (s *TestingComposeSuite) SetUpSuite(c *C) { 23 | s.capstanBinary = "../capstan" 24 | } 25 | 26 | var _ = Suite(&TestingComposeSuite{}) 27 | 28 | func (s *TestingComposeSuite) TestCollectPathContentsWithDir(c *C) { 29 | // We are going to create an empty temp directory. 30 | tmp, _ := ioutil.TempDir("", "compose") 31 | defer os.RemoveAll(tmp) 32 | 33 | // Also add few files. 34 | os.MkdirAll(filepath.Join(tmp, "usr", "bin"), 0755) 35 | os.MkdirAll(filepath.Join(tmp, "usr", "lib"), 0755) 36 | ioutil.WriteFile(filepath.Join(tmp, "file1"), []byte("file1"), 0644) 37 | ioutil.WriteFile(filepath.Join(tmp, "usr", "bin", "file2"), []byte("file2"), 0644) 38 | ioutil.WriteFile(filepath.Join(tmp, "usr", "bin", "file3"), []byte("file3"), 0644) 39 | ioutil.WriteFile(filepath.Join(tmp, "usr", "lib", "file4"), []byte("file4"), 0644) 40 | ioutil.WriteFile(filepath.Join(tmp, "usr", "lib", "file5"), []byte("file5"), 0644) 41 | 42 | var m map[string]string 43 | m, err := cmd.CollectPathContents(tmp) 44 | c.Assert(err, IsNil) 45 | 46 | c.Assert(len(m), Equals, 8) 47 | c.Assert(m[filepath.Join(tmp, "file1")], Equals, "/file1") 48 | c.Assert(m[filepath.Join(tmp, "usr", "bin", "file2")], Equals, "/usr/bin/file2") 49 | } 50 | 51 | func (s *TestingComposeSuite) TestCollectPathContentsWithFile(c *C) { 52 | // We are going to create an empty temp directory. 53 | tmp, _ := ioutil.TempDir("", "compose") 54 | //defer os.RemoveAll(tmp) 55 | 56 | // Also add few files. 57 | os.MkdirAll(filepath.Join(tmp, "usr", "bin"), 0755) 58 | ioutil.WriteFile(filepath.Join(tmp, "usr", "bin", "file1"), []byte("file1"), 0644) 59 | 60 | var m map[string]string 61 | m, err := cmd.CollectPathContents(filepath.Join(tmp, "usr", "bin", "file1")) 62 | c.Assert(err, IsNil) 63 | 64 | c.Assert(len(m), Equals, 1) 65 | c.Assert(m[filepath.Join(tmp, "usr", "bin", "file1")], Equals, "/file1") 66 | } 67 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package cmd 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | 14 | "github.com/cloudius-systems/capstan/core" 15 | "github.com/cloudius-systems/capstan/util" 16 | "github.com/urfave/cli/v2" 17 | ) 18 | 19 | // ConfigPrint prints current capstan configuration to console. 20 | func ConfigPrint(c *cli.Context) error { 21 | repo := util.NewRepo(c.String("u")) 22 | fmt.Println("--- global configuration") 23 | repo.PrintRepo() 24 | fmt.Println() 25 | 26 | fmt.Println("--- curent directory configuration") 27 | fmt.Println("CAPSTANIGNORE:") 28 | // Read .capstanignore if exists. 29 | capstanignorePath := "./.capstanignore" 30 | if _, err := os.Stat(capstanignorePath); os.IsNotExist(err) { 31 | capstanignorePath = "" 32 | } 33 | if capstanignore, err := core.CapstanignoreInit(capstanignorePath); err == nil { 34 | capstanignore.PrintPatterns() 35 | } else { 36 | fmt.Println(err) 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /cmd/delete.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * Modifications copyright (C) 2015 XLAB, Ltd. 4 | * 5 | * This work is open source software, licensed under the terms of the 6 | * BSD license as described in the LICENSE file in the top-level directory. 7 | */ 8 | 9 | package cmd 10 | 11 | import ( 12 | "fmt" 13 | "github.com/cloudius-systems/capstan/hypervisor/gce" 14 | "github.com/cloudius-systems/capstan/hypervisor/qemu" 15 | "github.com/cloudius-systems/capstan/hypervisor/vbox" 16 | "github.com/cloudius-systems/capstan/hypervisor/vmw" 17 | "github.com/cloudius-systems/capstan/util" 18 | ) 19 | 20 | func Delete(name string) error { 21 | var err error 22 | instanceName, instancePlatform := util.SearchInstance(name) 23 | if instanceName == "" { 24 | fmt.Printf("Instance: %s not found\n", name) 25 | return nil 26 | } 27 | 28 | switch instancePlatform { 29 | case "qemu": 30 | qemu.StopVM(name) 31 | err = qemu.DeleteVM(name) 32 | case "vbox": 33 | vbox.StopVM(name) 34 | err = vbox.DeleteVM(name) 35 | case "vmw": 36 | vmw.StopVM(name) 37 | err = vmw.DeleteVM(name) 38 | case "gce": 39 | gce.StopVM(name) 40 | err = gce.DeleteVM(name) 41 | } 42 | if err != nil { 43 | fmt.Printf("Failed to delete instance: %s\n", name) 44 | return err 45 | } 46 | 47 | fmt.Printf("Deleted instance: %s\n", name) 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /cmd/info.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package cmd 9 | 10 | import ( 11 | "fmt" 12 | "github.com/cloudius-systems/capstan/image" 13 | ) 14 | 15 | func Info(path string) error { 16 | format, err := image.Probe(path) 17 | if err != nil { 18 | return err 19 | } 20 | switch format { 21 | case image.VDI: 22 | fmt.Printf("%s: VDI\n", path) 23 | case image.QCOW2: 24 | fmt.Printf("%s: QCOW2\n", path) 25 | case image.VMDK: 26 | fmt.Printf("%s: VMDK\n", path) 27 | default: 28 | fmt.Printf("%s: not a runnable image\n", path) 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /cmd/instance.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * Modifications copyright (C) 2015 XLAB, Ltd. 4 | * 5 | * This work is open source software, licensed under the terms of the 6 | * BSD license as described in the LICENSE file in the top-level directory. 7 | */ 8 | 9 | package cmd 10 | 11 | import ( 12 | "fmt" 13 | "github.com/cloudius-systems/capstan/hypervisor/gce" 14 | "github.com/cloudius-systems/capstan/hypervisor/qemu" 15 | "github.com/cloudius-systems/capstan/hypervisor/vbox" 16 | "github.com/cloudius-systems/capstan/hypervisor/vmw" 17 | "github.com/cloudius-systems/capstan/util" 18 | "io/ioutil" 19 | "os" 20 | "path/filepath" 21 | ) 22 | 23 | func Instances() error { 24 | header := fmt.Sprintf("%-35s %-10s %-10s %-15s", "Name", "Platform", "Status", "Image") 25 | fmt.Println(header) 26 | rootDir := filepath.Join(util.ConfigDir(), "instances") 27 | platforms, _ := ioutil.ReadDir(rootDir) 28 | for _, platform := range platforms { 29 | if platform.IsDir() { 30 | platformDir := filepath.Join(rootDir, platform.Name()) 31 | instances, _ := ioutil.ReadDir(platformDir) 32 | for _, instance := range instances { 33 | if instance.IsDir() { 34 | instanceDir := filepath.Join(platformDir, instance.Name()) 35 | 36 | // Instance only exists if osv.config is present. 37 | if _, err := os.Stat(filepath.Join(instanceDir, "osv.config")); os.IsNotExist(err) { 38 | continue 39 | } 40 | 41 | printInstance(instance.Name(), platform.Name(), instanceDir) 42 | } 43 | } 44 | } 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func printInstance(name, platform, dir string) error { 51 | var status string 52 | 53 | switch platform { 54 | case "qemu": 55 | status, _ = qemu.GetVMStatus(name, dir) 56 | case "vbox": 57 | status, _ = vbox.GetVMStatus(name, dir) 58 | case "vmw": 59 | status, _ = vmw.GetVMStatus(name, dir) 60 | case "gce": 61 | status, _ = gce.GetVMStatus(name, dir) 62 | } 63 | fmt.Printf("%-35s %-10s %-10s %-15s\n", name, platform, status, "") 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /cmd/pull.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * Modifications copyright (C) 2015 XLAB, Ltd. 4 | * 5 | * This work is open source software, licensed under the terms of the 6 | * BSD license as described in the LICENSE file in the top-level directory. 7 | */ 8 | 9 | package cmd 10 | 11 | import ( 12 | "github.com/cloudius-systems/capstan/util" 13 | ) 14 | 15 | func Pull(r *util.Repo, hypervisor string, image string) error { 16 | remote, err := util.IsRemoteImage(r.URL, image) 17 | if err != nil { 18 | return err 19 | } 20 | if remote { 21 | return r.DownloadImage(hypervisor, image) 22 | } 23 | return r.PullImage(image) 24 | } 25 | -------------------------------------------------------------------------------- /cmd/runtime.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * Modifications copyright (C) 2015 XLAB, Ltd. 4 | * 5 | * This work is open source software, licensed under the terms of the 6 | * BSD license as described in the LICENSE file in the top-level directory. 7 | */ 8 | 9 | package cmd 10 | 11 | import ( 12 | "fmt" 13 | "github.com/cloudius-systems/capstan/runtime" 14 | "io/ioutil" 15 | "os" 16 | "regexp" 17 | "strings" 18 | ) 19 | 20 | func RuntimePreview(runtimeName string, plain bool) error { 21 | // Resolve runtime 22 | rt, err := runtime.PickRuntime(runtime.RuntimeType(runtimeName)) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | res := fmt.Sprintln("--------- meta/run.yaml ---------") 28 | res += fmt.Sprint(composeConf(rt)) 29 | res += fmt.Sprintln("---------------------------------") 30 | 31 | if plain { 32 | res = removeComments(res) 33 | } 34 | 35 | // Actually print. 36 | fmt.Print(res) 37 | 38 | fmt.Println("Use 'capstan runtime init' to persist this template into $CWD/meta/run.yaml") 39 | 40 | return nil 41 | } 42 | 43 | func RuntimeInit(runtimeName string, plain bool, force bool) error { 44 | // Resolve runtime 45 | rt, err := runtime.PickRuntime(runtime.RuntimeType(runtimeName)) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | // Package must exist before we set run command for it. 51 | if _, err = os.Stat("meta/package.yaml"); os.IsNotExist(err) { 52 | return fmt.Errorf("Not a valid capstan pakage. Are you in corret directory?\n%s", err) 53 | } 54 | 55 | // Don't override existing meta/run.yaml 56 | if _, err = os.Stat("./meta/run.yaml"); err == nil && !force { 57 | return fmt.Errorf("meta/run.yaml already exists, use --force to override it") 58 | } 59 | 60 | // Compose content 61 | content := fmt.Sprint(composeConf(rt)) 62 | 63 | if plain { 64 | content = removeComments(content) 65 | } 66 | 67 | // Write 68 | if err = ioutil.WriteFile("meta/run.yaml", []byte(content), 0644); err != nil { 69 | return fmt.Errorf("Faile to write to meta/run.yaml: %s", err) 70 | } 71 | 72 | fmt.Println("meta/run.yaml stub successfully added to your package. Please customize it in editor.") 73 | 74 | return nil 75 | } 76 | 77 | func RuntimeList() string { 78 | res := fmt.Sprintf("%-20s%-50s%-20s\n", "RUNTIME", "DESCRIPTION", "DEPENDENCIES") 79 | for _, runtimeType := range runtime.SupportedRuntimes { 80 | rt, _ := runtime.PickRuntime(runtimeType) 81 | res += fmt.Sprintf("%-20s%-50s%-20s\n", string(runtimeType), rt.GetRuntimeDescription(), rt.GetDependencies()) 82 | } 83 | return res 84 | } 85 | 86 | func removeComments(s string) string { 87 | // Remove all comments. 88 | re := regexp.MustCompile("(?m)^ *" + "#" + ".*$[\r\n]+") 89 | s = re.ReplaceAllString(s, "") 90 | 91 | // Remove all empty lines. 92 | re = regexp.MustCompile("(?m)^ *$[\r\n]+") 93 | s = re.ReplaceAllString(s, "") 94 | 95 | return s 96 | } 97 | 98 | func composeConf(rt runtime.Runtime) string { 99 | res := ` 100 | runtime: RUNTIME 101 | 102 | config_set: 103 | 104 | ################################################################ 105 | ### This is one configuration set (feel free to rename it). ### 106 | ################################################################ 107 | myconfig1: 108 | PLACEHOLDER 109 | 110 | # Add as many named configurations as you need 111 | 112 | # OPTIONAL 113 | # What config_set should be used as default. 114 | # This value can be overwritten with --runconfig argument. 115 | config_set_default: myconfig1 116 | ` 117 | // Properly indent runtime-specific part. 118 | s := strings.TrimSpace(rt.GetYamlTemplate()) 119 | s = strings.Replace(s, "\n", "\n ", -1) 120 | res = strings.Replace(res, "PLACEHOLDER", s, -1) 121 | 122 | // Set runtime 123 | res = strings.Replace(res, "RUNTIME", rt.GetRuntimeName(), -1) 124 | return res 125 | } 126 | -------------------------------------------------------------------------------- /cmd/runtime_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package cmd 9 | 10 | import ( 11 | . "github.com/cloudius-systems/capstan/testing" 12 | . "gopkg.in/check.v1" 13 | ) 14 | 15 | type runtimeSuite struct{} 16 | 17 | var _ = Suite(&runtimeSuite{}) 18 | 19 | func (s *suite) TestRuntimeList(c *C) { 20 | // This is what we're testing here. 21 | txt := RuntimeList() 22 | 23 | // Expectations. 24 | expected := ` 25 | RUNTIME {13}DESCRIPTION {11}DEPENDENCIES {8} 26 | native {13}Run arbitrary command inside OSv {11}\[\] 27 | node {13}Run JavaScript NodeJS 4.4.5 application {11}\[node-4.4.5 \] 28 | java {13}Run Java application {11}\[openjdk8-zulu-compact1\] 29 | python {13}Run Python 2.7 application {11}\[python-2.7 \] 30 | ` 31 | c.Check(txt, MatchesMultiline, FixIndent(expected)) 32 | } 33 | -------------------------------------------------------------------------------- /cmd/stack.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * Modifications copyright (C) 2015 XLAB, Ltd. 4 | * 5 | * This work is open source software, licensed under the terms of the 6 | * BSD license as described in the LICENSE file in the top-level directory. 7 | */ 8 | 9 | package cmd 10 | 11 | import ( 12 | "fmt" 13 | "os" 14 | 15 | "github.com/cloudius-systems/capstan/provider/openstack" 16 | "github.com/cloudius-systems/capstan/util" 17 | "github.com/urfave/cli/v2" 18 | ) 19 | 20 | // OpenStackPush picks best flavor, composes package, builds .qcow2 image and uploads it to OpenStack. 21 | func OpenStackPush(c *cli.Context) error { 22 | verbose := c.Bool("verbose") 23 | imageName := c.Args().First() 24 | pullMissing := c.Bool("pull-missing") 25 | 26 | if imageName == "" { 27 | return fmt.Errorf("USAGE: capstan stack push [command options] image-name") 28 | } 29 | 30 | // Use the provided repository. 31 | repo := util.NewRepo(c.String("u")) 32 | 33 | // Get temporary name of the application. 34 | appName := imageName 35 | if verbose { 36 | fmt.Printf("appName: %s\n", appName) 37 | } 38 | 39 | // Authenticate against OpenStack Identity 40 | credentials, err := openstack.ObtainCredentials(c, verbose) 41 | if err != nil { 42 | return err 43 | } 44 | clientNova, clientGlance, err := openstack.GetClients(credentials, verbose) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | // Pick appropriate flavor. 50 | diskMB, _ := util.ParseMemSize(c.String("size")) 51 | flavor, err := openstack.GetOrPickFlavor(clientNova, c.String("flavor"), diskMB, -1, verbose) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | if verbose { 57 | fmt.Printf("Picked flavor %s (disk %dGB, memory %dMB)\n", flavor.Name, flavor.Disk, flavor.RAM) 58 | } 59 | 60 | // Always use the current directory for the package to compose. 61 | packageDir, _ := os.Getwd() 62 | 63 | // flavor.Disk is in GB, we need MB 64 | sizeMB := 1024 * int64(flavor.Disk) 65 | 66 | bootOpts := BootOptions{ 67 | Cmd: c.String("run"), 68 | Boot: c.StringSlice("boot"), 69 | EnvList: c.StringSlice("env"), 70 | PackageDir: packageDir, 71 | } 72 | 73 | // Compose image locally. 74 | fmt.Printf("Creating image of user-usable size %d MB.\n", sizeMB) 75 | err = ComposePackage(repo, []string{}, sizeMB, false, verbose, pullMissing, packageDir, appName, &bootOpts, "zfs", "") 76 | if err != nil { 77 | return err 78 | } 79 | 80 | // Remove local copy of image after uploaded to OpenStack. 81 | if !c.Bool("keep-image") { 82 | defer repo.RemoveImage(appName) 83 | } else { 84 | if verbose { 85 | fmt.Println("Keeping image locally. Please remove it manually.") 86 | } 87 | } 88 | 89 | // Push to OpenStack. 90 | fmt.Println("Uploading image to OpenStack. This may take a while.") 91 | imageFilepath := repo.ImagePath("qemu", appName) 92 | openstack.PushImage(clientGlance, imageName, imageFilepath, flavor, verbose) 93 | fmt.Printf("Image '%s' [src: %s] successfully uploaded to OpenStack.\n", imageName, packageDir) 94 | 95 | return nil 96 | } 97 | 98 | // OpenStackRun picks best flavor for image and runs instacne(s) on OpenStack. 99 | func OpenStackRun(c *cli.Context) error { 100 | verbose := c.Bool("verbose") 101 | imageName := c.Args().First() 102 | count := c.Int("count") 103 | 104 | if imageName == "" { 105 | fmt.Println("USAGE: capstan stack run [command options] image-name") 106 | os.Exit(1) 107 | } 108 | 109 | name := c.String("name") 110 | if name == "" { 111 | name = fmt.Sprintf("instance-of-%s", imageName) 112 | } 113 | 114 | // Authenticate against OpenStack Identity 115 | credentials, err := openstack.ObtainCredentials(c, verbose) 116 | if err != nil { 117 | return err 118 | } 119 | clientNova, _, err := openstack.GetClients(credentials, verbose) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | // Obtain image metadata. 125 | image, err := openstack.GetImageMeta(clientNova, imageName, verbose) 126 | if err != nil { 127 | return err 128 | } 129 | 130 | // Pick appropriate flavor. 131 | diskMB := 1024 * int64(image.MinDisk) 132 | memoryMB, _ := util.ParseMemSize(c.String("mem")) 133 | flavor, err := openstack.GetOrPickFlavor(clientNova, c.String("flavor"), diskMB, memoryMB, verbose) 134 | if err != nil { 135 | return err 136 | } 137 | fmt.Printf("Picked flavor %s (disk %dGB, memory %dMB)\n", flavor.Name, flavor.Disk, flavor.RAM) 138 | 139 | // Make sure that flavor meets minimum requirements for the image 140 | if image.MinDisk > flavor.Disk || image.MinRAM > flavor.RAM { 141 | fmt.Printf("ABORTED: Flavor '%s' (disk %dGB, memory %dMB) violates image minimum requirements (disk >= %dGB, memory >= %dMB)\n", 142 | flavor.Name, flavor.Disk, flavor.RAM, image.MinDisk, image.MinRAM) 143 | os.Exit(1) 144 | } 145 | 146 | // Launch instances. 147 | fmt.Printf("Launching %d instances from image '%s'...\n", count, imageName) 148 | err = openstack.LaunchInstances(clientNova, name, imageName, flavor.Name, count, verbose) 149 | if err != nil { 150 | return err 151 | } 152 | fmt.Println("Instances launched.") 153 | 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /cmd/stop.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * Modifications copyright (C) 2015 XLAB, Ltd. 4 | * 5 | * This work is open source software, licensed under the terms of the 6 | * BSD license as described in the LICENSE file in the top-level directory. 7 | */ 8 | 9 | package cmd 10 | 11 | import ( 12 | "fmt" 13 | "github.com/cloudius-systems/capstan/hypervisor/gce" 14 | "github.com/cloudius-systems/capstan/hypervisor/qemu" 15 | "github.com/cloudius-systems/capstan/hypervisor/vbox" 16 | "github.com/cloudius-systems/capstan/hypervisor/vmw" 17 | "github.com/cloudius-systems/capstan/util" 18 | ) 19 | 20 | func Stop(name string) error { 21 | instanceName, instancePlatform := util.SearchInstance(name) 22 | if instanceName == "" { 23 | fmt.Printf("Instance: %s not found\n", name) 24 | return nil 25 | } 26 | 27 | var err error 28 | switch instancePlatform { 29 | case "qemu": 30 | err = qemu.StopVM(name) 31 | case "vbox": 32 | err = vbox.StopVM(name) 33 | case "vmw": 34 | err = vmw.StopVM(name) 35 | case "gce": 36 | err = gce.StopVM(name) 37 | } 38 | 39 | if err != nil { 40 | fmt.Printf("Failed to stop instance: %s\n", name) 41 | } 42 | 43 | fmt.Printf("Stopped instance: %s\n", name) 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /cmd/testdata/hashing/dir1/dir3/another-file: -------------------------------------------------------------------------------- 1 | Testing yet another file 2 | -------------------------------------------------------------------------------- /cmd/testdata/hashing/dir1/dir3/file3: -------------------------------------------------------------------------------- 1 | File 3 2 | -------------------------------------------------------------------------------- /cmd/testdata/hashing/dir1/file2: -------------------------------------------------------------------------------- 1 | File 2 in directory 1 2 | -------------------------------------------------------------------------------- /cmd/testdata/hashing/dir2/file-in-dir2: -------------------------------------------------------------------------------- 1 | This is a file residing in directory 2. It is supposedly the biggest file there are. 2 | -------------------------------------------------------------------------------- /cmd/testdata/hashing/file1: -------------------------------------------------------------------------------- 1 | File 1 content 2 | -------------------------------------------------------------------------------- /cmd/testdata/hashing/file4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudius-systems/capstan/6bb7574c54296a47a30984604dcbb41c8cf8ad8a/cmd/testdata/hashing/file4 -------------------------------------------------------------------------------- /cmd/testdata/hashing/meta/package.yaml: -------------------------------------------------------------------------------- 1 | name: pkg.name 2 | title: Hashing test 3 | author: Unknown 4 | -------------------------------------------------------------------------------- /cmd/testdata/hashing/symlink-to-file1: -------------------------------------------------------------------------------- 1 | file1 -------------------------------------------------------------------------------- /cmd/volume.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package cmd 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "path/filepath" 14 | 15 | "github.com/cloudius-systems/capstan/hypervisor" 16 | "github.com/cloudius-systems/capstan/hypervisor/qemu" 17 | ) 18 | 19 | // Volume is an extended version of hypervisor.Volume with volume creation data. 20 | type Volume struct { 21 | hypervisor.Volume 22 | SizeMB int64 23 | Name string 24 | } 25 | 26 | const ( 27 | VOLUMES_DIR string = "volumes" 28 | VOLUME_FORMAT_DEFAULT string = "raw" 29 | ) 30 | 31 | // CreateVolume creates volume with given specifications. 32 | func CreateVolume(packagePath string, volume Volume) error { 33 | if _, err := os.Stat(filepath.Join(packagePath, "meta", "package.yaml")); os.IsNotExist(err) { 34 | return fmt.Errorf("Must be in package root directory") 35 | } 36 | volumesDir := filepath.Join(packagePath, VOLUMES_DIR) 37 | if _, err := os.Stat(volumesDir); os.IsNotExist(err) { 38 | if err := os.Mkdir(volumesDir, 0744); err != nil { 39 | return fmt.Errorf("Could not create volumes dir '%s': %s", volumesDir, err) 40 | } 41 | } 42 | 43 | // Set defaults 44 | if volume.Format == "" { 45 | volume.Format = VOLUME_FORMAT_DEFAULT 46 | } 47 | 48 | // Create actual volume. 49 | volume.Path = filepath.Join(volumesDir, fmt.Sprintf(volume.Name)) 50 | if err := qemu.CreateVolume(volume.Path, volume.Format, volume.SizeMB); err != nil { 51 | return fmt.Errorf("Could not create volume: %s", err) 52 | } 53 | 54 | // Write metadata. 55 | if err := volume.PersistMetadata(); err != nil { 56 | return fmt.Errorf("Could not persist volume metadata: %s", err) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | // DeleteVolume deletes volume and its metadata with given name. 63 | func DeleteVolume(packagePath, name string, verbose bool) error { 64 | path := filepath.Join(packagePath, VOLUMES_DIR, name) 65 | meta := fmt.Sprintf("%s.yaml", path) 66 | 67 | // Remove the volume itself. 68 | if _, err := os.Stat(path); err == nil { 69 | if err := os.Remove(path); err == nil { 70 | if verbose { 71 | fmt.Println("Removed volume", path) 72 | } 73 | } else { 74 | return err 75 | } 76 | } else { 77 | return fmt.Errorf("Could not find volume with name '%s'", name) 78 | } 79 | 80 | // Remove the metadata if it exists. 81 | if _, err := os.Stat(meta); err == nil { 82 | if err := os.Remove(meta); err == nil { 83 | if verbose { 84 | fmt.Println("Removed volume metadata", meta) 85 | } 86 | } else { 87 | return err 88 | } 89 | } 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /cmd/volume_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package cmd 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "path/filepath" 14 | 15 | "github.com/cloudius-systems/capstan/hypervisor" 16 | "github.com/cloudius-systems/capstan/util" 17 | 18 | . "github.com/cloudius-systems/capstan/testing" 19 | . "gopkg.in/check.v1" 20 | ) 21 | 22 | type volumesSuite struct { 23 | capstanBinary string 24 | packageDir string 25 | packageFiles map[string]string 26 | repo *util.Repo 27 | } 28 | 29 | func (s *volumesSuite) SetUpTest(c *C) { 30 | s.packageDir = c.MkDir() 31 | os.Chmod(s.packageDir, 0777) 32 | } 33 | 34 | var _ = Suite(&volumesSuite{}) 35 | 36 | func (s *volumesSuite) TestCreateVolume(c *C) { 37 | m := []struct { 38 | comment string 39 | volume Volume 40 | expectedMeta string 41 | expectedInfo string 42 | }{ 43 | { 44 | "create default format", 45 | Volume{ 46 | SizeMB: 128, 47 | Name: "vol1", 48 | }, 49 | ` 50 | format: raw 51 | `, 52 | ` 53 | file format: raw 54 | virtual size: 128M \(134217728 bytes\) 55 | `, 56 | }, 57 | { 58 | "create qcow2", 59 | Volume{ 60 | Volume: hypervisor.Volume{ 61 | Format: "qcow2", 62 | }, 63 | SizeMB: 128, 64 | Name: "vol1", 65 | }, 66 | ` 67 | format: qcow2 68 | `, 69 | ` 70 | file format: qcow2 71 | virtual size: 128M \(134217728 bytes\) 72 | `, 73 | }, 74 | { 75 | "create raw", 76 | Volume{ 77 | Volume: hypervisor.Volume{ 78 | Format: "raw", 79 | }, 80 | SizeMB: 128, 81 | Name: "vol1", 82 | }, 83 | ` 84 | format: raw 85 | `, 86 | ` 87 | file format: raw 88 | virtual size: 128M \(134217728 bytes\) 89 | `, 90 | }, 91 | } 92 | for i, args := range m { 93 | c.Logf("CASE #%d: %s", i, args.comment) 94 | 95 | // Prepare. 96 | volumesDir := filepath.Join(s.packageDir, "volumes") 97 | metaFile := filepath.Join(volumesDir, fmt.Sprintf("%s.yaml", args.volume.Name)) 98 | volumeFile := filepath.Join(volumesDir, args.volume.Name) 99 | ClearDirectory(volumesDir) 100 | PrepareFiles(s.packageDir, map[string]string{"/meta/package.yaml": PackageYamlText}) 101 | 102 | // This is what we're testing here. 103 | err := CreateVolume(s.packageDir, args.volume) 104 | 105 | // Expectations. 106 | c.Assert(err, IsNil) 107 | c.Check(metaFile, FileMatches, FixIndent(args.expectedMeta)) 108 | c.Check([]string{"qemu-img", "info", volumeFile}, CmdOutputMatches, FixIndent(args.expectedInfo)) 109 | } 110 | } 111 | 112 | func (s *volumesSuite) TestCreateVolumeInvalidNoPackage(c *C) { 113 | // This is what we're testing here. 114 | err := CreateVolume(s.packageDir, Volume{}) 115 | 116 | // Expectations. 117 | c.Check(err, ErrorMatches, "Must be in package root directory") 118 | } 119 | 120 | func (s *volumesSuite) TestCreateVolumeInvalidAlreadyExists(c *C) { 121 | // Prepare. 122 | PrepareFiles(s.packageDir, map[string]string{ 123 | "/meta/package.yaml": PackageYamlText, 124 | "/volumes/vol1": DefaultText, 125 | "/volumes/vol1.yaml": DefaultText, 126 | }) 127 | 128 | // This is what we're testing here. 129 | err := CreateVolume(s.packageDir, Volume{Name: "vol1", Volume: hypervisor.Volume{Format: "vdi"}}) 130 | 131 | // Expectations. 132 | c.Check(err, ErrorMatches, "Could not create volume: Volume already exists") 133 | } 134 | 135 | func (s *volumesSuite) TestDeleteVolume(c *C) { 136 | m := []struct { 137 | comment string 138 | state map[string]string 139 | name string 140 | expectedVolumes map[string]interface{} 141 | }{ 142 | { 143 | "simple", 144 | map[string]string{ 145 | "/volumes/vol1": DefaultText, 146 | "/volumes/vol1.yaml": DefaultText, 147 | }, 148 | "vol1", 149 | map[string]interface{}{}, 150 | }, 151 | } 152 | for i, args := range m { 153 | c.Logf("CASE #%d: %s", i, args.comment) 154 | 155 | // Prepare. 156 | volumesDir := filepath.Join(s.packageDir, "volumes") 157 | args.state["/meta/package.yaml"] = PackageYamlText 158 | PrepareFiles(s.packageDir, args.state) 159 | 160 | // This is what we're testing here. 161 | err := DeleteVolume(s.packageDir, args.name, false) 162 | 163 | // Expectations. 164 | c.Assert(err, IsNil) 165 | c.Check(volumesDir, DirEquals, args.expectedVolumes) 166 | } 167 | } 168 | 169 | func (s *volumesSuite) TestDeleteVolumeInvalid(c *C) { 170 | m := []struct { 171 | comment string 172 | state map[string]string 173 | name string 174 | err string 175 | }{ 176 | { 177 | "delete when not even volumes directory exists", 178 | map[string]string{}, 179 | "nonexistent", 180 | "Could not find volume with name 'nonexistent'", 181 | }, 182 | { 183 | "delete nonexistent", 184 | map[string]string{ 185 | "/volumes/vol1.qcow2": DefaultText, 186 | "/volumes/vol1.qcow2.yaml": DefaultText, 187 | }, 188 | "nonexistent", 189 | "Could not find volume with name 'nonexistent'", 190 | }, 191 | } 192 | for i, args := range m { 193 | c.Logf("CASE #%d: %s", i, args.comment) 194 | 195 | // Prepare. 196 | args.state["/meta/package.yaml"] = PackageYamlText 197 | PrepareFiles(s.packageDir, args.state) 198 | 199 | // This is what we're testing here. 200 | err := DeleteVolume(s.packageDir, args.name, false) 201 | 202 | // Expectations. 203 | c.Assert(err, ErrorMatches, args.err) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /core/capstanignore.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package core 9 | 10 | import ( 11 | "bufio" 12 | "fmt" 13 | "os" 14 | "regexp" 15 | "strings" 16 | ) 17 | 18 | type Capstanignore interface { 19 | LoadFile(path string) error 20 | AddPattern(pattern string) error 21 | PrintPatterns() 22 | IsIgnored(path string) bool 23 | } 24 | 25 | var CAPSTANIGNORE_ALWAYS []string = []string{ 26 | "/meta/*", "/mpm-pkg", "/.git", "/.capstanignore", "/.gitignore", "/volumes", 27 | } 28 | 29 | // CapstanignoreInit creates a new Capstanignore struct that is 30 | // used when deciding whether a file should be included in unikernel 31 | // or not. You can provide `path` to the .capstanignore file to load 32 | // it or pass empty string "" if you have none. Note that once having 33 | // capstanignore struct you can load as many files as you want (using 34 | // .LoadFile function) or manually add as many patterns as you like 35 | // (using .AddPattern function). 36 | func CapstanignoreInit(path string) (Capstanignore, error) { 37 | c := capstanignore{} 38 | 39 | // Load capstanignore file if path was given. 40 | if path != "" { 41 | if err := c.LoadFile(path); err != nil { 42 | return nil, fmt.Errorf("failed to parse .capstanignore: %s\n", err) 43 | } 44 | } 45 | 46 | // Always ignore some common paths. 47 | for _, pattern := range CAPSTANIGNORE_ALWAYS { 48 | c.AddPattern(pattern) 49 | } 50 | return &c, nil 51 | } 52 | 53 | type capstanignore struct { 54 | patterns []string // list of all ignored patterns 55 | compiledPatterns []*regexp.Regexp // list of compiled patterns 56 | } 57 | 58 | // LoadFile attempts to parse .capstanignore file on given path. 59 | // If success, it remembers all patterns and closes file. 60 | func (c *capstanignore) LoadFile(path string) error { 61 | if file, err := os.Open(path); err == nil { 62 | defer file.Close() 63 | 64 | scanner := bufio.NewScanner(file) 65 | for scanner.Scan() { 66 | line := strings.TrimSpace(scanner.Text()) 67 | if line == "" || strings.HasPrefix(line, "#") { 68 | continue 69 | } 70 | if errPattern := c.AddPattern(line); errPattern != nil { 71 | return errPattern 72 | } 73 | } 74 | } else { 75 | return err 76 | } 77 | return nil 78 | } 79 | 80 | // AddPattern adds a pattern to be ignored. 81 | func (c *capstanignore) AddPattern(pattern string) error { 82 | // Protect user from strange behavior when ignoring whole /meta folder. 83 | // (runscript files don't get created if ignored) 84 | if pattern == "/meta" { 85 | return fmt.Errorf("please remove '/meta' from .capstanignore") 86 | } 87 | 88 | safePattern := transformCapstanignoreToRegex(pattern) 89 | if compiled, err := regexp.Compile(safePattern); err == nil { 90 | c.patterns = append(c.patterns, pattern) 91 | c.compiledPatterns = append(c.compiledPatterns, compiled) 92 | } else { 93 | return err 94 | } 95 | 96 | return nil 97 | } 98 | 99 | // IsIgnored returns true if path given is on ignore list. 100 | // But notice that if a folder is ignored, it is up to caller 101 | // to ignore all files beneath as well. E.g. if pattern `/myfolder` 102 | // is used, then IsIgnored will return false for all subfolders and 103 | // files inside the `/myfolder` directory. 104 | func (c *capstanignore) IsIgnored(path string) bool { 105 | for _, pattern := range c.compiledPatterns { 106 | if pattern.MatchString(path) { 107 | return true 108 | } 109 | } 110 | return false 111 | } 112 | 113 | func (c *capstanignore) PrintPatterns() { 114 | for _, pattern := range c.patterns { 115 | fmt.Println(pattern) 116 | } 117 | } 118 | 119 | // transformCapstanignoreToRegex transforms capstanignore synstax to regex systax. 120 | func transformCapstanignoreToRegex(pattern string) string { 121 | // preprocess 122 | pattern = strings.Replace(pattern, "/**/", "{two-stars}", -1) 123 | if strings.HasSuffix(pattern, "/*") { 124 | pattern = strings.TrimSuffix(pattern, "/*") 125 | pattern = pattern + "{all-beneath}" 126 | } 127 | 128 | // Implicit ^ at the beginning 129 | if !strings.HasPrefix(pattern, "^") { 130 | pattern = "^" + pattern 131 | } 132 | // Star * means only one folder level 133 | pattern = strings.Replace(pattern, "*", "[^/]*", -1) 134 | // Dot . means actual dot 135 | pattern = strings.Replace(pattern, ".", "\\.", -1) 136 | // Two stars between two slashes /**/ mean all folder levels 137 | pattern = strings.Replace(pattern, "{two-stars}", ".*", -1) 138 | // /* at the end means also all subfolders 139 | pattern = strings.Replace(pattern, "{all-beneath}", "/.*", 1) 140 | // Implicit $ at the end 141 | if !strings.HasSuffix(pattern, "$") { 142 | pattern = pattern + "$" 143 | } 144 | 145 | return pattern 146 | } 147 | -------------------------------------------------------------------------------- /core/hashcache.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package core 9 | 10 | import ( 11 | "gopkg.in/yaml.v2" 12 | "io/ioutil" 13 | "os" 14 | ) 15 | 16 | type HashCache map[string]string 17 | 18 | func NewHashCache() HashCache { 19 | return make(map[string]string) 20 | } 21 | 22 | // ParseHashCache looks for a file at given location and tries to 23 | // parse the HashCache config. In case the file does not exist 24 | // or is not a valid HashCache file, it fails with an error. 25 | func ParseHashCache(cachePath string) (HashCache, error) { 26 | var hc HashCache 27 | 28 | // Make sure the cache file exists. 29 | if _, err := os.Stat(cachePath); os.IsNotExist(err) { 30 | return hc, err 31 | } 32 | 33 | // Read the cache file. 34 | d, err := ioutil.ReadFile(cachePath) 35 | if err != nil { 36 | return hc, err 37 | } 38 | 39 | // And parse it. 40 | if err := hc.parse(d); err != nil { 41 | return hc, err 42 | } 43 | 44 | return hc, nil 45 | } 46 | 47 | func (h *HashCache) parse(data []byte) error { 48 | if err := yaml.Unmarshal(data, h); err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (h *HashCache) WriteToFile(path string) error { 56 | data, err := yaml.Marshal(h) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | err = ioutil.WriteFile(path, data, 0644) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /core/image.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package core 9 | 10 | type Image struct { 11 | Name string 12 | Hypervisor string 13 | } 14 | -------------------------------------------------------------------------------- /core/package.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package core 9 | 10 | import ( 11 | "fmt" 12 | "io/ioutil" 13 | "os" 14 | "strings" 15 | "time" 16 | 17 | "gopkg.in/yaml.v2" 18 | ) 19 | 20 | type Package struct { 21 | Name string 22 | Title string 23 | Author string `yaml:"author,omitempty"` 24 | Version string `yaml:"version,omitempty"` 25 | Require []string `yaml:"require,omitempty"` 26 | Binary map[string]string `yaml:"binary,omitempty"` 27 | Created YamlTime `yaml:"created"` 28 | Platform string `yaml:"platform,omitempty"` 29 | } 30 | 31 | func (p *Package) Parse(data []byte) error { 32 | if err := yaml.Unmarshal(data, p); err != nil { 33 | return err 34 | } 35 | 36 | if p.Name == "" { 37 | return fmt.Errorf("'name' must be provided for the package") 38 | } 39 | 40 | if p.Title == "" { 41 | return fmt.Errorf("'title' must be provided for the package") 42 | } 43 | 44 | if p.Author == "" { 45 | return fmt.Errorf("'author' must be provided for the package") 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func ParsePackageManifestAndFallbackToDefault(manifestFile string) (Package, error) { 52 | // Make sure the metadata file exists. 53 | if _, err := os.Stat(manifestFile); os.IsNotExist(err) { 54 | fmt.Printf("Manifest file %s does not exist. Assuming default manifest\n", manifestFile) 55 | return Package{ 56 | Name: "App", 57 | Title: "App", 58 | Author: "Anonymous", 59 | Created: YamlTime{time.Now()}, 60 | }, nil 61 | } else { 62 | return ParsePackageManifest(manifestFile) 63 | } 64 | } 65 | 66 | func ParsePackageManifest(manifestFile string) (Package, error) { 67 | var pkg Package 68 | 69 | // Make sure the metadata file exists. 70 | if _, err := os.Stat(manifestFile); os.IsNotExist(err) { 71 | return pkg, fmt.Errorf("Manifest file %s does not exist", manifestFile) 72 | } 73 | 74 | // Read the package descriptor. 75 | d, err := ioutil.ReadFile(manifestFile) 76 | if err != nil { 77 | return pkg, err 78 | } 79 | 80 | // And parse it. This must succeed in order to be able to proceed. 81 | if err := pkg.Parse(d); err != nil { 82 | return pkg, err 83 | } 84 | 85 | return pkg, nil 86 | } 87 | 88 | func (p *Package) String() string { 89 | res := fmt.Sprintf("%-50s %-50s %-15s %-20s %-15s", p.Name, p.Title, p.Version, p.Created, p.Platform) 90 | return strings.TrimSpace(res) 91 | } 92 | -------------------------------------------------------------------------------- /core/rpm.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package core 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "os/exec" 14 | ) 15 | 16 | type RpmPackage struct { 17 | Name string 18 | Version string 19 | Release string 20 | Arch string 21 | } 22 | 23 | func (p *RpmPackage) Download() error { 24 | if _, err := os.Stat(p.Filename()); os.IsNotExist(err) { 25 | fmt.Printf("Downloading %s...\n", p.Filename()) 26 | cmd := exec.Command("curl", "-O", p.URL()) 27 | _, err = cmd.Output() 28 | if err != nil { 29 | return err 30 | } 31 | 32 | } 33 | return nil 34 | } 35 | 36 | func (p *RpmPackage) URL() string { 37 | baseUrl := "http://kojipkgs.fedoraproject.org/packages/" 38 | return fmt.Sprintf("%s%s/%s/%s/%s/%s", baseUrl, p.Name, p.Version, p.Release, p.Arch, p.Filename()) 39 | } 40 | 41 | func (p *RpmPackage) Filename() string { 42 | return fmt.Sprintf("%s-%s-%s.%s.rpm", p.Name, p.Version, p.Release, p.Arch) 43 | } 44 | -------------------------------------------------------------------------------- /core/template.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package core 9 | 10 | import ( 11 | "fmt" 12 | "gopkg.in/yaml.v2" 13 | "io/ioutil" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | ) 18 | 19 | // A template is a configuration file that describes how to build a VM image. 20 | // It is usually representeed as a `Capstanfile` file on disk. 21 | type Template struct { 22 | Base string 23 | RpmBase *RpmPackage "rpm-base" 24 | Cmdline string 25 | Build string 26 | Files map[string]string 27 | Rootfs string 28 | } 29 | 30 | // IsTemplateFile returns true if filename points to a valid template file; 31 | // otherwise returns false. 32 | func IsTemplateFile(filename string) bool { 33 | _, err := ReadTemplateFile(filename) 34 | return err == nil 35 | } 36 | 37 | // ReadTemplateFile parses a Template from a file. 38 | func ReadTemplateFile(filename string) (*Template, error) { 39 | data, err := ioutil.ReadFile(filename) 40 | if err != nil { 41 | return nil, err 42 | } 43 | c, err := ParseTemplate(data) 44 | if err != nil { 45 | return nil, err 46 | } 47 | if err := c.substituteVars(); err != nil { 48 | return nil, err 49 | } 50 | return c, nil 51 | } 52 | 53 | // ParseTemplate parses a Template from a byte array. 54 | func ParseTemplate(data []byte) (*Template, error) { 55 | t := Template{} 56 | if err := yaml.Unmarshal(data, &t); err != nil { 57 | return nil, err 58 | } 59 | if t.Cmdline == "" { 60 | return nil, fmt.Errorf("\"cmdline\" not found") 61 | } 62 | if t.Rootfs == "" { 63 | t.Rootfs = "ROOTFS" 64 | } else { 65 | if _, err := os.Stat(t.Rootfs); os.IsNotExist(err) { 66 | fmt.Printf("Capstanfile: rootfs: %s does not exist\n", t.Rootfs) 67 | return nil, err 68 | } 69 | } 70 | return &t, nil 71 | } 72 | 73 | func (t *Template) substituteVars() error { 74 | for target, source := range t.Files { 75 | t.Files[target] = strings.Replace(source, "&", filepath.Base(target), -1) 76 | } 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /core/template_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package core 9 | 10 | import ( 11 | "testing" 12 | ) 13 | 14 | var configTests = []struct { 15 | Spec string 16 | Err string 17 | }{ 18 | {"base: osv-base\n", "\"cmdline\" not found"}, 19 | {"base: osv-base\ncmdline: foo.so\n", ""}, 20 | {"base: osv-base\ncmdline: foo.so\nfiles:\n", ""}, 21 | {"base: osv-base\ncmdline: foo.so\nbuild: make\n", ""}, 22 | } 23 | 24 | func TestTemplate(t *testing.T) { 25 | for _, test := range configTests { 26 | _, err := ParseTemplate([]byte(test.Spec)) 27 | var got string 28 | switch err { 29 | case nil: 30 | got = "" 31 | default: 32 | got = err.Error() 33 | } 34 | if want := test.Err; got != want { 35 | t.Errorf("Get(%q) error %#q, want %#q", test.Spec, got, want) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/yaml.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package core 9 | 10 | import ( 11 | "time" 12 | ) 13 | 14 | const FRIENDLY_TIME_F = "2006-01-02 15:04" 15 | 16 | type YamlTime struct { 17 | Time interface{} 18 | } 19 | 20 | // MarshalYAML transforms YamlTime object into a RFC3339 string. 21 | func (t YamlTime) MarshalYAML() (interface{}, error) { 22 | if v, ok := t.Time.(time.Time); ok { 23 | return v.Format(time.RFC3339), nil 24 | } else { 25 | return "N/A", nil 26 | } 27 | } 28 | 29 | // UnmarshalYAML parses string into YamlTime object. Following formats 30 | // are supported: RFC3339, FRIENDLY_TIME_F 31 | // If time string is invalid, a special YamlTime is created that is then 32 | // marked as '?' when printed as string. 33 | func (t *YamlTime) UnmarshalYAML(unmarshaller func(interface{}) error) error { 34 | unmarshaller(&t.Time) 35 | s, _ := t.Time.(string) 36 | 37 | if v, err := time.Parse(time.RFC3339, s); err == nil { 38 | t.Time = v 39 | } else if v, err := time.Parse(FRIENDLY_TIME_F, s); err == nil { 40 | t.Time = v 41 | } else { 42 | t.Time = nil 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func (t YamlTime) String() string { 49 | if v, ok := t.Time.(time.Time); ok { 50 | return v.Format(FRIENDLY_TIME_F) 51 | } else { 52 | return "N/A" 53 | } 54 | } 55 | 56 | func (t YamlTime) GetTime() *time.Time { 57 | if v, ok := t.Time.(time.Time); ok { 58 | return &v 59 | } else { 60 | return nil 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/yaml_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package core 9 | 10 | import ( 11 | "testing" 12 | "time" 13 | 14 | . "gopkg.in/check.v1" 15 | "gopkg.in/yaml.v2" 16 | ) 17 | 18 | // Hook up gocheck into the "go test" runner. 19 | func Test(t *testing.T) { TestingT(t) } 20 | 21 | type yamlSuite struct { 22 | } 23 | 24 | var _ = Suite(&yamlSuite{}) 25 | 26 | type yamlWithTimeFieldStruct struct { 27 | Created YamlTime `yaml:"created"` 28 | } 29 | 30 | func (*yamlSuite) TestYamlTimeFieldMarshall(c *C) { 31 | 32 | m := []struct { 33 | comment string 34 | created string 35 | expected string 36 | }{ 37 | { 38 | "simple", 39 | "2017-09-14T18:08:16+02:00", 40 | "created: \"2017-09-14T18:08:16+02:00\"", 41 | }, 42 | { 43 | "miliseconds should not get marshalled", 44 | "2017-09-14T18:08:16.123456789+02:00", 45 | "created: \"2017-09-14T18:08:16+02:00\"", 46 | }, 47 | { 48 | "empty", 49 | "", 50 | "created: N/A", 51 | }, 52 | } 53 | for i, args := range m { 54 | c.Logf("CASE #%d: %s", i, args.comment) 55 | 56 | // Prepare 57 | yamlTime := YamlTime{} 58 | if t, _ := time.Parse(time.RFC3339, args.created); args.created != "" { 59 | yamlTime.Time = t 60 | } 61 | obj := yamlWithTimeFieldStruct{ 62 | Created: yamlTime, 63 | } 64 | 65 | // This is what we're testing here. 66 | data, err := yaml.Marshal(&obj) 67 | 68 | // Expectations. 69 | c.Assert(err, IsNil) 70 | c.Check(string(data), Equals, args.expected+"\n") 71 | } 72 | } 73 | 74 | func (*yamlSuite) TestYamlTimeFieldUnmarshall(c *C) { 75 | 76 | m := []struct { 77 | comment string 78 | yamltext string 79 | expected string 80 | }{ 81 | { 82 | "RFC3339", 83 | "created: \"2017-09-14T18:08:16+02:00\"", 84 | "2017-09-14 18:08", 85 | }, 86 | { 87 | "RFC3339 with miliseconds", 88 | "created: 2017-09-14T18:08:16.123456789+02:00", 89 | "2017-09-14 18:08", 90 | }, 91 | { 92 | "friendly", 93 | "created: 2017-09-14 18:08", 94 | "2017-09-14 18:08", 95 | }, 96 | { 97 | "empty", 98 | "created: ", 99 | "N/A", 100 | }, 101 | { 102 | "missing", 103 | "", 104 | "N/A", 105 | }, 106 | } 107 | for i, args := range m { 108 | c.Logf("CASE #%d: %s", i, args.comment) 109 | 110 | // Prepare 111 | obj := yamlWithTimeFieldStruct{} 112 | 113 | // This is what we're testing here. 114 | err := yaml.Unmarshal([]byte(args.yamltext), &obj) 115 | 116 | // Expectations. 117 | c.Assert(err, IsNil) 118 | c.Check(obj.Created.String(), Equals, args.expected) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /cpio/cpio.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * Modifications copyright (C) 2015 XLAB, Ltd. 4 | * 5 | * This work is open source software, licensed under the terms of the 6 | * BSD license as described in the LICENSE file in the top-level directory. 7 | */ 8 | 9 | package cpio 10 | 11 | import ( 12 | "fmt" 13 | "net" 14 | ) 15 | 16 | const ( 17 | C_ISREG = 0100000 18 | C_ISLNK = 0120000 19 | C_ISDIR = 0040000 20 | ) 21 | 22 | func WritePadded(c net.Conn, data []byte) { 23 | c.Write(data) 24 | partial := len(data) % 4 25 | if partial != 0 { 26 | padding := make([]byte, 4-partial) 27 | c.Write(padding) 28 | } 29 | } 30 | 31 | func ToWireFormat(filename string, mode uint64, filesize int64) []byte { 32 | hdr := fmt.Sprintf("%s%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%s\u0000", 33 | "070701", // magic 34 | 0, // inode 35 | mode, // mode 36 | 0, // uid 37 | 0, // gid 38 | 0, // nlink 39 | 0, // mtime 40 | filesize, // filesize 41 | 0, // devmajor 42 | 0, // devminor 43 | 0, // rdevmajor 44 | 0, // rdevminor 45 | len(filename)+1, // namesize 46 | 0, // check 47 | filename) 48 | return []byte(hdr) 49 | } 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudius-systems/capstan 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/cheggaaa/pb/v3 v3.0.3 7 | github.com/gophercloud/gophercloud v0.7.0 8 | github.com/kr/pretty v0.1.0 // indirect 9 | github.com/mattn/go-colorable v0.1.4 // indirect 10 | github.com/urfave/cli/v2 v2.0.0 11 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 12 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce 13 | gopkg.in/yaml.v2 v2.2.8 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= 3 | github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= 4 | github.com/cheggaaa/pb/v3 v3.0.3 h1:8WApbyUmgMOz7WIxJVNK0IRDcRfAmTxcEdi0TuxjdP4= 5 | github.com/cheggaaa/pb/v3 v3.0.3/go.mod h1:Pp35CDuiEpHa/ZLGCtBbM6CBwMstv1bJlG884V+73Yc= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 8 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 9 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 10 | github.com/gophercloud/gophercloud v0.7.0 h1:vhmQQEM2SbnGCg2/3EzQnQZ3V7+UCGy9s8exQCprNYg= 11 | github.com/gophercloud/gophercloud v0.7.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss= 12 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 13 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 16 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 17 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 18 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 19 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 20 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 21 | github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= 22 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 23 | github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k= 24 | github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 28 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 29 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 30 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 31 | github.com/urfave/cli/v2 v2.0.0 h1:+HU9SCbu8GnEUFtIBfuUNXN39ofWViIEJIp6SURMpCg= 32 | github.com/urfave/cli/v2 v2.0.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 33 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 34 | golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 35 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 36 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 37 | golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 38 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 39 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 40 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 41 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 42 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU= 45 | golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 46 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 47 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 48 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 49 | golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 50 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 51 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 53 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 54 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 55 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= 56 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= 57 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 58 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 59 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 60 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 61 | -------------------------------------------------------------------------------- /hypervisor/default_darwin.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package hypervisor 9 | 10 | func Default() string { 11 | return "vbox" 12 | } 13 | -------------------------------------------------------------------------------- /hypervisor/default_freebsd.go: -------------------------------------------------------------------------------- 1 | package hypervisor 2 | 3 | func Default() string { 4 | return "qemu" 5 | } 6 | -------------------------------------------------------------------------------- /hypervisor/default_linux.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package hypervisor 9 | 10 | func Default() string { 11 | return "qemu" 12 | } 13 | -------------------------------------------------------------------------------- /hypervisor/default_windows.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package hypervisor 9 | 10 | func Default() string { 11 | return "vbox" 12 | } 13 | -------------------------------------------------------------------------------- /hypervisor/hyperkit/hyperkit.go: -------------------------------------------------------------------------------- 1 | package hyperkit 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "os/user" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | type VMConfig struct { 15 | Name string 16 | Dir string 17 | Image string 18 | VmlinuzPath string 19 | Cmd string 20 | Memory int64 21 | Cpus int 22 | Networking string 23 | Bridge string 24 | InstanceDir string 25 | ConfigFile string 26 | MAC string 27 | } 28 | 29 | const ( 30 | legacyVPNKitSock = "Library/Containers/com.docker.docker/Data/s50" 31 | defaultVPNKitSock = "Library/Containers/com.docker.docker/Data/vpnkit.eth.sock" 32 | ) 33 | 34 | func LaunchVM(c *VMConfig, verbose bool, extra ...string) (*exec.Cmd, error) { 35 | 36 | vmArgs, err := c.vmArguments() 37 | if err != nil { 38 | return nil, err 39 | } 40 | args := append(vmArgs, extra...) 41 | path, err := hyperkitExecutable() 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | if verbose { 47 | fmt.Printf("Invoking HYPERKIT at: %s with arguments:", path) 48 | for _, arg := range args { 49 | if strings.HasPrefix(arg, "-") { 50 | fmt.Printf("\n %s", arg) 51 | } else { 52 | fmt.Printf(" %s", arg) 53 | } 54 | } 55 | fmt.Printf("\n") 56 | } 57 | 58 | cmd := exec.Command(path, args...) 59 | 60 | cmd.Stdin = os.Stdin 61 | if err := cmd.Start(); err != nil { 62 | return nil, err 63 | } 64 | 65 | return cmd, nil 66 | } 67 | 68 | func (c *VMConfig) vmArguments() ([]string, error) { 69 | args := make([]string, 0) 70 | args = append(args, "-A") // Enable ACPI 71 | args = append(args, "-x") // Enable x2APIC 72 | args = append(args, "-c", strconv.Itoa(c.Cpus)) // Number of cpus 73 | args = append(args, "-m", strconv.FormatInt(c.Memory, 10)+"M") // Memory 74 | args = append(args, "-f", fmt.Sprintf("kexec,%s,,%s", c.VmlinuzPath, c.Cmd)) //firmware, kernel and commandline 75 | args = append(args, "-l", "com1,stdio") // ??? 76 | args = append(args, "-s", "0:0,hostbridge") // PCI bus 77 | args = append(args, "-s", "31,lpc") // ??? 78 | 79 | nextSlot := 1 80 | args = append(args, "-s", fmt.Sprintf("%d:0,virtio-blk,%s", nextSlot, c.Image)) // VirtIO block device 81 | nextSlot++ 82 | 83 | switch c.Networking { 84 | case "vpnkit": 85 | vpnSockPath, err := vpnSocketPath("auto") 86 | if err != nil { 87 | return nil, err 88 | } 89 | args = append(args, "-s", fmt.Sprintf("%d:0,virtio-vpnkit,path=%s", nextSlot, vpnSockPath)) 90 | nextSlot++ 91 | vsockDirPath := filepath.Join(c.InstanceDir, "vsockState") 92 | err = os.MkdirAll(vsockDirPath, 0775) 93 | if err != nil { 94 | return args, errors.New(fmt.Sprintf("%s: failed to create vstate dir", vsockDirPath)) 95 | } 96 | //TODO: Potentially add ',guest_forwards=8080' to the string below to handle port forwarding 97 | args = append(args, "-s", fmt.Sprintf("%d,virtio-sock,guest_cid=%d,path=%s", 98 | nextSlot, 3, vsockDirPath)) 99 | nextSlot++ 100 | case "vnet": 101 | args = append(args, "-s", fmt.Sprintf("%d:0,virtio-net", nextSlot)) 102 | nextSlot++ 103 | } 104 | 105 | return args, nil 106 | } 107 | 108 | func vpnSocketPath(vpnkitsock string) (string, error) { 109 | if vpnkitsock == "auto" { 110 | vpnkitsock = filepath.Join(getHome(), defaultVPNKitSock) 111 | if _, err := os.Stat(vpnkitsock); err != nil { 112 | vpnkitsock = filepath.Join(getHome(), legacyVPNKitSock) 113 | } 114 | } 115 | if vpnkitsock == "" { 116 | return "", nil 117 | } 118 | 119 | vpnkitsock = filepath.Clean(vpnkitsock) 120 | _, err := os.Stat(vpnkitsock) 121 | if err != nil { 122 | return "", err 123 | } 124 | return vpnkitsock, nil 125 | } 126 | 127 | func getHome() string { 128 | if usr, err := user.Current(); err == nil { 129 | return usr.HomeDir 130 | } 131 | return os.Getenv("HOME") 132 | } 133 | 134 | var defaultHyperKits = []string{"hyperkit", 135 | "com.docker.hyperkit", 136 | "/usr/local/bin/hyperkit", 137 | "/Applications/Docker.app/Contents/Resources/bin/hyperkit", 138 | "/Applications/Docker.app/Contents/MacOS/com.docker.hyperkit"} 139 | 140 | func hyperkitExecutable() (string, error) { 141 | paths := defaultHyperKits 142 | path := os.Getenv("CAPSTAN_HYPERKIT_PATH") 143 | if len(path) > 0 { 144 | paths = append([]string{path}, paths...) 145 | } 146 | for _, path = range paths { 147 | if _, err := os.Stat(path); err == nil { 148 | return path, nil 149 | } 150 | } 151 | return "", fmt.Errorf("No HYPERKIT installation found. Use the CAPSTAN_HYPERKIT_PATH environment variable to specify its path.") 152 | } 153 | -------------------------------------------------------------------------------- /hypervisor/qemu/qemu_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package qemu 9 | 10 | import ( 11 | "testing" 12 | 13 | . "github.com/cloudius-systems/capstan/testing" 14 | . "gopkg.in/check.v1" 15 | ) 16 | 17 | // Hook up gocheck into the "go test" runner. 18 | func Test(t *testing.T) { TestingT(t) } 19 | 20 | type suite struct{} 21 | 22 | var _ = Suite(&suite{}) 23 | 24 | func (*suite) TestVersionParsing(c *C) { 25 | m := []struct { 26 | in string 27 | out *Version 28 | }{ 29 | {"QEMU emulator version 1.0 (qemu-kvm-1.0), Copyright (c) 2003-2008 Fabrice Bellard", &Version{Major: 1, Minor: 0, Patch: 0}}, 30 | {"QEMU emulator version 1.6.2, Copyright (c) 2003-2008 Fabrice Bellard", &Version{Major: 1, Minor: 6, Patch: 2}}, 31 | {"QEMU PC emulator version 0.12.1 (qemu-kvm-0.12.1.2), Copyright (c) 2003-2008 Fabrice Bellard", &Version{Major: 0, Minor: 12, Patch: 1}}, 32 | } 33 | for i, args := range m { 34 | c.Logf("CASE #%d", i) 35 | 36 | // This is what we're testing here. 37 | version, err := ParseVersion(args.in) 38 | 39 | // Expectations. 40 | c.Check(err, IsNil) 41 | c.Check(version, DeepEquals, args.out) 42 | } 43 | } 44 | 45 | func (s *suite) TestVmArguments(c *C) { 46 | m := []struct { 47 | comment string 48 | config VMConfig 49 | expected []string 50 | }{ 51 | { 52 | "basic", 53 | VMConfig{}, 54 | []string{ 55 | "-vnc", "unix:", 56 | "-m", "0", 57 | "-smp", "0", 58 | "-device", "virtio-blk-pci,id=blk0,bootindex=0,drive=hd0", 59 | "-drive", "file=,if=none,id=hd0,aio=threads,cache=unsafe", 60 | "-device", "virtio-rng-pci", 61 | "-chardev", "stdio,mux=on,id=stdio,signal=off", 62 | "-device", "isa-serial,chardev=stdio", 63 | "-netdev", "user,id=un0,net=192.168.122.0/24,host=192.168.122.1", 64 | "-device", "virtio-net-pci,netdev=un0", 65 | "-chardev", "socket,id=charmonitor,path=,server=on,wait=off", 66 | "-mon", "chardev=charmonitor,id=monitor,mode=control", 67 | }, 68 | }, 69 | // Volumes. 70 | { 71 | "single volume", 72 | VMConfig{ 73 | Volumes: []string{"/path/vol1.img"}, 74 | }, 75 | []string{ 76 | "-drive", "file=/path/vol1.img,if=none,id=hd1,aio=native,cache=none,format=raw", 77 | "-device", "virtio-blk-pci,id=blk1,bootindex=1,drive=hd1", 78 | }, 79 | }, 80 | { 81 | "single volume with metadata", 82 | VMConfig{ 83 | Volumes: []string{"/path/vol1.img:format=qcow2:aio=threads:cache=writethrough"}, 84 | }, 85 | []string{ 86 | "-drive", "file=/path/vol1.img,if=none,id=hd1,aio=threads,cache=writethrough,format=qcow2", 87 | "-device", "virtio-blk-pci,id=blk1,bootindex=1,drive=hd1", 88 | }, 89 | }, 90 | { 91 | "two volumes", 92 | VMConfig{ 93 | Volumes: []string{"/path/vol1.img", "/path/vol2.img"}, 94 | }, 95 | []string{ 96 | "-drive", "file=/path/vol1.img,if=none,id=hd1,aio=native,cache=none,format=raw", 97 | "-device", "virtio-blk-pci,id=blk1,bootindex=1,drive=hd1", 98 | "-drive", "file=/path/vol2.img,if=none,id=hd2,aio=native,cache=none,format=raw", 99 | "-device", "virtio-blk-pci,id=blk2,bootindex=2,drive=hd2", 100 | }, 101 | }, 102 | { 103 | "two volumes, one with metadata", 104 | VMConfig{ 105 | Volumes: []string{"/path/vol1.img:format=qcow2", "/path/vol2.img"}, 106 | }, 107 | []string{ 108 | "-drive", "file=/path/vol1.img,if=none,id=hd1,aio=native,cache=none,format=qcow2", 109 | "-device", "virtio-blk-pci,id=blk1,bootindex=1,drive=hd1", 110 | "-drive", "file=/path/vol2.img,if=none,id=hd2,aio=native,cache=none,format=raw", 111 | "-device", "virtio-blk-pci,id=blk2,bootindex=2,drive=hd2", 112 | }, 113 | }, 114 | // AioType. 115 | { 116 | "aio type native", 117 | VMConfig{ 118 | AioType: "native", 119 | }, 120 | []string{ 121 | "-device", "virtio-blk-pci,id=blk0,bootindex=0,drive=hd0", 122 | "-drive", "file=,if=none,id=hd0,aio=native,cache=unsafe", 123 | }, 124 | }, 125 | } 126 | for i, args := range m { 127 | c.Logf("CASE #%d: %s", i, args.comment) 128 | 129 | // Prepare. 130 | qemuVersion := &Version{Major: 2, Minor: 5, Patch: 0} 131 | config := s.setDefaultAttributes(args.config, c) 132 | 133 | // This is what we're testing here. 134 | qemuArgs, err := config.vmArguments(qemuVersion) 135 | 136 | // Expectations. 137 | c.Assert(err, IsNil) 138 | c.Check(qemuArgs, ContainsArray, args.expected) 139 | } 140 | } 141 | 142 | func (*suite) setDefaultAttributes(conf VMConfig, c *C) VMConfig { 143 | conf.DisableKvm = true 144 | if conf.Networking == "" { 145 | conf.Networking = "nat" 146 | } 147 | if conf.AioType == "" { 148 | conf.AioType = "threads" 149 | } 150 | 151 | return conf 152 | } 153 | -------------------------------------------------------------------------------- /hypervisor/util.go: -------------------------------------------------------------------------------- 1 | package hypervisor 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/yaml.v2" 6 | "io/ioutil" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | type Volume struct { 12 | Path string `yaml:"-"` 13 | Format string `yaml:"format,omitempty"` // raw|qcow2|... 14 | AioType string `yaml:"aio,omitempty"` // native|threads 15 | Cache string `yaml:"cache,omitempty"` // none|unsafe|writethrough... 16 | } 17 | 18 | // ParseVolumes parses --volume strings that are of following format: 19 | // --volume {volumePath}[:{options}] 20 | // Example: --volume /path/to/myvolume.img:format=raw:aio=native 21 | func ParseVolumes(volumeStrings []string) ([]Volume, error) { 22 | res := []Volume{} 23 | if volumeStrings == nil { 24 | return res, nil 25 | } 26 | for _, volumeStr := range volumeStrings { 27 | if v, err := parseVolume(volumeStr); err == nil { 28 | res = append(res, *v) 29 | } else { 30 | return res, err 31 | } 32 | 33 | } 34 | return res, nil 35 | } 36 | 37 | func parseVolume(volumeStr string) (*Volume, error) { 38 | v := &Volume{ 39 | Path: "", 40 | Format: "raw", 41 | AioType: "native", 42 | Cache: "none", 43 | } 44 | 45 | for idx, part := range strings.Split(volumeStr, ":") { 46 | if idx == 0 { // Volume path 47 | if path, err := filepath.Abs(part); err == nil { 48 | v.Path = path 49 | continue 50 | } else { 51 | return nil, err 52 | } 53 | } 54 | 55 | // Volume settings 56 | if !strings.Contains(part, "=") { 57 | return nil, fmt.Errorf("Please use '=' for assignment of volume settings. Example: --volume /vol.img:format=raw") 58 | } 59 | 60 | keyVal := strings.SplitN(part, "=", 2) 61 | keyVal[0] = strings.ToLower(keyVal[0]) 62 | if keyVal[0] == "format" { 63 | v.Format = keyVal[1] 64 | } else if keyVal[0] == "aio" { 65 | v.AioType = keyVal[1] 66 | } else if keyVal[0] == "cache" { 67 | v.Cache = keyVal[1] 68 | } else { 69 | return nil, fmt.Errorf("Unknown volume setting: '%s'", keyVal[0]) 70 | } 71 | 72 | } 73 | return v, nil 74 | } 75 | 76 | func (v *Volume) PersistMetadata() error { 77 | data, err := yaml.Marshal(v) 78 | if err != nil { 79 | return err 80 | } 81 | path := fmt.Sprintf("%s.yaml", v.Path) 82 | return ioutil.WriteFile(path, []byte(data), 0644) 83 | } 84 | -------------------------------------------------------------------------------- /hypervisor/util_test.go: -------------------------------------------------------------------------------- 1 | package hypervisor 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | // Hook up gocheck into the "go test" runner. 10 | func Test(t *testing.T) { TestingT(t) } 11 | 12 | type suite struct{} 13 | 14 | var _ = Suite(&suite{}) 15 | 16 | func (*suite) TestParseVolume(c *C) { 17 | m := []struct { 18 | comment string 19 | volumeString string 20 | expected *Volume 21 | }{ 22 | { 23 | "simplest", 24 | "/path/to/volume.img", 25 | &Volume{ 26 | Path: "/path/to/volume.img", 27 | Format: "raw", 28 | AioType: "native", 29 | Cache: "none", 30 | }, 31 | }, 32 | { 33 | "format qcow2", 34 | "/path/to/volume.img:format=qcow2", 35 | &Volume{ 36 | Path: "/path/to/volume.img", 37 | Format: "qcow2", 38 | AioType: "native", 39 | Cache: "none", 40 | }, 41 | }, 42 | { 43 | "aio threads", 44 | "/path/to/volume.img:aio=threads", 45 | &Volume{ 46 | Path: "/path/to/volume.img", 47 | Format: "raw", 48 | AioType: "threads", 49 | Cache: "none", 50 | }, 51 | }, 52 | { 53 | "cache writethrough", 54 | "/path/to/volume.img:cache=writethrough", 55 | &Volume{ 56 | Path: "/path/to/volume.img", 57 | Format: "raw", 58 | AioType: "native", 59 | Cache: "writethrough", 60 | }, 61 | }, 62 | { 63 | "three at a time", 64 | "/path/to/volume.img:aio=threads:cache=writethrough:format=qcow2", 65 | &Volume{ 66 | Path: "/path/to/volume.img", 67 | Format: "qcow2", 68 | AioType: "threads", 69 | Cache: "writethrough", 70 | }, 71 | }, 72 | { 73 | "uppercase key", 74 | "/path/to/volume.img:FORMAT=qcow2", 75 | &Volume{ 76 | Path: "/path/to/volume.img", 77 | Format: "qcow2", 78 | AioType: "native", 79 | Cache: "none", 80 | }, 81 | }, 82 | } 83 | for i, args := range m { 84 | c.Logf("CASE #%d: %s", i, args.comment) 85 | 86 | // This is what we're testing here. 87 | volume, err := parseVolume(args.volumeString) 88 | 89 | // Expectations. 90 | c.Assert(err, IsNil) 91 | c.Check(volume, DeepEquals, args.expected) 92 | } 93 | } 94 | 95 | func (*suite) TestParseVolumes(c *C) { 96 | m := []struct { 97 | comment string 98 | volumeStrings []string 99 | expected []string 100 | }{ 101 | { 102 | "single volume", 103 | []string{"/volume1.img"}, 104 | []string{"/volume1.img"}, 105 | }, 106 | { 107 | "three volumes, order is preserved", 108 | []string{"/volume1.img", "/volume2.img", "/volume3.img"}, 109 | []string{"/volume1.img", "/volume2.img", "/volume3.img"}, 110 | }, 111 | } 112 | for i, args := range m { 113 | c.Logf("CASE #%d: %s", i, args.comment) 114 | 115 | // This is what we're testing here. 116 | volumes, err := ParseVolumes(args.volumeStrings) 117 | 118 | // Expectations. 119 | c.Assert(err, IsNil) 120 | paths := []string{} 121 | for _, volume := range volumes { 122 | paths = append(paths, volume.Path) 123 | } 124 | c.Check(paths, DeepEquals, args.expected) 125 | } 126 | } 127 | 128 | func (*suite) TestParseVolumeInvalid(c *C) { 129 | m := []struct { 130 | comment string 131 | volumeString string 132 | err string 133 | }{ 134 | { 135 | "only colon", 136 | "/path/to/volume.img:", 137 | "Please use '=' for assignment of volume settings. Example: --volume /vol.img:format=raw", 138 | }, 139 | { 140 | "only key", 141 | "/path/to/volume.img:format", 142 | "Please use '=' for assignment of volume settings. Example: --volume /vol.img:format=raw", 143 | }, 144 | { 145 | "illegal attribute", 146 | "/path/to/volume.img:format=qcow2:illegal=value", 147 | "Unknown volume setting: 'illegal'", 148 | }, 149 | } 150 | for i, args := range m { 151 | c.Logf("CASE #%d: %s", i, args.comment) 152 | 153 | // This is what we're testing here. 154 | _, err := parseVolume(args.volumeString) 155 | 156 | // Expectations. 157 | c.Check(err, ErrorMatches, args.err) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /image/gce/gce.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package gce 9 | 10 | import ( 11 | "encoding/binary" 12 | "os" 13 | "strings" 14 | ) 15 | 16 | const ( 17 | GZ_MAGIC1 = 0x1F 18 | GZ_MAGIC2 = 0x8B 19 | ) 20 | 21 | type Header struct { 22 | MagicNumber1 uint8 23 | MagicNumber2 uint8 24 | } 25 | 26 | func ProbeTarball(f *os.File) bool { 27 | header, err := readHeader(f) 28 | if err != nil { 29 | return false 30 | } 31 | return (header.MagicNumber1 == GZ_MAGIC1) && (header.MagicNumber2 == GZ_MAGIC2) 32 | } 33 | 34 | func readHeader(f *os.File) (*Header, error) { 35 | var header Header 36 | err := binary.Read(f, binary.LittleEndian, &header) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return &header, nil 41 | } 42 | 43 | func ProbeGS(path string) bool { 44 | return strings.HasPrefix(path, "gs://") 45 | } 46 | -------------------------------------------------------------------------------- /image/probe.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package image 9 | 10 | import ( 11 | "github.com/cloudius-systems/capstan/image/gce" 12 | "github.com/cloudius-systems/capstan/image/qcow2" 13 | "github.com/cloudius-systems/capstan/image/vdi" 14 | "github.com/cloudius-systems/capstan/image/vmdk" 15 | "os" 16 | ) 17 | 18 | type ImageFormat int 19 | 20 | const ( 21 | QCOW2 ImageFormat = iota 22 | VDI 23 | VMDK 24 | GCE_TARBALL 25 | GCE_GS 26 | RAW 27 | Unknown 28 | ) 29 | 30 | func Probe(path string) (ImageFormat, error) { 31 | if gce.ProbeGS(path) { 32 | return GCE_GS, nil 33 | } 34 | 35 | f, err := os.Open(path) 36 | if err != nil { 37 | return Unknown, err 38 | } 39 | defer f.Close() 40 | f.Seek(0, os.SEEK_SET) 41 | if qcow2.Probe(f) { 42 | return QCOW2, nil 43 | } 44 | f.Seek(0, os.SEEK_SET) 45 | if vdi.Probe(f) { 46 | return VDI, nil 47 | } 48 | f.Seek(0, os.SEEK_SET) 49 | if vmdk.Probe(f) { 50 | return VMDK, nil 51 | } 52 | f.Seek(0, os.SEEK_SET) 53 | if gce.ProbeTarball(f) { 54 | return GCE_TARBALL, nil 55 | } 56 | // Since Raw image format does not have a header we cannot tell whether the file is actually 57 | // a raw image or just a bunch of bytes. qemu-img info nevertheless returns 'raw' format for 58 | // files that do start with one of the common headers (magic). Raw format may be used for 59 | // loader images, produced directly from OSv build process. 60 | return RAW, nil 61 | } 62 | 63 | func IsCloudImage(path string) bool { 64 | format, _ := Probe(path) 65 | return format == GCE_GS 66 | } 67 | -------------------------------------------------------------------------------- /image/qcow2/qcow2.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package qcow2 9 | 10 | import ( 11 | "encoding/binary" 12 | "os" 13 | ) 14 | 15 | const ( 16 | QCOW2_MAGIC = ('Q' << 24) | ('F' << 16) | ('I' << 8) | 0xfb 17 | ) 18 | 19 | type Header struct { 20 | Magic uint32 21 | Version uint32 22 | BackingFileOffset uint64 23 | BackingFileSize uint32 24 | ClusterBits uint32 25 | Size uint64 26 | CryptMethod uint32 27 | L1Size uint32 28 | L1TableOffset uint64 29 | RefcountTableOffset uint64 30 | RefcountTableClusters uint32 31 | NbSnapshots uint32 32 | SnapshotsOffset uint64 33 | } 34 | 35 | func Probe(f *os.File) bool { 36 | header, err := readHeader(f) 37 | if err != nil { 38 | return false 39 | } 40 | return header.Magic == QCOW2_MAGIC 41 | } 42 | 43 | func readHeader(f *os.File) (*Header, error) { 44 | var header Header 45 | err := binary.Read(f, binary.BigEndian, &header) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return &header, nil 50 | } 51 | -------------------------------------------------------------------------------- /image/vdi/vdi.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package vdi 9 | 10 | import ( 11 | "encoding/binary" 12 | "os" 13 | ) 14 | 15 | const ( 16 | VDI_SIGNATURE = 0xbeda107f 17 | ) 18 | 19 | type Header struct { 20 | Text [0x40]byte 21 | Signature uint32 22 | Version uint32 23 | HeaderSize uint32 24 | ImageType uint32 25 | ImageFlags uint32 26 | Description [256]byte 27 | OffsetBmap uint32 28 | OffsetData uint32 29 | Cylinders uint32 30 | Heads uint32 31 | Sectors uint32 32 | SectorSize uint32 33 | Unused1 uint32 34 | DiskSize uint64 35 | BlockSize uint32 36 | BlockExtra uint32 37 | BlocksInImage uint32 38 | BlocksAllocated uint32 39 | UuidImage [16]byte 40 | UuidLastSnap [16]byte 41 | UuidLink [16]byte 42 | UuidParent [16]byte 43 | Unused2 [7]uint64 44 | } 45 | 46 | func Probe(f *os.File) bool { 47 | header, err := readHeader(f) 48 | if err != nil { 49 | return false 50 | } 51 | return header.Signature == VDI_SIGNATURE 52 | } 53 | 54 | func readHeader(f *os.File) (*Header, error) { 55 | var header Header 56 | err := binary.Read(f, binary.LittleEndian, &header) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return &header, nil 61 | } 62 | -------------------------------------------------------------------------------- /image/vmdk/vmdk.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package vmdk 9 | 10 | import ( 11 | "encoding/binary" 12 | "os" 13 | ) 14 | 15 | const ( 16 | VMDK_MAGIC = 0x564d444b 17 | ) 18 | 19 | type SectorType uint64 20 | type Bool uint8 21 | 22 | type Header struct { 23 | MagicNumber uint32 24 | Version uint32 25 | Flags uint32 26 | Capacity SectorType 27 | GrainSize SectorType 28 | DescriptorOffset SectorType 29 | DescriptorSize SectorType 30 | NumGTEsPerGT uint32 31 | RgdOffset SectorType 32 | GdOffset SectorType 33 | OverHead SectorType 34 | UncleanShutdown Bool 35 | SingleEndLineChar byte 36 | NonEndLineChar byte 37 | DoubleEndLineChar1 byte 38 | DoubleEndLineChar2 byte 39 | CompressAlgorithm uint16 40 | Pad [433]uint8 41 | } 42 | 43 | func Probe(f *os.File) bool { 44 | header, err := readHeader(f) 45 | if err != nil { 46 | return false 47 | } 48 | return header.MagicNumber == VMDK_MAGIC 49 | } 50 | 51 | func readHeader(f *os.File) (*Header, error) { 52 | var header Header 53 | err := binary.Read(f, binary.LittleEndian, &header) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return &header, nil 58 | } 59 | -------------------------------------------------------------------------------- /install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | package=github.com/cloudius-systems/capstan 4 | 5 | # Clone github repo into $GOPATH/src/$package, but don't install yet 6 | go get -d $package 7 | 8 | # Calculate version 9 | cd $GOPATH/src/$package 10 | version=$(scripts/version) 11 | 12 | # Clean 13 | rm $GOPATH/bin/capstan 2> /dev/null 14 | 15 | # Install with VERSION string properly set 16 | go install -ldflags "-X main.VERSION='$version' -w -s" $package 17 | -------------------------------------------------------------------------------- /install.bat: -------------------------------------------------------------------------------- 1 | set package=github.com/cloudius-systems/capstan 2 | 3 | :: Clone github repo into $GOPATH/src/$package, but don't install yet 4 | go get -d %package% 5 | 6 | :: Calculate version 7 | cd /d %GOPATH%/src/%package% 8 | for /f %%i in ('git describe --tags') do set version=%%i 9 | 10 | :: Clean 11 | rm %GOPATH%\bin\capstan 2> nul 12 | 13 | :: Install with VERSION string properly set 14 | go install -ldflags "-X main.VERSION=%version% " %package% 15 | -------------------------------------------------------------------------------- /make-dist: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | package=github.com/cloudius-systems/capstan 4 | git_version=$(scripts/version) 5 | 6 | IFS='-' read -ra version_parts <<< "$git_version" 7 | version="${version_parts[0]}" 8 | 9 | OLDIFS=$IFS 10 | IFS=' '; set -- $(go version) 11 | IFS='.'; set -- $3 12 | IFS=$OLDIFS 13 | if [ $2 -gt 4 ]; then 14 | link_operator="=" 15 | else 16 | link_operator=" " 17 | fi 18 | 19 | echo "Building Linux amd64" 20 | env GOOS=linux GOARCH=amd64 go build -a -ldflags "-X main.VERSION$link_operator'$version' -w -s" -tags netgo -v -o dist/linux_amd64/capstan $package 21 | s3cmd put --acl-public --guess-mime-type ./dist/linux_amd64/capstan s3://mikelangelo-capstan/capstan/$version/linux_amd64/capstan 22 | 23 | echo "Building Darwin amd64" 24 | env GOOS=darwin GOARCH=amd64 go build -a -ldflags "-X main.VERSION$link_operator'$version' -w -s" -tags netgo -v -o dist/darwin_amd64/capstan $package 25 | s3cmd put --acl-public --guess-mime-type ./dist/darwin_amd64/capstan s3://mikelangelo-capstan/capstan/$version/darwin_amd64/capstan 26 | -------------------------------------------------------------------------------- /nat/nat.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package nat 9 | 10 | import ( 11 | "strings" 12 | ) 13 | 14 | type Rule struct { 15 | HostPort string 16 | GuestPort string 17 | } 18 | 19 | func Parse(rules []string) []Rule { 20 | fwds := make([]Rule, 0, 0) 21 | for _, rule := range rules { 22 | ports := strings.Split(rule, ":") 23 | fwds = append(fwds, Rule{HostPort: ports[0], GuestPort: ports[1]}) 24 | } 25 | return fwds 26 | } 27 | -------------------------------------------------------------------------------- /provider/openstack/openstack_auth.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package openstack 9 | 10 | import ( 11 | "fmt" 12 | "github.com/gophercloud/gophercloud" 13 | "github.com/gophercloud/gophercloud/openstack" 14 | "github.com/urfave/cli/v2" 15 | "os" 16 | ) 17 | 18 | // OPENSTACK_CREDENTIALS_FLAGS is a list of argumets that are used for OpenStack authentication. 19 | // Append it to Flags list to enable direct authentication i.e. without environment variables. 20 | var OPENSTACK_CREDENTIALS_FLAGS = []cli.Flag{ 21 | &cli.StringFlag{Name: "OS_AUTH_URL", Usage: "OpenStack auth url (e.g. http://10.0.2.15:5000/v2.0)"}, 22 | &cli.StringFlag{Name: "OS_TENANT_ID", Usage: "OpenStack tenant id (e.g. 3dfe7bf545ff4885a3912a92a4a5f8e0)"}, 23 | &cli.StringFlag{Name: "OS_TENANT_NAME", Usage: "OpenStack tenant name (e.g. admin)"}, 24 | &cli.StringFlag{Name: "OS_PROJECT_NAME", Usage: "OpenStack project name (e.g. admin)"}, 25 | &cli.StringFlag{Name: "OS_USERNAME", Usage: "OpenStack username (e.g. admin)"}, 26 | &cli.StringFlag{Name: "OS_PASSWORD", Usage: "OpenStack password (*TODO*: leave blank to be prompted)"}, 27 | &cli.StringFlag{Name: "OS_REGION_NAME", Usage: "OpenStack username (e.g. RegionOne)"}, 28 | } 29 | 30 | // ObtainCredentials attempts to obtain OpenStack credentials either from command args eihter from env. 31 | // If at least one command argument regarding credentials is non-empty, environment is ignored. 32 | func ObtainCredentials(c *cli.Context, verbose bool) (*gophercloud.AuthOptions, error) { 33 | // Obtain credentials passed as script arguments 34 | credentials, err := AuthOptionsFromArgs(c) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | // Obtain credentials from environment if they were not passed as arguments 40 | if credentials == nil { 41 | if verbose { 42 | fmt.Println("Using OpenStack credentials from environment variables") 43 | } 44 | 45 | // Allocate. 46 | credentials = new(gophercloud.AuthOptions) 47 | 48 | // Retrieve credentials from environment variables. 49 | *credentials, err = openstack.AuthOptionsFromEnv() 50 | if err != nil { 51 | return nil, err 52 | } 53 | } 54 | return credentials, nil 55 | } 56 | 57 | // AuthOptionsFromArgs fetches OpenStack credentials from command-line arguments. 58 | // If not even a single argument is passed, then return nil without error. 59 | func AuthOptionsFromArgs(c *cli.Context) (*gophercloud.AuthOptions, error) { 60 | authURL := c.String("OS_AUTH_URL") 61 | username := c.String("OS_USERNAME") 62 | userID := c.String("OS_USERID") 63 | password := c.String("OS_PASSWORD") 64 | tenantID := c.String("OS_TENANT_ID") 65 | tenantName := c.String("OS_TENANT_NAME") 66 | domainID := c.String("OS_DOMAIN_ID") 67 | domainName := c.String("OS_DOMAIN_NAME") 68 | 69 | // If non of the arguments is set, user does not make use of comand line authentication. 70 | if authURL == "" && 71 | username == "" && 72 | userID == "" && 73 | password == "" && 74 | tenantID == "" && 75 | tenantName == "" && 76 | domainID == "" && 77 | domainName == "" { 78 | return nil, nil 79 | } 80 | 81 | if authURL == "" { 82 | return nil, fmt.Errorf("Argument --OS_AUTH_URL needs to be set.") 83 | } 84 | 85 | if username == "" && userID == "" { 86 | return nil, fmt.Errorf("Argument --OS_USERNAME needs to be set.") 87 | } 88 | 89 | if password == "" { 90 | return nil, fmt.Errorf("Argument --OS_PASSWORD needs to be set.") 91 | } 92 | 93 | if tenantName == "" && tenantID == "" { 94 | return nil, fmt.Errorf("Argument --OS_TENANT_NAME needs to be set.") 95 | } 96 | 97 | ao := gophercloud.AuthOptions{ 98 | IdentityEndpoint: authURL, 99 | UserID: userID, 100 | Username: username, 101 | Password: password, 102 | TenantID: tenantID, 103 | TenantName: tenantName, 104 | DomainID: domainID, 105 | DomainName: domainName, 106 | } 107 | 108 | return &ao, nil 109 | } 110 | 111 | // GetClients authenticates against OpenStack Identity and obtains Nova and Glance client. 112 | // Pass nil credentials to fetch it from environment. 113 | func GetClients(credentials *gophercloud.AuthOptions, verbose bool) (*gophercloud.ServiceClient, *gophercloud.ServiceClient, error) { 114 | // Perform authentication. 115 | provider, err := openstack.AuthenticatedClient(*credentials) 116 | if err != nil { 117 | return nil, nil, err 118 | } 119 | 120 | // Obtain different clients 121 | clientNova, _ := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ 122 | Region: os.Getenv("OS_REGION_NAME"), 123 | }) 124 | clientGlance, _ := openstack.NewImageServiceV2(provider, gophercloud.EndpointOpts{ 125 | Region: os.Getenv("OS_REGION_NAME"), 126 | }) 127 | 128 | return clientNova, clientGlance, nil 129 | } 130 | -------------------------------------------------------------------------------- /provider/openstack/openstack_internal_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package openstack 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" 14 | 15 | . "gopkg.in/check.v1" 16 | ) 17 | 18 | func Test(t *testing.T) { TestingT(t) } 19 | 20 | type TestingOpenstackSuite struct{} 21 | 22 | var _ = Suite(&TestingOpenstackSuite{}) 23 | 24 | func (s *TestingOpenstackSuite) TestPickOptimalFlavor(c *C) { 25 | m := []struct { 26 | comment string 27 | flavors []flavors.Flavor 28 | optimal string 29 | err string 30 | }{ 31 | { 32 | "no matching flavor", 33 | []flavors.Flavor{}, "", "No matching flavors to pick from", 34 | }, 35 | { 36 | "prefer smaller disk for equal memory", 37 | []flavors.Flavor{ 38 | flavors.Flavor{Disk: 30, RAM: 1, ID: "f01"}, 39 | flavors.Flavor{Disk: 10, RAM: 1, ID: "f02"}, 40 | flavors.Flavor{Disk: 20, RAM: 1, ID: "f03"}, 41 | flavors.Flavor{Disk: 40, RAM: 1, ID: "f04"}, 42 | }, "f02", "", 43 | }, 44 | { 45 | "prefer smaller disk regardless memory", 46 | []flavors.Flavor{ 47 | flavors.Flavor{Disk: 50, RAM: 1, ID: "f01"}, 48 | flavors.Flavor{Disk: 1, RAM: 16, ID: "f02"}, 49 | flavors.Flavor{Disk: 40, RAM: 1, ID: "f03"}, 50 | flavors.Flavor{Disk: 20, RAM: 1, ID: "f04"}, 51 | }, "f02", "", 52 | }, 53 | { 54 | "prefer smaller memory for equal hdd", 55 | []flavors.Flavor{ 56 | flavors.Flavor{Disk: 10, RAM: 3, ID: "f01"}, 57 | flavors.Flavor{Disk: 10, RAM: 2, ID: "f02"}, 58 | flavors.Flavor{Disk: 10, RAM: 1, ID: "f03"}, 59 | flavors.Flavor{Disk: 10, RAM: 8, ID: "f04"}, 60 | }, "f03", "", 61 | }, 62 | } 63 | for _, args := range m { 64 | c.Log(args.comment) 65 | 66 | flavor, err := selectBestFlavor(args.flavors, false) 67 | if args.err != "" { 68 | c.Check(err, ErrorMatches, args.err) 69 | } else { 70 | c.Check(flavor, NotNil) 71 | c.Check(err, IsNil) 72 | c.Check(flavor.ID, Equals, args.optimal) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /runtime/java.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package runtime 9 | 10 | import ( 11 | "fmt" 12 | "strings" 13 | 14 | "github.com/cloudius-systems/capstan/util" 15 | ) 16 | 17 | // javaPackages specifies what packages are fully compatible with this runtime. 18 | // For the time being, these are: 19 | // openjdk8-zulu-compact1 20 | // openjdk8-zulu-compact3-with-java-beans 21 | // openjdk7 22 | var javaPackages = []string{"^openjdk.*"} 23 | 24 | type javaRuntime struct { 25 | CommonRuntime `yaml:"-,inline"` 26 | Xms string `yaml:"xms"` 27 | Xmx string `yaml:"xmx"` 28 | Classpath []string `yaml:"classpath"` 29 | JvmArgs []string `yaml:"jvm_args"` 30 | Main string `yaml:"main"` 31 | Args []string `yaml:"args"` 32 | } 33 | 34 | // 35 | // Interface implementation 36 | // 37 | 38 | func (conf javaRuntime) GetRuntimeName() string { 39 | return string(Java) 40 | } 41 | func (conf javaRuntime) GetRuntimeDescription() string { 42 | return "Run Java application" 43 | } 44 | func (conf javaRuntime) GetDependencies() []string { 45 | return []string{"openjdk8-zulu-compact1"} 46 | } 47 | func (conf javaRuntime) Validate() error { 48 | // Only validate java-specific environment variables when base is openjdk-like. 49 | if isCompatibleBase(conf.Base, javaPackages) { 50 | if conf.Main == "" { 51 | return fmt.Errorf("'main' must be provided") 52 | } 53 | } else { 54 | if conf.Xms != "" || conf.Xmx != "" || len(conf.Classpath) > 0 || 55 | len(conf.JvmArgs) > 0 || conf.Main != "" || len(conf.Args) > 0 { 56 | return fmt.Errorf("incompatible arguments specified [xms,xmx,classpath,jvm_args,main,args] for custom 'base'") 57 | } 58 | } 59 | 60 | return conf.CommonRuntime.Validate() 61 | } 62 | func (conf javaRuntime) GetBootCmd(cmdConfs map[string]*CmdConfig, env map[string]string) (string, error) { 63 | if conf.Base == "" { // Allow user to use e.g. "openjdk7:java" package instead default one. 64 | conf.Base = "openjdk8-zulu-compact1:java" 65 | } 66 | 67 | // Only set java-specific environment variables when base is openjdk-like. 68 | if isCompatibleBase(conf.Base, javaPackages) { 69 | if len(conf.Classpath) == 0 { 70 | conf.Classpath = append(conf.Classpath, "/") 71 | } 72 | if strings.HasSuffix(conf.Main, ".jar") && !util.StringInSlice("-jar", conf.JvmArgs) { 73 | conf.JvmArgs = append(conf.JvmArgs, "-jar") 74 | } 75 | conf.setDefaultEnv(map[string]string{ 76 | "XMS": conf.Xms, 77 | "XMX": conf.Xmx, 78 | "CLASSPATH": strings.Join(conf.Classpath, ":"), 79 | "JVM_ARGS": conf.concatJvmArgs(), 80 | "MAIN": conf.Main, 81 | "ARGS": strings.Join(conf.Args, " "), 82 | }) 83 | } 84 | 85 | return conf.CommonRuntime.BuildBootCmd("", cmdConfs, env) 86 | } 87 | func (conf javaRuntime) GetYamlTemplate() string { 88 | return ` 89 | # REQUIRED 90 | # Fully classified name of the main class. 91 | # Example value: main.Hello 92 | main: 93 | 94 | # OPTIONAL 95 | # A list of paths where classes and other resources can be found. 96 | # By default, the unikernel root "/" is added to the classpath. 97 | # Example value: classpath: 98 | # - / 99 | # - /src 100 | classpath: 101 | - 102 | 103 | # OPTIONAL 104 | # Initial and maximum JVM memory size. 105 | # Example value: xms: 512m 106 | xms: 107 | xmx: 108 | 109 | # OPTIONAL 110 | # A list of JVM args. 111 | # Example value: jvm_args: 112 | # - -Djava.net.preferIPv4Stack=true 113 | # - -Dhadoop.log.dir=/hdfs/logs 114 | jvm_args: 115 | - 116 | 117 | # OPTIONAL 118 | # A list of command line args used by the application. 119 | # Example value: args: 120 | # - argument1 121 | # - argument2 122 | args: 123 | - 124 | ` + conf.CommonRuntime.GetYamlTemplate() 125 | } 126 | 127 | // 128 | // Utility 129 | // 130 | 131 | func (conf javaRuntime) concatJvmArgs() string { 132 | if len(conf.JvmArgs) > 0 { 133 | return strings.Join(conf.JvmArgs, " ") 134 | } else { 135 | // This is a workaround since runscript is currently unable to 136 | // handle empty environment variable as a parameter. So we set 137 | // dummy value unless user provided some actual value. 138 | return "-Dx=y" 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /runtime/native.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package runtime 9 | 10 | import "fmt" 11 | 12 | type nativeRuntime struct { 13 | CommonRuntime `yaml:"-,inline"` 14 | BootCmd string `yaml:"bootcmd"` 15 | } 16 | 17 | // 18 | // Interface implementation 19 | // 20 | 21 | func (conf nativeRuntime) GetRuntimeName() string { 22 | return string(Native) 23 | } 24 | func (conf nativeRuntime) GetRuntimeDescription() string { 25 | return "Run arbitrary command inside OSv" 26 | } 27 | func (conf nativeRuntime) GetDependencies() []string { 28 | return []string{} 29 | } 30 | func (conf nativeRuntime) Validate() error { 31 | if conf.Base == "" { 32 | if conf.BootCmd == "" { 33 | return fmt.Errorf("'bootcmd' must be provided") 34 | } 35 | } 36 | 37 | return conf.CommonRuntime.Validate() 38 | } 39 | func (conf nativeRuntime) GetBootCmd(cmdConfs map[string]*CmdConfig, env map[string]string) (string, error) { 40 | cmd := conf.BootCmd 41 | return conf.CommonRuntime.BuildBootCmd(cmd, cmdConfs, env) 42 | } 43 | func (conf nativeRuntime) GetYamlTemplate() string { 44 | return ` 45 | # REQUIRED 46 | # Command to be executed in OSv. 47 | # Note that package root will correspond to filesystem root (/) in OSv image. 48 | # Example value: /usr/bin/simpleFoam.so -help 49 | bootcmd: 50 | ` 51 | } 52 | -------------------------------------------------------------------------------- /runtime/node.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package runtime 9 | 10 | import ( 11 | "fmt" 12 | "strings" 13 | ) 14 | 15 | type nodeJsRuntime struct { 16 | CommonRuntime `yaml:"-,inline"` 17 | NodeArgs []string `yaml:"node_args"` 18 | Main string `yaml:"main"` 19 | Args []string `yaml:"args"` 20 | IsShell bool `yaml:"shell"` // run interactive node interpreter 21 | } 22 | 23 | // 24 | // Interface implementation 25 | // 26 | 27 | func (conf nodeJsRuntime) GetRuntimeName() string { 28 | return string(NodeJS) 29 | } 30 | func (conf nodeJsRuntime) GetRuntimeDescription() string { 31 | return "Run JavaScript NodeJS 4.4.5 application" 32 | } 33 | func (conf nodeJsRuntime) GetDependencies() []string { 34 | return []string{"node-4.4.5"} 35 | } 36 | func (conf nodeJsRuntime) Validate() error { 37 | if conf.Base != "" { 38 | if conf.IsShell || len(conf.NodeArgs) > 0 || conf.Main != "" || len(conf.Args) > 0 { 39 | return fmt.Errorf("incompatible arguments specified [shell,node_args,main,args] for custom 'base'") 40 | } 41 | } else if conf.IsShell { 42 | if conf.Main != "" || len(conf.Args) > 0 { 43 | return fmt.Errorf("incompatible arguments specified [main,args] for shell=true") 44 | } 45 | if conf.Env["MAIN"] != "" || conf.Env["ARGS"] != "" { 46 | return fmt.Errorf("incompatible 'env' keys specified [MAIN,ARGS] for shell=true") 47 | } 48 | } else { 49 | if conf.Main == "" { 50 | return fmt.Errorf("'main' must be provided") 51 | } 52 | } 53 | 54 | return conf.CommonRuntime.Validate() 55 | } 56 | func (conf nodeJsRuntime) GetBootCmd(cmdConfs map[string]*CmdConfig, env map[string]string) (string, error) { 57 | conf.Base = "node-4.4.5:node" 58 | conf.setDefaultEnv(map[string]string{ 59 | "NODE_ARGS": conf.concatNodeArgs(), 60 | }) 61 | 62 | if conf.IsShell { 63 | conf.Env["MAIN"] = "" 64 | conf.Env["ARGS"] = "" 65 | } else { 66 | conf.setDefaultEnv(map[string]string{ 67 | "MAIN": conf.Main, 68 | "ARGS": strings.Join(conf.Args, " "), 69 | }) 70 | } 71 | return conf.CommonRuntime.BuildBootCmd("", cmdConfs, env) 72 | } 73 | func (conf nodeJsRuntime) GetYamlTemplate() string { 74 | return ` 75 | # REQUIRED 76 | # Filepath of the NodeJS entrypoint (where server is defined). 77 | # Note that package root will correspond to filesystem root (/) in OSv image. 78 | # Example value: /server.js 79 | main: 80 | 81 | # OPTIONAL 82 | # A list of Node.js args. 83 | # Example value: node_args: 84 | # - --require module1 85 | node_args: 86 | - 87 | 88 | # OPTIONAL 89 | # A list of command line args used by the application. 90 | # Example value: args: 91 | # - argument1 92 | # - argument2 93 | args: 94 | - 95 | 96 | # OPTIONAL 97 | # Set to true to only run node shell. Note that "main" and "args" will then be ignored. 98 | shell: false 99 | ` + conf.CommonRuntime.GetYamlTemplate() 100 | } 101 | 102 | // 103 | // Utility 104 | // 105 | 106 | func (conf nodeJsRuntime) concatNodeArgs() string { 107 | if len(conf.NodeArgs) > 0 { 108 | return strings.Join(conf.NodeArgs, " ") 109 | } else { 110 | // This is a workaround since runscript is currently unable to 111 | // handle empty environment variable as a parameter. So we set 112 | // dummy value unless user provided some actual value. 113 | return "--" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /runtime/python.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package runtime 9 | 10 | import ( 11 | "fmt" 12 | "strings" 13 | ) 14 | 15 | type pythonRuntime struct { 16 | CommonRuntime `yaml:"-,inline"` 17 | PythonArgs []string `yaml:"python_args"` 18 | Main string `yaml:"main"` 19 | Args []string `yaml:"args"` 20 | IsShell bool `yaml:"shell"` // run interactive python interpreter 21 | } 22 | 23 | // 24 | // Interface implementation 25 | // 26 | 27 | func (conf pythonRuntime) GetRuntimeName() string { 28 | return string(Python) 29 | } 30 | func (conf pythonRuntime) GetRuntimeDescription() string { 31 | return "Run Python 2.7 application" 32 | } 33 | func (conf pythonRuntime) GetDependencies() []string { 34 | return []string{"python-2.7"} 35 | } 36 | func (conf pythonRuntime) Validate() error { 37 | if conf.Base != "" { 38 | if conf.IsShell || len(conf.PythonArgs) > 0 || conf.Main != "" || len(conf.Args) > 0 { 39 | return fmt.Errorf("incompatible arguments specified [shell,python_args,main,args] for custom 'base'") 40 | } 41 | } else if conf.IsShell { 42 | if conf.Main != "" || len(conf.Args) > 0 { 43 | return fmt.Errorf("incompatible arguments specified [main,args] for shell=true") 44 | } 45 | if conf.Env["MAIN"] != "" || conf.Env["ARGS"] != "" { 46 | return fmt.Errorf("incompatible 'env' keys specified [MAIN,ARGS] for shell=true") 47 | } 48 | } else { 49 | if conf.Main == "" { 50 | return fmt.Errorf("'main' must be provided") 51 | } 52 | } 53 | 54 | return conf.CommonRuntime.Validate() 55 | } 56 | func (conf pythonRuntime) GetBootCmd(cmdConfs map[string]*CmdConfig, env map[string]string) (string, error) { 57 | conf.Base = "python-2.7:python" 58 | conf.setDefaultEnv(map[string]string{ 59 | "PYTHON_ARGS": conf.concatPythonArgs(), 60 | }) 61 | 62 | if conf.IsShell { 63 | conf.Env["MAIN"] = "-" 64 | conf.Env["ARGS"] = "" 65 | } else { 66 | conf.setDefaultEnv(map[string]string{ 67 | "MAIN": conf.Main, 68 | "ARGS": strings.Join(conf.Args, " "), 69 | }) 70 | } 71 | return conf.CommonRuntime.BuildBootCmd("", cmdConfs, env) 72 | } 73 | func (conf pythonRuntime) GetYamlTemplate() string { 74 | return ` 75 | # REQUIRED 76 | # Filepath of the Python script. 77 | # Note that package root will correspond to filesystem root (/) in OSv image. 78 | # Example value: /hello-world.py 79 | main: 80 | 81 | # OPTIONAL 82 | # A list of Python args. 83 | # Example value: node_args: 84 | # - -O 85 | python_args: 86 | - 87 | 88 | # OPTIONAL 89 | # A list of command line args used by the application. 90 | # Example value: args: 91 | # - argument1 92 | # - argument2 93 | args: 94 | - 95 | ` + conf.CommonRuntime.GetYamlTemplate() 96 | } 97 | 98 | // 99 | // Utility 100 | // 101 | 102 | func (conf pythonRuntime) concatPythonArgs() string { 103 | if len(conf.PythonArgs) > 0 { 104 | return strings.Join(conf.PythonArgs, " ") 105 | } else { 106 | // This is a workaround since runscript is currently unable to 107 | // handle empty environment variable as a parameter. So we set 108 | // dummy value unless user provided some actual value. 109 | return "-O" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /scripts/capstan.bash: -------------------------------------------------------------------------------- 1 | #!bash 2 | # 3 | # bash completion file for core capstan commands 4 | # 5 | 6 | __capstan_instances() 7 | { 8 | local instances="$(capstan instances | awk '{print $1}' | sed 1d)" 9 | COMPREPLY=( $( compgen -W "$instances" -- "$cur" ) ) 10 | } 11 | 12 | __capstan_images() 13 | { 14 | local images="$(capstan images | awk '{print $1}' | sed 1d)" 15 | COMPREPLY=( $( compgen -W "$images" -- "$cur" ) ) 16 | } 17 | 18 | _capstan_delete() 19 | { 20 | __capstan_instances 21 | } 22 | 23 | _capstan_stop() 24 | { 25 | __capstan_instances 26 | } 27 | 28 | _capstan_rmi() 29 | { 30 | __capstan_images 31 | } 32 | 33 | _capstan_capstan() 34 | { 35 | case "$cur" in 36 | -*) 37 | COMPREPLY=( $( compgen -W "-h -v --help --version" -- "$cur" ) ) 38 | ;; 39 | *) 40 | COMPREPLY=( $( compgen -W "$commands help" -- "$cur" ) ) 41 | ;; 42 | esac 43 | } 44 | 45 | _capstan() 46 | { 47 | local commands=" 48 | info 49 | import 50 | pull 51 | rmi 52 | run 53 | build 54 | images 55 | search 56 | instances 57 | stop 58 | delete 59 | " 60 | 61 | COMPREPLY=() 62 | local cur words cword 63 | _get_comp_words_by_ref -n : cur words cword 64 | 65 | local command='capstan' 66 | local counter=1 67 | while [ $counter -lt $cword ]; do 68 | case "${words[$counter]}" in 69 | -*) 70 | (( counter++ )) 71 | ;; 72 | *) 73 | command="${words[$counter]}" 74 | break 75 | ;; 76 | esac 77 | done 78 | 79 | local completions_func=_capstan_${command} 80 | declare -F $completions_func >/dev/null && $completions_func 81 | 82 | return 0 83 | } 84 | 85 | complete -F _capstan capstan 86 | -------------------------------------------------------------------------------- /scripts/download: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | case "$OSTYPE" in 6 | darwin*) NAME="darwin_capstan" ;; 7 | linux*) NAME="capstan" ;; 8 | freebsd*) NAME="capstan";; 9 | *) echo "Your operating system ('$OSTYPE') is not supported by Capstan. Exiting." && exit 1 ;; 10 | esac 11 | 12 | case "$HOSTTYPE" in 13 | x86_64*) ARCH="amd64" ;; 14 | amd64*) ARCH="amd64" ;; 15 | *) echo "OSv only supports 64-bit x86. Exiting." && exit 1 ;; 16 | esac 17 | 18 | URL="https://github.com/cloudius-systems/capstan/releases/latest/download/${NAME}" 19 | DIR="$HOME/bin" 20 | 21 | mkdir -p $DIR 22 | 23 | echo "Downloading Capstan binary: $URL" 24 | 25 | curl -# -L $URL > $DIR/capstan || exit 26 | 27 | chmod u+x $DIR/capstan 28 | -------------------------------------------------------------------------------- /scripts/generate_cli_doc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # This script is meant to be run manually when you introduce a change in capstan API. 4 | # It stores capstan --help texts into markdown file so that user is able to read them without 5 | # having Capstan even installed. Please note that if you think some command/argument is not 6 | # explained well enough, then you should UPDATE CAPSTAN to print better description (not this 7 | # script). 8 | # 9 | # Before running the script: 10 | # * make sure that latest capstan executable is in folder $GOPATH/bin 11 | # * 12 | # 13 | # Run like this: 14 | # $ cd $HOME/go/src/github.com/cloudius-systems/capstan 15 | # $ ./scripts/generate_cli_doc.py 16 | # The result is Documentation/generated/CLI.md file (overrides the old file completely). 17 | # 18 | 19 | 20 | import subprocess 21 | import os 22 | from datetime import datetime 23 | 24 | 25 | class Command: 26 | def __init__(self, cmd): 27 | self.cmd = cmd 28 | 29 | 30 | class Group: 31 | def __init__(self, title, description, commands): 32 | self.title = title 33 | self.description = description 34 | self.commands = commands 35 | 36 | 37 | RESULT_FILE = os.path.join('.', 'Documentation', 'generated', 'CLI.md') 38 | CAPSTAN_DIR = os.path.join(os.environ['GOPATH'], 'bin') 39 | GROUPS = [ 40 | Group('Working with application packages', 41 | 'These commands are useful when packaging my application into Capstan package.', [ 42 | Command('capstan package init'), 43 | Command('capstan package collect'), 44 | Command('capstan package compose'), 45 | Command('capstan package describe'), 46 | ]), 47 | Group('Integrating existing packages', 48 | 'These commands are useful when we intend to use package from remote repository.', [ 49 | Command('capstan package list'), 50 | Command('capstan package search'), 51 | Command('capstan package pull'), 52 | ]), 53 | Group('Working with runtimes', 54 | 'Runtime-related commands.', [ 55 | Command('capstan runtime list'), 56 | Command('capstan runtime preview'), 57 | Command('capstan runtime init'), 58 | ]), 59 | Group('Executing unikernel', 60 | 'Commands used to run composed package.', [ 61 | Command('capstan run'), 62 | ]), 63 | Group('Executing unikernel on OpenStack', 64 | 'Commands used to compose unikernel, upload it to OpenStack Glance and run it with OpenStack Nova.', [ 65 | Command('capstan stack push'), 66 | Command('capstan stack run'), 67 | ]), 68 | Group('Configuring Capstan tool', 69 | 'Commands used to configure Capstan.', [ 70 | Command('capstan config print'), 71 | ]), 72 | Group('Contextualizing unikernel remotely', 73 | 'Commands used to contextualize remote unikernel (i.e. on OpenStack Glance).', [ 74 | Command('capstan package compose-remote'), 75 | ]), 76 | ] 77 | 78 | def get_command_description(command, flags=['--help']): 79 | stdout, stderr = subprocess.Popen( 80 | command.cmd.split() + flags, 81 | stdout=subprocess.PIPE, 82 | stderr=subprocess.PIPE, 83 | env={'PATH': CAPSTAN_DIR} 84 | ).communicate() 85 | 86 | if stderr: 87 | print ('STDERR: %s' % stderr) 88 | return None 89 | return stdout 90 | 91 | 92 | def generate_cli_documentation(): 93 | res = ''' 94 | 100 | 101 | # CLI Reference 102 | Here we describe Capstan CLI in detail. Please note that this very same information can be obtained 103 | by adding --help flag to any of the listed commands. 104 | ''' 105 | general_descr = get_command_description(Command('capstan')) 106 | res += ''' 107 | ## General: 108 | ``` 109 | %s 110 | ``` 111 | ''' % (general_descr) 112 | 113 | for group in GROUPS: 114 | res += ''' 115 | ## %s 116 | %s 117 | ''' % (group.title, group.description) 118 | 119 | for command in group.commands: 120 | descr = get_command_description(command) 121 | if descr is not None: 122 | res += ''' 123 | ### %s 124 | ``` 125 | %s 126 | ``` 127 | ''' % (command.cmd, descr) 128 | 129 | # Append some visible metadata 130 | res += '\n---\n' # vertical space 131 | res += '' 132 | res += ' Documentation compiled on: %s\n' % datetime.utcnow().strftime('%Y/%m/%d %H:%M') 133 | res += '
\n' 134 | res += ' %s' % get_command_description(Command('capstan'), flags=['--version']) 135 | res += '
' 136 | 137 | with open(RESULT_FILE, 'w') as f: 138 | f.write(res) 139 | 140 | 141 | if __name__ == '__main__': 142 | # verify that Capstan executable exists 143 | try: 144 | get_command_description(Command('capstan')) 145 | except: 146 | print('Capstan executable could not be found inside %s' % CAPSTAN_DIR) 147 | exit() 148 | 149 | print('Generating CLI documentation into %s' % RESULT_FILE) 150 | generate_cli_documentation() 151 | print('CLI documentation dumped into: %s' % RESULT_FILE) 152 | -------------------------------------------------------------------------------- /scripts/version: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git describe --tags 4 | -------------------------------------------------------------------------------- /test/core/capstanignore_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package core_test 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/cloudius-systems/capstan/core" 14 | . "gopkg.in/check.v1" 15 | ) 16 | 17 | func Test(t *testing.T) { TestingT(t) } 18 | 19 | type testingCapstanignoreSuite struct{} 20 | 21 | var _ = Suite(&testingCapstanignoreSuite{}) 22 | 23 | func (s *testingCapstanignoreSuite) TestIsIgnored(c *C) { 24 | m := []struct { 25 | comment string 26 | pattern string 27 | path string 28 | shouldIgnore bool 29 | }{ 30 | { 31 | "fully specified file in root #1", 32 | "/myfile.txt", "/myfile.txt", true, 33 | }, 34 | { 35 | "fully specified file in root #2", 36 | "/myfile.txt", "/myfolder/myfile.txt", false, 37 | }, 38 | 39 | { 40 | "fully specified file not in root #1", 41 | "/myfolder/myfile.txt", "/myfile.txt", false, 42 | }, 43 | { 44 | "fully specified file not in root #2", 45 | "/myfolder/myfile.txt", "/myfolder/myfile.txt", true, 46 | }, 47 | { 48 | "file by extension in root #1", 49 | "/*.txt", "/myfile.txt", true, 50 | }, 51 | { 52 | "file by extension in root #2", 53 | "/*.txt", "/myfolder/myfile.txt", false, 54 | }, 55 | { 56 | "file by extension not in root #1", 57 | "/myfolder/*.txt", "/myfile.txt", false, 58 | }, 59 | { 60 | "file by extension not in root #2", 61 | "/myfolder/*.txt", "/myfolder/myfile.txt", true, 62 | }, 63 | { 64 | "file by extension not in root #3", 65 | "/myfolder/*.txt", "/myfolder/subfolder/myfile.txt", false, 66 | }, 67 | { 68 | "fully specified file in any subfolder #1", 69 | "/**/file.txt", "/myfile.txt", true, 70 | }, 71 | { 72 | "fully specified file in any subfolder #2", 73 | "/**/file.txt", "/myfolder/myfile.txt", true, 74 | }, 75 | { 76 | "fully specified file in any subfolder #3", 77 | "/**/file.txt", "/myfolder/subfolder/myfile.txt", true, 78 | }, 79 | { 80 | "whole folder one level #1", 81 | "/myfolder/*", "/myfolder/myfile.txt", true, 82 | }, 83 | { 84 | "whole folder one level #2", 85 | "/myfolder/*", "/myfolder", false, 86 | }, 87 | { 88 | "whole folder one level #3", 89 | "/myfolder/*", "/myfolder/subfolder/myfile.txt", true, 90 | }, 91 | { 92 | "whole folder one level #4", 93 | "/myfolder/*", "/myfolder/subfolder", true, 94 | }, 95 | { 96 | "whole folder two levels #1", 97 | "/myfolder/subfolder/*", "/myfolder/subfolder", false, 98 | }, 99 | { 100 | "whole folder two levels #2", 101 | "/myfolder/subfolder/*", "/myfolder", false, 102 | }, 103 | { 104 | "whole folder two whole levels #1", 105 | "/myfolder/*/*", "/myfolder", false, 106 | }, 107 | { 108 | "whole folder two whole levels #2", 109 | "/myfolder/*/*", "/myfolder/subfolder", false, 110 | }, 111 | { 112 | "whole folder two whole levels #3", 113 | "/myfolder/*/*", "/myfolder/subfolder/myfile.txt", true, 114 | }, 115 | { 116 | "any text file in project #1", 117 | "/**/*.txt", "/myfile.txt", true, 118 | }, 119 | { 120 | "any text file in project #2", 121 | "/**/*.txt", "/myfolder/myfile.txt", true, 122 | }, 123 | { 124 | "any text file in project #3", 125 | "/**/*.txt", "/myfolder/subfolder/myfile.txt", true, 126 | }, 127 | { 128 | "additional test #1", 129 | "/subfolder/*", "/myfolder/subfolder/myfile.txt", false, 130 | }, 131 | { 132 | "additional test #2", 133 | "/myfolder/*.txt", "/myfolder/myfileXtxt", false, 134 | }, 135 | { 136 | "additional test #3", 137 | "/myfolder", "/myfolder2", false, 138 | }, 139 | { 140 | "additional test #4", 141 | "/myfolder/*", "/myfolder2", false, 142 | }, 143 | { 144 | "always ignore /meta/*", 145 | "", "/meta/package.yaml", true, 146 | }, 147 | { 148 | "always ignore /mpm-pkg", 149 | "", "/mpm-pkg", true, 150 | }, 151 | { 152 | "always ignore /.git", 153 | "", "/.git", true, 154 | }, 155 | { 156 | "always ignore /.capstanignore", 157 | "", "/.capstanignore", true, 158 | }, 159 | { 160 | "always ignore /.gitignore", 161 | "", "/.gitignore", true, 162 | }, 163 | { 164 | "always ignore /volumes", 165 | "", "/volumes", true, 166 | }, 167 | } 168 | for i, args := range m { 169 | c.Logf("CASE #%d: %s", i, args.comment) 170 | 171 | // Setup 172 | capstanignore, _ := core.CapstanignoreInit("") 173 | capstanignore.AddPattern(args.pattern) 174 | 175 | // This is what we're testing here. 176 | ignoreYesNo := capstanignore.IsIgnored(args.path) 177 | 178 | // Expectations. 179 | c.Check(ignoreYesNo, Equals, args.shouldIgnore) 180 | } 181 | } 182 | 183 | func (s *testingCapstanignoreSuite) TestIsIgnoredMeta(c *C) { 184 | // Setup 185 | capstanignore, _ := core.CapstanignoreInit("") 186 | 187 | // This is what we're testing here. 188 | err := capstanignore.AddPattern("/meta") 189 | 190 | // Expectations. 191 | c.Check(err, ErrorMatches, "please remove '/meta' from .capstanignore") 192 | } 193 | -------------------------------------------------------------------------------- /test/runtime/runtime_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package runtime_test 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/cloudius-systems/capstan/runtime" 14 | . "github.com/cloudius-systems/capstan/testing" 15 | . "gopkg.in/check.v1" 16 | ) 17 | 18 | func Test(t *testing.T) { TestingT(t) } 19 | 20 | type testingRuntimeSuite struct{} 21 | 22 | var _ = Suite(&testingRuntimeSuite{}) 23 | 24 | func (s *testingRuntimeSuite) TestPrependEnvsPrefix(c *C) { 25 | m := []struct { 26 | comment string 27 | cmd string 28 | env map[string]string 29 | soft bool 30 | expectedCmd string 31 | expectedEnv []string 32 | err string 33 | }{ 34 | { 35 | "no variable in environment", 36 | "/node server.js", map[string]string{}, false, 37 | "/node server.js", []string{}, 38 | "", 39 | }, 40 | { 41 | "single variable in environment", 42 | "/node server.js", map[string]string{"PORT": "8000"}, false, 43 | "/node server.js", []string{"--env=PORT=8000"}, 44 | "", 45 | }, 46 | { 47 | "two variables in environment", 48 | "/node server.js", map[string]string{"PORT": "8000", "ENDPOINT": "foo.com"}, false, 49 | "/node server.js", []string{"--env=PORT=8000", "--env=ENDPOINT=foo.com"}, 50 | "", 51 | }, 52 | { 53 | "no variable in environment - soft", 54 | "/node server.js", map[string]string{}, true, 55 | "/node server.js", []string{}, 56 | "", 57 | }, 58 | { 59 | "single variable in environment - soft", 60 | "/node server.js", map[string]string{"PORT": "8000"}, true, 61 | "/node server.js", []string{"--env=PORT?=8000"}, 62 | "", 63 | }, 64 | { 65 | "two variables in environment - soft", 66 | "/node server.js", map[string]string{"PORT": "8000", "ENDPOINT": "foo.com"}, true, 67 | "/node server.js", []string{"--env=PORT?=8000", "--env=ENDPOINT?=foo.com"}, 68 | "", 69 | }, 70 | } 71 | for i, args := range m { 72 | c.Logf("CASE #%d: %s", i, args.comment) 73 | 74 | // This is what we're testing here. 75 | res, err := runtime.PrependEnvsPrefix(args.cmd, args.env, args.soft) 76 | 77 | // Expectations. 78 | if args.err != "" { 79 | c.Check(err, ErrorMatches, args.err) 80 | } else { 81 | c.Check(res, BootCmdEquals, args.expectedCmd, args.expectedEnv) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/util/parser_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package util_test 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/cloudius-systems/capstan/util" 14 | . "gopkg.in/check.v1" 15 | ) 16 | 17 | func Test(t *testing.T) { TestingT(t) } 18 | 19 | type testingParserSuite struct{} 20 | 21 | var _ = Suite(&testingParserSuite{}) 22 | 23 | func (s *testingParserSuite) TestParseEnvironmentList(c *C) { 24 | m := []struct { 25 | comment string 26 | envList []string 27 | expectedRes map[string]string 28 | err string 29 | }{ 30 | { 31 | "no parameters", 32 | []string{}, 33 | map[string]string{}, 34 | "", 35 | }, 36 | { 37 | "single parameter", 38 | []string{"PORT=8000"}, 39 | map[string]string{"PORT": "8000"}, 40 | "", 41 | }, 42 | { 43 | "two parameters", 44 | []string{"PORT=8000", "ENDPOINT=foo.com"}, 45 | map[string]string{"PORT": "8000", "ENDPOINT": "foo.com"}, 46 | "", 47 | }, 48 | { 49 | "invalid char (space) #1", 50 | []string{"NAME=my name"}, 51 | map[string]string{}, 52 | "failed to parse --env argument .*", 53 | }, 54 | { 55 | "invalid char (space) #2", 56 | []string{"MY NAME=name"}, 57 | map[string]string{}, 58 | "failed to parse --env argument .*", 59 | }, 60 | { 61 | "invalid char (space) #3", 62 | []string{"MY NAME=my name"}, 63 | map[string]string{}, 64 | "failed to parse --env argument .*", 65 | }, 66 | { 67 | "value with equals sign #1", 68 | []string{"NAME=my=name"}, 69 | map[string]string{"NAME": "my=name"}, 70 | "", 71 | }, 72 | { 73 | "value with equals sign #2", 74 | []string{"NAME==name"}, 75 | map[string]string{"NAME": "=name"}, 76 | "", 77 | }, 78 | { 79 | "one parameter ok, other not", 80 | []string{"PORT=8000", "ENDPOINT=i am invalid"}, 81 | map[string]string{}, 82 | "failed to parse --env argument .*", 83 | }, 84 | { 85 | "same parameter two times", 86 | []string{"PORT=8000", "PORT=9999"}, 87 | map[string]string{"PORT": "9999"}, 88 | "", 89 | }, 90 | { 91 | "empty value", 92 | []string{"PORT="}, 93 | map[string]string{"PORT": ""}, 94 | "", 95 | }, 96 | { 97 | "not key=value format", 98 | []string{"PORT"}, 99 | map[string]string{}, 100 | "failed to parse --env argument .*", 101 | }, 102 | } 103 | for i, args := range m { 104 | c.Logf("CASE #%d: %s", i, args.comment) 105 | 106 | // This is what we're testing here. 107 | res, err := util.ParseEnvironmentList(args.envList) 108 | 109 | // Expectations. 110 | if args.err != "" { 111 | c.Check(err, ErrorMatches, args.err) 112 | } else { 113 | c.Check(res, DeepEquals, args.expectedRes) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /testing/checkers_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | // Hook up gocheck into the "go test" runner. 10 | func Test(t *testing.T) { TestingT(t) } 11 | 12 | type suite struct{} 13 | 14 | var _ = Suite(&suite{}) 15 | 16 | func (*suite) TestContainsArrayStringCheck(c *C) { 17 | m := []struct { 18 | comment string 19 | obtained []string 20 | expected []string 21 | err string 22 | }{ 23 | { 24 | "simplest", 25 | []string{"a", "b", "c"}, 26 | []string{"b", "c"}, 27 | "", 28 | }, 29 | { 30 | "full", 31 | []string{"a", "b", "c"}, 32 | []string{"a", "b", "c"}, 33 | "", 34 | }, 35 | { 36 | "mismatch #1", 37 | []string{"a", "b", "c"}, 38 | []string{"x"}, 39 | "Obtained array does not contain expected subarray", 40 | }, 41 | { 42 | "mismatch #2", 43 | []string{"a", "b", "c"}, 44 | []string{"a", "c"}, 45 | "Obtained array does not contain expected subarray", 46 | }, 47 | { 48 | "too short obtained", 49 | []string{"a"}, 50 | []string{"x", "y", "z"}, 51 | "Obtained array is shorter than wanted", 52 | }, 53 | { 54 | "empty expected", 55 | []string{"a", "b", "c"}, 56 | []string{}, 57 | "Expected array must not be empty", 58 | }, 59 | } 60 | for i, args := range m { 61 | c.Logf("CASE #%d: %s", i, args.comment) 62 | 63 | // Prepare. 64 | obtained := []interface{}{args.obtained, args.expected} 65 | names := []string{} 66 | 67 | // This is what we're testing here. 68 | isOk, errStr := ContainsArray.Check(obtained, names) 69 | 70 | // Expectations. 71 | c.Assert(errStr, Equals, args.err) 72 | c.Assert(isOk, Equals, args.err == "") 73 | } 74 | } 75 | 76 | func (*suite) TestContainsArrayIntCheck(c *C) { 77 | m := []struct { 78 | comment string 79 | obtained []int 80 | expected []int 81 | err string 82 | }{ 83 | { 84 | "simplest", 85 | []int{1, 2, 3}, 86 | []int{2, 3}, 87 | "", 88 | }, 89 | } 90 | for i, args := range m { 91 | c.Logf("CASE #%d: %s", i, args.comment) 92 | 93 | // Prepare. 94 | obtained := []interface{}{args.obtained, args.expected} 95 | names := []string{} 96 | 97 | // This is what we're testing here. 98 | isOk, errStr := ContainsArray.Check(obtained, names) 99 | 100 | // Expectations. 101 | c.Assert(errStr, Equals, args.err) 102 | c.Assert(isOk, Equals, args.err == "") 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /testing/common.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package testing 9 | 10 | import ( 11 | "fmt" 12 | "io/ioutil" 13 | "net/http" 14 | "net/http/httptest" 15 | "strings" 16 | ) 17 | 18 | const TIMESTAMP_REGEX string = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?([+-]\d{2}:\d{2})?[Zz]?` 19 | 20 | // FixIndent moves the inline yaml content to the very left. 21 | // This way we are able to write inline yaml content that is 22 | // nicely aligned with other code. 23 | func FixIndent(s string) string { 24 | s = strings.TrimSpace(s) + "\n" 25 | return strings.Replace(s, "\t", "", -1) 26 | } 27 | 28 | func MockGitHubApiServer() *httptest.Server { 29 | return httptest.NewServer( 30 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | path := "../util/testdata/github" + r.RequestURI + "/payload" 32 | fmt.Printf("httptest: Mocking: %s with %s \n", r.RequestURI, path) 33 | if payload, err := ioutil.ReadFile(path); err == nil { 34 | if strings.HasPrefix(r.RequestURI, "/repos") { 35 | mockServerURL := "http://" + r.Host 36 | payloadStr := strings.Replace(string(payload), "https://github.com", mockServerURL, -1) 37 | w.Write([]byte(payloadStr)) 38 | } else { 39 | w.Write(payload) 40 | } 41 | } else { 42 | http.Error(w, "not found", http.StatusNotFound) 43 | } 44 | })) 45 | } 46 | -------------------------------------------------------------------------------- /testing/prepare_files.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package testing 9 | 10 | import ( 11 | "io/ioutil" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | ) 16 | 17 | // 18 | // Common File Content Templates 19 | // 20 | 21 | const PackageYamlText string = ` 22 | name: package-name 23 | title: PackageTitle 24 | author: package-author 25 | ` 26 | 27 | const DefaultText string = ` 28 | Some text. 29 | ` 30 | 31 | // PrepareFiles realizes map[filepath]content into given directory. 32 | // E.g. directory = /tmp/sample, files = {"/file01.txt" => "Foo Bar"} will result in 33 | // 34 | // /tmp/sample/ 35 | // |- file01.txt 36 | // 37 | // where file01.txt will contain text content of "Foo Bar". 38 | func PrepareFiles(directory string, files map[string]string) error { 39 | for path, content := range files { 40 | path = strings.TrimPrefix(path, "/") 41 | 42 | // Create directory structure. 43 | if err := os.MkdirAll(filepath.Join(directory, filepath.Dir(path)), 0700); err != nil { 44 | return err 45 | } 46 | // Create file with content. 47 | if err := ioutil.WriteFile(filepath.Join(directory, path), []byte(content), 0700); err != nil { 48 | return err 49 | } 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func ClearDirectory(directory string) { 56 | os.RemoveAll(directory) 57 | os.Mkdir(directory, 0777) 58 | } 59 | -------------------------------------------------------------------------------- /util/github_releases_repository_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/cloudius-systems/capstan/testing" 5 | . "gopkg.in/check.v1" 6 | "net/http/httptest" 7 | ) 8 | 9 | func (s *suite) SetUpSuite(c *C) { 10 | s.server = testing.MockGitHubApiServer() 11 | } 12 | 13 | func (s *suite) TearDownSuite(c *C) { 14 | s.server.Close() 15 | } 16 | 17 | type suite struct { 18 | repo *Repo 19 | server *httptest.Server 20 | } 21 | 22 | func (s *suite) SetUpTest(c *C) { 23 | s.repo = NewRepo(DefaultRepositoryUrl) 24 | s.repo.Path = c.MkDir() 25 | s.repo.UseS3 = false 26 | s.repo.GithubURL = s.server.URL 27 | } 28 | 29 | var _ = Suite(&suite{}) 30 | 31 | func (s *suite) TestGithubPackageInfoRemote(c *C) { 32 | s.repo.ReleaseTag = "v0.53.0" 33 | packageName := "osv.httpserver-api" 34 | appPackage := s.repo.PackageInfoRemote(packageName) 35 | c.Assert(appPackage, NotNil) 36 | c.Check(appPackage.Name, Equals, packageName) 37 | } 38 | 39 | func (s *suite) TestGithubDownloadLoaderImage(c *C) { 40 | s.repo.ReleaseTag = "v0.57.0" 41 | loaderName, err := s.repo.DownloadLoaderImage("osv-loader", "qemu") 42 | c.Assert(err, IsNil) 43 | c.Check(loaderName, Equals, "osv-loader") 44 | } 45 | 46 | func (s *suite) TestGithubListPackagesRemote(c *C) { 47 | s.repo.ReleaseTag = "any" 48 | err := s.repo.ListPackagesRemote("") 49 | c.Assert(err, IsNil) 50 | } 51 | 52 | func (s *suite) TestGithubDownloadPackageRemote(c *C) { 53 | s.repo.ReleaseTag = "v0.53.0" 54 | err := s.repo.DownloadPackageRemote("osv.httpserver-api") 55 | c.Assert(err, IsNil) 56 | } 57 | -------------------------------------------------------------------------------- /util/github_repository.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package util 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | "os" 14 | "os/exec" 15 | "path/filepath" 16 | ) 17 | 18 | func (r *Repo) PullImage(image string) error { 19 | workTree := r.workTree(image) 20 | _, err := os.Stat(workTree) 21 | if os.IsNotExist(err) { 22 | return r.cloneImage(image) 23 | } 24 | if err != nil { 25 | return err 26 | } 27 | return r.updateImage(image) 28 | } 29 | 30 | func (r *Repo) cloneImage(image string) error { 31 | fmt.Printf("Pulling %s...\n", image) 32 | workTree := r.workTree(image) 33 | gitUrl := fmt.Sprintf("https://github.com/%s", image) 34 | cmd := exec.Command("git", "clone", "--depth", "1", gitUrl, workTree) 35 | out, err := cmd.CombinedOutput() 36 | if err != nil { 37 | fmt.Println(string(out)) 38 | return errors.New(fmt.Sprintf("%s: unable to pull remote image", image)) 39 | } 40 | return nil 41 | } 42 | 43 | func (r *Repo) updateImage(image string) error { 44 | fmt.Printf("Updating %s...\n", image) 45 | workTree := r.workTree(image) 46 | gitDir := r.gitDir(image) 47 | cmd := exec.Command("git", "--git-dir", gitDir, "--work-tree", workTree, "remote", "update") 48 | out, err := cmd.CombinedOutput() 49 | if err != nil { 50 | fmt.Println(string(out)) 51 | return err 52 | } 53 | cmd = exec.Command("git", "--git-dir", gitDir, "--work-tree", workTree, "merge", "origin/master") 54 | out, err = cmd.CombinedOutput() 55 | if err != nil { 56 | fmt.Println(string(out)) 57 | return err 58 | } 59 | return nil 60 | } 61 | 62 | func (r *Repo) gitDir(image string) string { 63 | return filepath.Join(r.workTree(image), ".git") 64 | } 65 | 66 | func (r *Repo) workTree(image string) string { 67 | return filepath.Join(r.RepoPath(), image) 68 | } 69 | -------------------------------------------------------------------------------- /util/image_util.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * Modifications copyright (C) 2015 XLAB, Ltd. 4 | * 5 | * This work is open source software, licensed under the terms of the 6 | * BSD license as described in the LICENSE file in the top-level directory. 7 | */ 8 | 9 | package util 10 | 11 | import ( 12 | "fmt" 13 | "os" 14 | "os/exec" 15 | ) 16 | 17 | func ConvertImageToQCOW2(imagePath string) error { 18 | if _, err := os.Stat(imagePath); os.IsNotExist(err) { 19 | return err 20 | } 21 | 22 | cmd := exec.Command("qemu-img", "convert", "-f", "raw", "-O", "qcow2", imagePath, imagePath+".qcow2") 23 | _, err := cmd.Output() 24 | if err != nil { 25 | fmt.Printf("Converting image %s to QCOW2 format failed in qemu-img\n", imagePath) 26 | return err 27 | } 28 | 29 | // Cleanup: remove raw image file first. 30 | if err := os.Remove(imagePath); err != nil { 31 | return err 32 | } 33 | 34 | // Finally, rename the QCOW2 file into the target appName. 35 | if err := os.Rename(imagePath+".qcow2", imagePath); err != nil { 36 | return err 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func ResizeImage(imagePath string, targetSize uint64) error { 43 | if _, err := os.Stat(imagePath); os.IsNotExist(err) { 44 | return err 45 | } 46 | 47 | cmd := exec.Command("qemu-img", "resize", imagePath, fmt.Sprintf("%db", targetSize)) 48 | _, err := cmd.Output() 49 | if err != nil { 50 | fmt.Printf("Resizing %s to new size %db failed in qemu-img\n", imagePath, targetSize) 51 | return err 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func SetPartition(image string, partition int, start uint64, size uint64) error { 58 | partition = 0x1be + ((partition - 1) * 0x10) 59 | 60 | cyl, head, sec := chs(start / 512) 61 | cyl_end, head_end, sec_end := chs((start + size) / 512) 62 | 63 | nbdFile, err := NewNbdFile(image) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | if err := nbdFile.WriteByte(uint64(partition+1), byte(head)); err != nil { 69 | return err 70 | } 71 | if err := nbdFile.WriteByte(uint64(partition+5), byte(head_end)); err != nil { 72 | return err 73 | } 74 | if err := nbdFile.WriteShort(uint64(partition+2), uint16(cyl<<6|sec)); err != nil { 75 | return err 76 | } 77 | if err := nbdFile.WriteShort(uint64(partition+6), uint16(cyl_end<<6|sec_end)); err != nil { 78 | return err 79 | } 80 | 81 | systemId := 0x83 82 | if err := nbdFile.WriteByte(uint64(partition+4), byte(systemId)); err != nil { 83 | return err 84 | } 85 | 86 | if err := nbdFile.WriteInt(uint64(partition+8), uint32(start/512)); err != nil { 87 | return err 88 | } 89 | if err := nbdFile.WriteInt(uint64(partition+12), uint32(size/512)); err != nil { 90 | return err 91 | } 92 | 93 | if err := nbdFile.Close(); err != nil { 94 | return err 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func SetCmdLine(imagePath string, cmdLine string) error { 101 | nbdFile, err := NewNbdFile(imagePath) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | padding := 512 - (len(cmdLine) % 512) 107 | 108 | data := append([]byte(cmdLine), make([]byte, padding)...) 109 | 110 | if err := nbdFile.Write(512, data); err != nil { 111 | return err 112 | } 113 | 114 | if err := nbdFile.Close(); err != nil { 115 | return err 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func chs(x uint64) (uint64, uint64, uint64) { 122 | sectorsPerTrack := uint64(63) 123 | heads := uint64(255) 124 | 125 | c := (x / sectorsPerTrack) / heads 126 | h := (x / sectorsPerTrack) % heads 127 | s := (x % sectorsPerTrack) + 1 128 | 129 | if c > 1023 { 130 | c = 1023 131 | h = 254 132 | s = 63 133 | } 134 | 135 | return c, h, s 136 | } 137 | -------------------------------------------------------------------------------- /util/mac.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/rand" 5 | "net" 6 | ) 7 | 8 | // Generate a MAC address. 9 | func GenerateMAC() (net.HardwareAddr, error) { 10 | buf := make([]byte, 6) 11 | _, err := rand.Read(buf) 12 | if err != nil { 13 | return nil, err 14 | } 15 | buf[0] &= 0xFE // Unicast 16 | buf[0] |= 0x02 // Locally administered 17 | return net.HardwareAddr(buf), nil 18 | } 19 | -------------------------------------------------------------------------------- /util/package_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package util_test 9 | 10 | import ( 11 | . "gopkg.in/check.v1" 12 | "testing" 13 | ) 14 | 15 | // Hook up gocheck into the "go test" runner. 16 | func Test(t *testing.T) { TestingT(t) } 17 | -------------------------------------------------------------------------------- /util/parser.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | func ParseMemSize(memory string) (int64, error) { 11 | r, _ := regexp.Compile("([0-9]+)(m|mb|M|MB|g|gb|G|GB)$") 12 | match := r.FindStringSubmatch(memory) 13 | if len(match) != 3 { 14 | return -1, fmt.Errorf("%s: unrecognized memory size", memory) 15 | } 16 | size, _ := strconv.ParseInt(match[1], 10, 64) 17 | unit := match[2] 18 | switch unit { 19 | case "g", "gb", "G", "GB": 20 | size *= 1024 21 | } 22 | if size == 0 { 23 | return -1, fmt.Errorf("%s: memory size must be larger than zero", memory) 24 | } 25 | return size, nil 26 | } 27 | 28 | func ParseEnvironmentList(envList []string) (map[string]string, error) { 29 | res := make(map[string]string) 30 | 31 | for _, part := range envList { 32 | if keyValue := strings.SplitN(part, "=", 2); len(keyValue) < 2 { 33 | return nil, fmt.Errorf("failed to parse --env argument '%s': missing =", part) 34 | } else if strings.Contains(keyValue[0], " ") { 35 | return nil, fmt.Errorf("failed to parse --env argument '%s': key must not contain spaces", part) 36 | } else if strings.Contains(keyValue[1], " ") { 37 | return nil, fmt.Errorf("failed to parse --env argument '%s': value must not contain spaces", part) 38 | } else { 39 | res[keyValue[0]] = keyValue[1] 40 | } 41 | } 42 | return res, nil 43 | } 44 | -------------------------------------------------------------------------------- /util/parser_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package util 9 | 10 | import ( 11 | "testing" 12 | ) 13 | 14 | func TestParseMemSize(t *testing.T) { 15 | m := map[string]int64{ 16 | "64MB": 64, 17 | "64M": 64, 18 | "64mb": 64, 19 | "64m": 64, 20 | "1GB": 1024, 21 | "1G": 1024, 22 | "1gb": 1024, 23 | "1g": 1024, 24 | } 25 | for key, value := range m { 26 | size, err := ParseMemSize(key) 27 | if err != nil { 28 | t.Errorf("capstan: %v", err) 29 | } 30 | if e, g := value, size; e != g { 31 | t.Errorf("capstan: want %q, got %q", e, g) 32 | } 33 | } 34 | } 35 | 36 | func TestParseMemSizeErrors(t *testing.T) { 37 | m := map[string]string{ 38 | "0M": "0M: memory size must be larger than zero", 39 | "0G": "0G: memory size must be larger than zero", 40 | "64foo": "64foo: unrecognized memory size", 41 | "64": "64: unrecognized memory size", 42 | "foo": "foo: unrecognized memory size", 43 | } 44 | for key, value := range m { 45 | size, err := ParseMemSize(key) 46 | if err == nil { 47 | t.Errorf("capstan: expected error, got %d", size) 48 | } 49 | if err != nil && err.Error() != value { 50 | t.Errorf("capstan: %v", err) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /util/remote_repository.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "github.com/cheggaaa/pb/v3" 7 | "github.com/cloudius-systems/capstan/core" 8 | "gopkg.in/yaml.v2" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | type FileInfo struct { 19 | Namespace string 20 | Name string 21 | Description string 22 | Version string 23 | Created core.YamlTime `yaml:"created"` 24 | Platform string 25 | } 26 | 27 | type FilesInfo struct { 28 | images []FileInfo 29 | } 30 | 31 | type RemotePackageDownloadInfo struct { 32 | manifestURL string 33 | fileURL string 34 | } 35 | 36 | func FileInfoHeader() string { 37 | res := fmt.Sprintf("%-50s %-50s %-15s %-20s %-15s", "Name", "Description", "Version", "Created", "Platform") 38 | return strings.TrimSpace(res) 39 | } 40 | 41 | func (f *FileInfo) String() string { 42 | // Trim "/" prefix if there is one (happens when namespace is empty) 43 | name := strings.TrimLeft(f.Namespace+"/"+f.Name, "/") 44 | platform := f.Platform 45 | if platform == "" { 46 | platform = "N/A" 47 | } 48 | res := fmt.Sprintf("%-50s %-50s %-15s %-20s %-15s", name, f.Description, f.Version, f.Created, platform) 49 | return strings.TrimSpace(res) 50 | } 51 | 52 | func ParseIndexYaml(path, ns, name string) (*FileInfo, error) { 53 | data, err := ioutil.ReadFile(filepath.Join(path, ns, name, "index.yaml")) 54 | if err != nil { 55 | return nil, err 56 | } 57 | f := FileInfo{} 58 | err = yaml.Unmarshal(data, &f) 59 | if err != nil { 60 | return nil, err 61 | } 62 | f.Namespace = ns 63 | f.Name = name 64 | return &f, nil 65 | } 66 | 67 | func RemoteFileInfo(repo_url string, path string) *FileInfo { 68 | var netClient = &http.Client{ 69 | Timeout: time.Second * 10, 70 | } 71 | resp, err := netClient.Get(repo_url + path) 72 | if err != nil { 73 | return nil 74 | } 75 | 76 | parts := strings.Split(path, "/") 77 | defer resp.Body.Close() 78 | f := FileInfo{} 79 | data, err := ioutil.ReadAll(resp.Body) 80 | if err != nil { 81 | return nil 82 | } 83 | if resp.StatusCode != 200 { 84 | fmt.Printf("The request %s returned non-200 [%d] response: %s.", 85 | repo_url+path, resp.StatusCode, string(data)) 86 | return nil 87 | } 88 | err = yaml.Unmarshal(data, &f) 89 | if err != nil { 90 | return nil 91 | } 92 | if err != nil { 93 | return nil 94 | } 95 | f.Namespace = parts[0] 96 | f.Name = parts[1] 97 | return &f 98 | } 99 | 100 | // remotePackageInfo downloads the given manifest files and tries to parse it. 101 | // core.Package struct is returned if it succeeds, otherwise nil. 102 | func remotePackageInfo(package_url string) *core.Package { 103 | var netClient = &http.Client{ 104 | Timeout: time.Second * 10, 105 | } 106 | resp, err := netClient.Get(package_url) 107 | if err != nil { 108 | return nil 109 | } 110 | 111 | defer resp.Body.Close() 112 | 113 | data, err := ioutil.ReadAll(resp.Body) 114 | if resp.StatusCode != 200 { 115 | fmt.Printf("The request %s returned non-200 [%d] response: %s.", 116 | package_url, resp.StatusCode, string(data)) 117 | return nil 118 | } 119 | 120 | var pkg core.Package 121 | 122 | if err := pkg.Parse(data); err != nil { 123 | return nil 124 | } 125 | 126 | return &pkg 127 | } 128 | 129 | func NeedsUpdate(localPkg, remotePkg *core.Package, compareCreated bool) (bool, error) { 130 | // Compare Version attribute. 131 | localVersion, err := VersionStringToInt(localPkg.Version) 132 | if err != nil { 133 | return true, err 134 | } 135 | remoteVersion, err := VersionStringToInt(remotePkg.Version) 136 | if err != nil { 137 | return true, err 138 | } 139 | needsUpdate := localVersion < remoteVersion 140 | if needsUpdate || !compareCreated { 141 | return needsUpdate, nil 142 | } 143 | 144 | // Compare Created attribute. 145 | createdLocal := localPkg.Created.GetTime() 146 | createdRemote := remotePkg.Created.GetTime() 147 | if createdLocal == nil || createdRemote == nil { 148 | return true, nil 149 | } 150 | return createdLocal.Before(*createdRemote), nil 151 | } 152 | 153 | func (r *Repo) downloadFile(fileURL string, destPath string, name string) error { 154 | compressed := strings.HasSuffix(fileURL, ".gz") 155 | output, err := os.Create(filepath.Join(destPath, strings.TrimSuffix(name, ".gz"))) 156 | if err != nil { 157 | return err 158 | } 159 | defer output.Close() 160 | fmt.Printf("Downloading %s... from %s\n", name, fileURL) 161 | tr := &http.Transport{ 162 | DisableCompression: true, 163 | Proxy: http.ProxyFromEnvironment, 164 | } 165 | client := &http.Client{Transport: tr} 166 | resp, err := client.Get(fileURL) 167 | if err != nil { 168 | return err 169 | } 170 | defer resp.Body.Close() 171 | bar := pb.New64(resp.ContentLength).Set(pb.Bytes, true) 172 | bar.Start() 173 | proxyReader := bar.NewProxyReader(resp.Body) 174 | var reader io.Reader = proxyReader 175 | if compressed { 176 | gzipReader, err := gzip.NewReader(proxyReader) 177 | if err != nil { 178 | return err 179 | } 180 | reader = gzipReader 181 | } 182 | _, err = io.Copy(output, reader) 183 | bar.Finish() 184 | if err != nil { 185 | return err 186 | } 187 | return nil 188 | } 189 | -------------------------------------------------------------------------------- /util/rofs_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Waldemar Kozaczuk. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package util_test 9 | 10 | import ( 11 | "github.com/cloudius-systems/capstan/cmd" 12 | "github.com/cloudius-systems/capstan/util" 13 | . "gopkg.in/check.v1" 14 | "io" 15 | "io/ioutil" 16 | "os" 17 | "path" 18 | "path/filepath" 19 | "strings" 20 | ) 21 | 22 | type rofsSuite struct{} 23 | 24 | var _ = Suite(&rofsSuite{}) 25 | 26 | func (*rofsSuite) TestWriteRofsImage(c *C) { 27 | // We are going to create an empty temp directory. 28 | tmp, _ := ioutil.TempDir("", "pkg") 29 | defer os.RemoveAll(tmp) 30 | // 31 | // Copy test data to the temp dir so that we can create ROFS image out of it 32 | err := copyDirectory("../cmd/testdata/hashing", tmp) 33 | 34 | paths, err := cmd.CollectDirectoryContents(tmp) 35 | c.Assert(err, IsNil) 36 | 37 | rofsImagePath := path.Join(tmp, "rofs.img") 38 | err = util.WriteRofsImage(rofsImagePath, paths, tmp, true) 39 | c.Assert(err, IsNil) 40 | 41 | rofsImage, err := os.OpenFile(rofsImagePath, os.O_RDONLY, 0644) 42 | c.Assert(err, IsNil) 43 | defer rofsImage.Close() 44 | 45 | rofsSb, err := util.ReadRofsSuperBlock(rofsImage) 46 | c.Assert(err, IsNil) 47 | c.Assert(rofsSb.InodesCount, Equals, uint64(11)) 48 | c.Assert(rofsSb.DirectoryEntriesCount, Equals, uint64(10)) 49 | c.Assert(rofsSb.SymlinksCount, Equals, uint64(1)) 50 | } 51 | 52 | func copyDirectory(srcDir string, dest string) error { 53 | return filepath.Walk(srcDir, func(path string, info os.FileInfo, _ error) error { 54 | relPath := strings.TrimPrefix(path, srcDir) 55 | fi, err := os.Lstat(path) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | switch { 61 | case fi.Mode()&os.ModeSymlink == os.ModeSymlink: 62 | linkTarget, _ := os.Readlink(path) 63 | if strings.HasPrefix(linkTarget, "/") || strings.HasPrefix(linkTarget, "..") { 64 | srcDir := filepath.Dir(path) 65 | 66 | if linkTarget, err = filepath.Abs(filepath.Join(srcDir, linkTarget)); err != nil { 67 | return err 68 | } 69 | linkTarget = strings.TrimPrefix(linkTarget, strings.TrimSuffix(path, dest)) 70 | } 71 | os.Symlink(linkTarget, filepath.Join(dest, relPath)) 72 | 73 | case fi.Mode().IsRegular(): 74 | from, err := os.Open(path) 75 | if err != nil { 76 | return err 77 | } 78 | defer from.Close() 79 | 80 | destFile := filepath.Join(dest, relPath) 81 | if err := os.MkdirAll(filepath.Dir(destFile), 0777); err != nil { 82 | return err 83 | } 84 | 85 | to, err := os.OpenFile(destFile, os.O_RDWR|os.O_CREATE, 0666) 86 | if err != nil { 87 | return err 88 | } 89 | defer to.Close() 90 | 91 | if _, err = io.Copy(to, from); err != nil { 92 | return err 93 | } 94 | } 95 | return nil 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /util/s3_repository_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package util 9 | 10 | import ( 11 | "github.com/cloudius-systems/capstan/core" 12 | "time" 13 | 14 | . "gopkg.in/check.v1" 15 | ) 16 | 17 | type s3repoSuite struct{} 18 | 19 | var _ = Suite(&s3repoSuite{}) 20 | 21 | func (*s3repoSuite) TestNeedsUpdate(c *C) { 22 | m := []struct { 23 | comment string 24 | localPkg core.Package 25 | localCreated string 26 | remotePkg core.Package 27 | remoteCreated string 28 | compareCreated bool 29 | expectedNeedsUpdate bool 30 | }{ 31 | { 32 | "regular case", 33 | core.Package{Version: "4.12.10"}, "", 34 | core.Package{Version: "5.0.0"}, "", 35 | false, 36 | true, 37 | }, 38 | { 39 | "already latest", 40 | core.Package{Version: "4.12.10"}, "", 41 | core.Package{Version: "4.12.10"}, "", 42 | false, 43 | false, 44 | }, 45 | { 46 | "local ahead of remote", 47 | core.Package{Version: "4.12.10"}, "", 48 | core.Package{Version: "3.0.0"}, "", 49 | false, 50 | false, 51 | }, 52 | { 53 | "needs update #major", 54 | core.Package{Version: "4.12.10"}, "", 55 | core.Package{Version: "5.12.10"}, "", 56 | false, 57 | true, 58 | }, 59 | { 60 | "needs update #minor", 61 | core.Package{Version: "4.12.10"}, "", 62 | core.Package{Version: "4.13.10"}, "", 63 | false, 64 | true, 65 | }, 66 | { 67 | "needs update #patch", 68 | core.Package{Version: "4.12.10"}, "", 69 | core.Package{Version: "4.13.11"}, "", 70 | false, 71 | true, 72 | }, 73 | { 74 | "update because of time created", 75 | core.Package{Version: "4.12.10"}, "2018-01-05 07:44", 76 | core.Package{Version: "4.12.10"}, "2018-01-05 07:45", 77 | true, 78 | true, 79 | }, 80 | { 81 | "both version and time created are latest", 82 | core.Package{Version: "4.12.10"}, "2018-01-05 07:44", 83 | core.Package{Version: "4.12.10"}, "2018-01-05 07:44", 84 | true, 85 | false, 86 | }, 87 | { 88 | "time created invalid", 89 | core.Package{Version: "4.12.10"}, "invalid", 90 | core.Package{Version: "4.12.10"}, "invalid", 91 | true, 92 | true, 93 | }, 94 | } 95 | for i, args := range m { 96 | c.Logf("CASE #%d: %s", i, args.comment) 97 | 98 | // Prepare. 99 | if t, err := time.Parse(core.FRIENDLY_TIME_F, args.localCreated); err == nil { 100 | args.localPkg.Created = core.YamlTime{t} 101 | } 102 | if t, err := time.Parse(core.FRIENDLY_TIME_F, args.remoteCreated); err == nil { 103 | args.remotePkg.Created = core.YamlTime{t} 104 | } 105 | 106 | // This is what we're testing here. 107 | res, err := NeedsUpdate(&args.localPkg, &args.remotePkg, args.compareCreated) 108 | 109 | // Expectations. 110 | c.Check(err, IsNil) 111 | c.Check(res, Equals, args.expectedNeedsUpdate) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /util/termios.go: -------------------------------------------------------------------------------- 1 | // +build darwin freebsd linux 2 | 3 | /* 4 | * Copyright (C) 2014 Cloudius Systems, Ltd. 5 | * 6 | * This work is open source software, licensed under the terms of the 7 | * BSD license as described in the LICENSE file in the top-level directory. 8 | */ 9 | 10 | package util 11 | 12 | import ( 13 | "os/exec" 14 | ) 15 | 16 | func RawTerm() error { 17 | cmd := exec.Command("stty", "raw") 18 | return cmd.Run() 19 | } 20 | 21 | func ResetTerm() { 22 | cmd := exec.Command("stty", "cooked") 23 | cmd.Run() 24 | } 25 | -------------------------------------------------------------------------------- /util/termios_windows.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package util 9 | 10 | func RawTerm() error { 11 | return nil 12 | } 13 | 14 | func ResetTerm() { 15 | } 16 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/index.yaml/payload: -------------------------------------------------------------------------------- 1 | Some stub data -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv-loader.qemu/payload: -------------------------------------------------------------------------------- 1 | Some stub data -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.bootstrap.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.bootstrap 2 | title: OSv Bootstrap 3 | author: Waldek Kozaczuk 4 | version: 0.51.0 5 | created: 2018-05-30T04:03:18Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.cli.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.cli 2 | title: Command Line 3 | author: Waldek Kozaczuk 4 | version: 0.51.0 5 | require: 6 | - osv.httpserver-api 7 | created: 2018-05-30T04:04:22Z 8 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.httpserver-api.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.httpserver-api 2 | title: OSv httpserver with APIs (backend) 3 | author: Waldek Kozaczuk 4 | version: 0.51.0 5 | created: 2018-05-30T04:03:54Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.httpserver-html5-cli.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.httpserver-html5-cli 2 | title: OSv HTML5 Terminal (frontend) 3 | author: Waldek Kozaczuk 4 | version: 0.51.0 5 | require: 6 | - osv.httpserver-api 7 | created: 2018-05-30T04:04:07Z 8 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.httpserver-html5-gui.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.httpserver-html5-gui 2 | title: OSv HTML5 GUI (frontend) 3 | author: Waldek Kozaczuk 4 | version: 0.51.0 5 | require: 6 | - osv.httpserver-api 7 | created: 2018-05-30T04:03:58Z 8 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.iperf.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.iperf 2 | title: Iperf 3 | author: Waldek Kozaczuk 4 | version: 2.0.5 5 | created: 2018-05-30T04:04:35Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.lighttpd.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.lighttpd 2 | title: Lighttpd 3 | author: Waldek Kozaczuk 4 | version: 1.4.45 5 | created: 2018-05-30T04:04:26Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.memcached.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.memcached 2 | title: Memcached 3 | author: Waldek Kozaczuk 4 | version: 1.4.21 5 | created: 2018-05-30T04:04:47Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.mysql.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.mysql 2 | title: MySQL 3 | author: Waldek Kozaczuk 4 | version: 5.6.40 5 | created: 2018-05-30T04:04:52Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.netperf.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.netperf 2 | title: Netperf 3 | author: Waldek Kozaczuk 4 | version: 2.7.0 5 | created: 2018-05-30T04:04:39Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.nginx.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.nginx 2 | title: NGINX 3 | author: Waldek Kozaczuk 4 | version: 1.12.2 5 | created: 2018-05-30T04:04:30Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.node-js.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.node-js 2 | title: Node JS 3 | author: Waldek Kozaczuk 4 | version: 8.11.2 5 | created: 2018-05-30T04:03:34Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.openjdk10-java-base.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.openjdk10-java-base 2 | title: Open JDK 10 (java-base) 3 | author: Waldek Kozaczuk 4 | version: 10.0.1 5 | require: 6 | - osv.run-java 7 | created: 2018-05-30T04:03:48Z 8 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.redis-memonly.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.redis-memonly 2 | title: Redis 3 | author: Waldek Kozaczuk 4 | version: 3.2.8 5 | created: 2018-05-30T04:04:43Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.run-go.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.run-go 2 | title: Run Golang apps 3 | author: Waldek Kozaczuk 4 | version: 0.51.0 5 | created: 2018-05-30T04:03:30Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.51.0/osv.run-java.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.run-java 2 | title: Run Java apps 3 | author: Waldek Kozaczuk 4 | version: 0.51.0 5 | created: 2018-06-17T17:22:44Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.52.0/osv.ffmpeg.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.ffmpeg 2 | title: ffmpeg 3 | author: Waldek Kozaczuk 4 | version: 4.0.2 5 | created: 2018-10-19T03:24:46Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.52.0/osv.httpserver-api.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.httpserver-api 2 | title: OSv httpserver with APIs (backend) 3 | author: Waldek Kozaczuk 4 | version: 0.52.0 5 | created: 2018-10-30T04:25:40Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.52.0/osv.libz.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.libz 2 | title: libz 3 | author: Waldek Kozaczuk 4 | version: 1.0.0 5 | created: 2018-10-30T21:23:03Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.52.0/osv.python3x.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.python3x 2 | title: python3x 3 | author: Waldek Kozaczuk 4 | version: 3.6.6 5 | created: 2018-10-19T03:22:05Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.53.0/osv.httpserver-api.mpm/payload: -------------------------------------------------------------------------------- 1 | Some stub data -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.53.0/osv.httpserver-api.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.httpserver-api 2 | title: OSv httpserver with APIs (backend) 3 | author: Waldek Kozaczuk 4 | version: 0.53.0 5 | created: 2019-03-12T03:11:16Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.53.0/osv.httpserver-html5-cli.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.httpserver-html5-cli 2 | title: OSv HTML5 Terminal (frontend) 3 | author: Waldek Kozaczuk 4 | version: 0.53.0 5 | require: 6 | - osv.httpserver-api 7 | created: 2019-03-26T11:47:28Z 8 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.53.0/osv.httpserver-html5-gui-and-cli.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.httpserver-html5-gui-and-cli 2 | title: OSv HTML5 Terminal (frontend) 3 | author: Waldek Kozaczuk 4 | version: 0.53.0 5 | require: 6 | - osv.httpserver-api 7 | created: 2019-03-12T03:11:26Z 8 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.53.0/osv.httpserver-html5-gui.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.httpserver-html5-gui 2 | title: OSv HTML5 GUI (frontend) 3 | author: Waldek Kozaczuk 4 | version: 0.53.0 5 | require: 6 | - osv.httpserver-api 7 | created: 2019-03-26T11:47:43Z 8 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.53.0/osv.python3x.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.python3x 2 | title: python3x 3 | author: Waldek Kozaczuk 4 | version: 3.6.6 5 | created: 2019-03-12T03:11:32Z 6 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.54.0/osv.bootstrap.mpm/payload: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudius-systems/capstan/6bb7574c54296a47a30984604dcbb41c8cf8ad8a/util/testdata/github/cloudius-systems/osv/releases/download/v0.54.0/osv.bootstrap.mpm/payload -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.54.0/osv.bootstrap.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.bootstrap 2 | title: OSv Bootstrap 3 | author: Anonymous 4 | version: 0.54.0 5 | created: "2019-09-16T07:52:38-04:00" 6 | platform: Ubuntu 19.04 7 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.54.0/osv.cli.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.cli 2 | title: OSv Command Line 3 | author: Anonymous 4 | version: 0.54.0 5 | created: "2019-09-16T07:53:09-04:00" 6 | platform: Ubuntu 19.04 7 | require: 8 | - osv.httpserver-api 9 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.54.0/osv.httpserver-api.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.httpserver-api 2 | title: OSv httpserver with APIs (backend) 3 | author: Anonymous 4 | version: 0.54.0 5 | created: "2019-09-16T07:52:43-04:00" 6 | platform: Ubuntu 19.04 7 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.54.0/osv.httpserver-html5-cli.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.httpserver-html5-cli 2 | title: OSv HTML5 Terminal (frontend) 3 | author: Anonymous 4 | version: 1.0.0 5 | created: "2019-09-16T07:53:04-04:00" 6 | platform: Ubuntu 19.04 7 | require: 8 | - osv.httpserver-api 9 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.54.0/osv.httpserver-html5-gui.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.httpserver-html5-gui 2 | title: OSv HTML5 GUI (frontend) 3 | author: Anonymous 4 | version: 0.54.0 5 | created: "2019-09-16T07:52:51-04:00" 6 | platform: Ubuntu 19.04 7 | require: 8 | - osv.httpserver-api 9 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.54.0/osv.libz.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.libz 2 | title: libz 3 | author: Anonymous 4 | version: 1.0.0 5 | created: "2019-09-16T07:53:29-04:00" 6 | platform: Ubuntu 19.04 7 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.54.0/osv.run-go.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.run-go 2 | title: Run Golang wrapper 3 | author: Anonymous 4 | version: 0.54.0 5 | created: "2019-09-16T07:53:23-04:00" 6 | platform: Ubuntu 19.04 7 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.54.0/osv.run-java.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.run-java 2 | title: Run Java wrapper 3 | author: Anonymous 4 | version: 0.54.0 5 | created: "2019-09-16T07:53:17-04:00" 6 | platform: Ubuntu 19.04 7 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.57.0/osv-loader.qemu.x86_64/payload: -------------------------------------------------------------------------------- 1 | name: osv.httpserver-html5-gui 2 | title: OSv HTML5 GUI (frontend) 3 | author: Waldek Kozaczuk 4 | version: 0.53.0 5 | require: 6 | - osv.httpserver-api 7 | created: 2019-03-26T11:47:43Z 8 | -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.57.0/osv.bootstrap.mpm.x86_64/payload: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudius-systems/capstan/6bb7574c54296a47a30984604dcbb41c8cf8ad8a/util/testdata/github/cloudius-systems/osv/releases/download/v0.57.0/osv.bootstrap.mpm.x86_64/payload -------------------------------------------------------------------------------- /util/testdata/github/cloudius-systems/osv/releases/download/v0.57.0/osv.bootstrap.yaml/payload: -------------------------------------------------------------------------------- 1 | name: osv.bootstrap 2 | title: OSv Bootstrap 3 | author: Anonymous 4 | version: 0.54.0 5 | created: "2019-09-16T07:52:38-04:00" 6 | platform: Ubuntu 19.04 7 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * Modifications copyright (C) 2015 XLAB, Ltd. 4 | * 5 | * This work is open source software, licensed under the terms of the 6 | * BSD license as described in the LICENSE file in the top-level directory. 7 | */ 8 | 9 | package util 10 | 11 | import ( 12 | "fmt" 13 | "io" 14 | "io/ioutil" 15 | "net" 16 | "os" 17 | "os/exec" 18 | "path/filepath" 19 | "regexp" 20 | "runtime" 21 | "strconv" 22 | "strings" 23 | "time" 24 | ) 25 | 26 | func ConfigDir() string { 27 | root := os.Getenv("CAPSTAN_ROOT") 28 | if root == "" { 29 | root = filepath.Join(HomePath(), "/.capstan/") 30 | } 31 | return root 32 | } 33 | 34 | func HomePath() string { 35 | if runtime.GOOS == "windows" { 36 | return filepath.Join(os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH")) 37 | } else { 38 | return os.Getenv("HOME") 39 | } 40 | } 41 | 42 | func ID() string { 43 | return fmt.Sprintf("i%v", time.Now().Unix()) 44 | } 45 | 46 | func CopyFile(src, dst string) *exec.Cmd { 47 | var cmd *exec.Cmd 48 | if runtime.GOOS == "windows" { 49 | cmd = exec.Command("cmd.exe", "/c", "copy", src, dst) 50 | } else { 51 | cmd = exec.Command("cp", src, dst) 52 | } 53 | return cmd 54 | } 55 | 56 | func CopyLocalFile(dst, src string) error { 57 | fi, err := os.Stat(src) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | s, err := os.Open(src) 63 | if err != nil { 64 | return err 65 | } 66 | // no need to check errors on read only file, we already got everything 67 | // we need from the filesystem, so nothing can go wrong now. 68 | defer s.Close() 69 | d, err := os.Create(dst) 70 | // Ensure the target file has the same mode as source 71 | d.Chmod(fi.Mode()) 72 | if err != nil { 73 | return err 74 | } 75 | if _, err := io.Copy(d, s); err != nil { 76 | d.Close() 77 | return err 78 | } 79 | return d.Close() 80 | } 81 | 82 | func SearchInstance(name string) (instanceName, instancePlatform string) { 83 | instanceName = "" 84 | instancePlatform = "" 85 | rootDir := filepath.Join(ConfigDir(), "instances") 86 | platforms, _ := ioutil.ReadDir(rootDir) 87 | for _, platform := range platforms { 88 | if !platform.IsDir() { 89 | continue 90 | } 91 | platformDir := filepath.Join(rootDir, platform.Name()) 92 | instances, _ := ioutil.ReadDir(platformDir) 93 | for _, instance := range instances { 94 | if !instance.IsDir() { 95 | continue 96 | } 97 | if name != instance.Name() { 98 | continue 99 | } 100 | 101 | // Instance only exists if osv.config is present. 102 | if _, err := os.Stat(filepath.Join(platformDir, name, "osv.config")); os.IsNotExist(err) { 103 | // Search no more. 104 | return 105 | } 106 | 107 | instanceName = instance.Name() 108 | instancePlatform = platform.Name() 109 | return 110 | } 111 | } 112 | return 113 | } 114 | 115 | func ConnectAndWait(network, path string) (net.Conn, error) { 116 | var conn net.Conn 117 | var err error 118 | for i := 0; i < 20; i++ { 119 | conn, err = Connect(network, path) 120 | if err == nil { 121 | break 122 | } 123 | time.Sleep(500 * time.Millisecond) 124 | } 125 | return conn, err 126 | } 127 | 128 | // RemoveOrphanedInstances removes directories of instances that were not persisted with --persist. 129 | func RemoveOrphanedInstances(verbose bool) error { 130 | // TODO: Implement function InstancesPath() 131 | qemuDir := filepath.Join(ConfigDir(), "instances", "qemu") 132 | 133 | // Do nothing when instances/qemu folder does not exist. 134 | if _, err := os.Stat(qemuDir); os.IsNotExist(err) { 135 | return nil 136 | } 137 | 138 | instanceDirs, _ := ioutil.ReadDir(qemuDir) 139 | for _, instanceDir := range instanceDirs { 140 | if instanceDir.IsDir() { 141 | instanceDir := filepath.Join(qemuDir, instanceDir.Name()) 142 | 143 | // Remove orphaned instance 144 | if _, err := os.Stat(filepath.Join(instanceDir, "osv.config")); os.IsNotExist(err) { 145 | if verbose { 146 | fmt.Println("Removing orphaned instance folder:", instanceDir) 147 | } 148 | 149 | if err = os.RemoveAll(instanceDir); err != nil { 150 | return err 151 | } 152 | } 153 | } 154 | } 155 | 156 | return nil 157 | } 158 | 159 | func ExtendMap(m map[string]string, additional map[string]string) { 160 | if m == nil || additional == nil { 161 | return 162 | } 163 | 164 | for key, value := range additional { 165 | if _, exists := m[key]; !exists { 166 | m[key] = value 167 | } 168 | } 169 | } 170 | 171 | func StringInSlice(a string, list []string) bool { 172 | for _, b := range list { 173 | if b == a { 174 | return true 175 | } 176 | } 177 | return false 178 | } 179 | 180 | // VersionStringToInt converts 1.2.3 into 1002003 to make it comparable. 181 | func VersionStringToInt(version string) (int, error) { 182 | if ok, _ := regexp.MatchString(`^\d{1,3}(\.\d{1,3}){0,2}$`, version); !ok { 183 | return 0, fmt.Errorf("Invalid version string: '%s'", version) 184 | } 185 | res := 0 186 | weight := 1000000 187 | for _, part := range strings.Split(version, ".") { 188 | n, _ := strconv.Atoi(part) 189 | res += weight * n 190 | weight /= 1000 191 | } 192 | return res, nil 193 | } 194 | -------------------------------------------------------------------------------- /util/util_darwin.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package util 9 | 10 | func IsDirectIOSupported(path string) bool { 11 | return false 12 | } 13 | -------------------------------------------------------------------------------- /util/util_freebsd.go: -------------------------------------------------------------------------------- 1 | /* FreeBSD has support for direct io via the DIRECTIO kernel option 2 | * since 4.9. The GENERIC kernel does not have it enabled as of 10.1. 3 | * Check the local /usr/src/sys/amd64/conf/GENERIC file and 4 | * https://www.freebsd.org/doc/en/books/handbook/kernelconfig-config.html 5 | */ 6 | 7 | package util 8 | 9 | func IsDirectIOSupported(path string) bool { 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /util/util_linux.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Cloudius Systems, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package util 9 | 10 | import ( 11 | "os" 12 | "syscall" 13 | ) 14 | 15 | func IsDirectIOSupported(path string) bool { 16 | f, err := os.OpenFile(path, syscall.O_DIRECT, 0) 17 | defer f.Close() 18 | return err == nil 19 | } 20 | -------------------------------------------------------------------------------- /util/util_posix.go: -------------------------------------------------------------------------------- 1 | // +build darwin freebsd linux 2 | 3 | /* 4 | * Copyright (C) 2014 Cloudius Systems, Ltd. 5 | * 6 | * This work is open source software, licensed under the terms of the 7 | * BSD license as described in the LICENSE file in the top-level directory. 8 | */ 9 | 10 | package util 11 | 12 | import ( 13 | "net" 14 | ) 15 | 16 | func Connect(network, path string) (net.Conn, error) { 17 | return net.Dial(network, path) 18 | } 19 | -------------------------------------------------------------------------------- /util/util_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 XLAB, Ltd. 3 | * 4 | * This work is open source software, licensed under the terms of the 5 | * BSD license as described in the LICENSE file in the top-level directory. 6 | */ 7 | 8 | package util 9 | 10 | import ( 11 | "os" 12 | 13 | . "gopkg.in/check.v1" 14 | ) 15 | 16 | type utilSuite struct{} 17 | 18 | var _ = Suite(&utilSuite{}) 19 | 20 | func (*utilSuite) TestConfigDir(c *C) { 21 | m := []struct { 22 | comment string 23 | env map[string]string 24 | expected string 25 | }{ 26 | { 27 | "simplest case", 28 | map[string]string{ 29 | "HOME": "/my/home", 30 | }, 31 | "/my/home/.capstan", 32 | }, 33 | { 34 | "with CAPSTAN_ROOT", 35 | map[string]string{ 36 | "HOME": "/my/home", 37 | "CAPSTAN_ROOT": "/capstan/root", 38 | }, 39 | "/capstan/root", 40 | }, 41 | } 42 | for i, args := range m { 43 | c.Logf("CASE #%d: %s", i, args.comment) 44 | 45 | // Prepare. 46 | originalEnv := setEnvironmentVars(args.env) 47 | 48 | // This is what we're testing here. 49 | dir := ConfigDir() 50 | 51 | // Expectations. 52 | c.Check(dir, Equals, args.expected) 53 | 54 | // Restore original environemt variables 55 | setEnvironmentVars(originalEnv) 56 | } 57 | } 58 | 59 | func (*utilSuite) TestVersionStringToInt(c *C) { 60 | m := []struct { 61 | comment string 62 | version string 63 | expected int 64 | }{ 65 | { 66 | "regular case", 67 | "4.12.10", 68 | 4012010, 69 | }, 70 | { 71 | "major version", 72 | "15.0.0", 73 | 15000000, 74 | }, 75 | { 76 | "minor version", 77 | "0.15.0", 78 | 15000, 79 | }, 80 | { 81 | "patch", 82 | "0.0.15", 83 | 15, 84 | }, 85 | { 86 | "missing patch", 87 | "0.15", 88 | 15000, 89 | }, 90 | { 91 | "missing minor", 92 | "15", 93 | 15000000, 94 | }, 95 | } 96 | for i, args := range m { 97 | c.Logf("CASE #%d: %s", i, args.comment) 98 | 99 | // This is what we're testing here. 100 | res, err := VersionStringToInt(args.version) 101 | 102 | // Expectations. 103 | c.Check(err, IsNil) 104 | c.Check(res, Equals, args.expected) 105 | } 106 | } 107 | 108 | func (*utilSuite) TestVersionStringToIntInvalid(c *C) { 109 | m := []struct { 110 | comment string 111 | version string 112 | expectedErr string 113 | }{ 114 | { 115 | "completely wrong version", 116 | "x", 117 | "Invalid version string: 'x'", 118 | }, 119 | { 120 | "partially wrong version", 121 | "1.x.3", 122 | "Invalid version string: '1.x.3'", 123 | }, 124 | { 125 | "empty version", 126 | "", 127 | "Invalid version string: ''", 128 | }, 129 | { 130 | "continues after patch", 131 | "1.2.3.4", 132 | "Invalid version string: '1.2.3.4'", 133 | }, 134 | { 135 | "major greater than 999", 136 | "1000.2.3", 137 | "Invalid version string: '1000.2.3'", 138 | }, 139 | { 140 | "minor greater than 999", 141 | "1.2000.3", 142 | "Invalid version string: '1.2000.3'", 143 | }, 144 | { 145 | "patch greater than 999", 146 | "1.2.3000", 147 | "Invalid version string: '1.2.3000'", 148 | }, 149 | } 150 | for i, args := range m { 151 | c.Logf("CASE #%d: %s", i, args.comment) 152 | 153 | // This is what we're testing here. 154 | _, err := VersionStringToInt(args.version) 155 | 156 | // Expectations. 157 | c.Check(err, ErrorMatches, args.expectedErr) 158 | } 159 | } 160 | 161 | // 162 | // Utility 163 | // 164 | 165 | func setEnvironmentVars(env map[string]string) map[string]string { 166 | original := map[string]string{} 167 | for key, value := range env { 168 | original[key] = value 169 | os.Setenv(key, value) 170 | } 171 | return original 172 | } 173 | -------------------------------------------------------------------------------- /util/util_win.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | /* 4 | * Copyright (C) 2014 Cloudius Systems, Ltd. 5 | * 6 | * This work is open source software, licensed under the terms of the 7 | * BSD license as described in the LICENSE file in the top-level directory. 8 | */ 9 | 10 | package util 11 | 12 | import ( 13 | "gopkg.in/natefinch/npipe.v2" 14 | "net" 15 | ) 16 | 17 | func Connect(network, path string) (net.Conn, error) { 18 | return npipe.Dial(path) 19 | } 20 | 21 | func IsDirectIOSupported(path string) bool { 22 | return false 23 | } 24 | --------------------------------------------------------------------------------