diff --git a/src/runtime/pkg/govmm/.github/workflows/main.yml b/src/runtime/pkg/govmm/.github/workflows/main.yml new file mode 100644 index 0000000000..912d3d225d --- /dev/null +++ b/src/runtime/pkg/govmm/.github/workflows/main.yml @@ -0,0 +1,23 @@ +on: ["pull_request"] +name: Unit tests +jobs: + test: + strategy: + matrix: + go-version: [1.15.x, 1.16.x] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + version: latest + args: -c .golangci.yml -v + - name: go test + run: go test ./... diff --git a/src/runtime/pkg/govmm/.gitignore b/src/runtime/pkg/govmm/.gitignore new file mode 100644 index 0000000000..b25c15b81f --- /dev/null +++ b/src/runtime/pkg/govmm/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/src/runtime/pkg/govmm/.golangci.yml b/src/runtime/pkg/govmm/.golangci.yml new file mode 100644 index 0000000000..6b7fcdebe9 --- /dev/null +++ b/src/runtime/pkg/govmm/.golangci.yml @@ -0,0 +1,35 @@ +# Copyright (c) 2021 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +run: + concurrency: 4 + deadline: 600s + skip-dirs: + - vendor +# Ignore auto-generated protobuf code. + skip-files: + - ".*\\.pb\\.go$" + +linters: + disable-all: true + enable: + - deadcode + - gocyclo + - gofmt + - gosimple + - govet + - ineffassign + - misspell + - staticcheck + - structcheck + - typecheck + - unconvert + - unused + - varcheck + +linters-settings: + gocyclo: + min_complexity: 15 + unused: + check-exported: true diff --git a/src/runtime/pkg/govmm/.travis.yml b/src/runtime/pkg/govmm/.travis.yml new file mode 100644 index 0000000000..8dc92a8205 --- /dev/null +++ b/src/runtime/pkg/govmm/.travis.yml @@ -0,0 +1,26 @@ +language: go + +go: + - "1.10" + - "1.11" + - tip +arch: + - s390x + +go_import_path: github.com/kata-containers/govmm + +matrix: + allow_failures: + - go: tip + +before_install: + - go get github.com/alecthomas/gometalinter + - gometalinter --install + - go get github.com/mattn/goveralls + +script: + - go env + - gometalinter --tests --vendor --disable-all --enable=misspell --enable=vet --enable=ineffassign --enable=gofmt --enable=gocyclo --cyclo-over=15 --enable=golint --enable=errcheck --enable=deadcode --enable=staticcheck -enable=gas ./... + +after_success: + - $GOPATH/bin/goveralls -repotoken $COVERALLS_TOKEN -v -service=travis-ci diff --git a/src/runtime/pkg/govmm/CONTRIBUTING.md b/src/runtime/pkg/govmm/CONTRIBUTING.md new file mode 100644 index 0000000000..451903e0a7 --- /dev/null +++ b/src/runtime/pkg/govmm/CONTRIBUTING.md @@ -0,0 +1,96 @@ +# Contributing to Virtual Machine Manager for Go + +Virtual Machine Manager for Go is an open source project licensed under the [Apache v2 License] (https://opensource.org/licenses/Apache-2.0) + +## Coding Style + +Virtual Machine Manager for Go follows the standard formatting recommendations and language idioms set out +in [Effective Go](https://golang.org/doc/effective_go.html) and in the +[Go Code Review Comments wiki](https://github.com/golang/go/wiki/CodeReviewComments). + +## Certificate of Origin + +In order to get a clear contribution chain of trust we use the [signed-off-by language] (https://01.org/community/signed-process) +used by the Linux kernel project. + +## Patch format + +Beside the signed-off-by footer, we expect each patch to comply with the following format: + +``` +Change summary + +More detailed explanation of your changes: Why and how. +Wrap it to 72 characters. +See [here] (http://chris.beams.io/posts/git-commit/) +for some more good advices. + +Fixes #NUMBER (or URL to the issue) + +Signed-off-by: +``` + +For example: + +``` +Fix poorly named identifiers + +One identifier, fnname, in func.go was poorly named. It has been renamed +to fnName. Another identifier retval was not needed and has been removed +entirely. + +Fixes #1 + +Signed-off-by: Mark Ryan +``` + +## New files + +Each Go source file in the Virtual Machine Manager for Go project must +contain the following header: + +``` +/* +// Copyright contributors to the Virtual Machine Manager for Go project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ +``` + +## Contributors File + +This CONTRIBUTORS.md file is a partial list of contributors to the +Virtual Machine Manager for Go project. To see the full list of +contributors, see the revision history in source control. + +Contributors who wish to be recognized in this file should add +themselves (or their employer, as appropriate). + +## Pull requests + +We accept github pull requests. + +## Quality Controls + +We request you give quality assurance some consideration by: + +* Adding go unit tests for changes where it makes sense. +* Enabling [Travis CI](https://travis-ci.org/kata-containers/govmm) on your github fork of Virtual Machine Manager for Go to get continuous integration feedback on your dev/test branches. + +## Issue tracking + +If you have a problem, please let us know. If it's a bug not already documented, by all means please [open an +issue in github](https://github.com/kata-containers/govmm/issues/new) so we all get visibility +the problem and work toward resolution. + +Any security issues discovered with govmm should be reported by following the instructions on https://01.org/security. diff --git a/src/runtime/pkg/govmm/COPYING b/src/runtime/pkg/govmm/COPYING new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/src/runtime/pkg/govmm/COPYING @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/runtime/pkg/govmm/README.md b/src/runtime/pkg/govmm/README.md new file mode 100644 index 0000000000..d96c4d9deb --- /dev/null +++ b/src/runtime/pkg/govmm/README.md @@ -0,0 +1,21 @@ +# Virtual Machine Manager for Go + +[![Go Report Card](https://goreportcard.com/badge/github.com/kata-containers/govmm)](https://goreportcard.com/report/github.com/kata-containers/govmm) +[![Build Status](https://travis-ci.org/kata-containers/govmm.svg?branch=master)](https://travis-ci.org/kata-containers/govmm) +[![GoDoc](https://godoc.org/github.com/kata-containers/govmm/qemu?status.svg)](https://godoc.org/github.com/kata-containers/govmm/qemu) +[![Coverage Status](https://coveralls.io/repos/github/kata-containers/govmm/badge.svg?branch=master)](https://coveralls.io/github/kata-containers/govmm?branch=master) + +Virtual Machine Manager for Go (govmm) is a suite of packages that +provide Go APIs for creating and managing virtual machines. There's +currently support for only one hypervisor, qemu/kvm (version 5.0 and +later), support for which is provided by the +github.com/kata-containers/govmm/qemu package. + +The qemu package provides APIs for launching qemu instances and for +managing those instances via QMP, once launched. VM instances can +be stopped, have devices attached to them and monitored for events +via the qemu APIs. + +The qemu package has no external dependencies apart from the Go +standard library and so is nice and easy to vendor inside other +projects. diff --git a/src/runtime/pkg/govmm/go.mod b/src/runtime/pkg/govmm/go.mod new file mode 100644 index 0000000000..377ac043fd --- /dev/null +++ b/src/runtime/pkg/govmm/go.mod @@ -0,0 +1,3 @@ +module github.com/kata-containers/govmm + +go 1.16 diff --git a/src/runtime/pkg/govmm/qemu/examples_test.go b/src/runtime/pkg/govmm/qemu/examples_test.go new file mode 100644 index 0000000000..1946aeef82 --- /dev/null +++ b/src/runtime/pkg/govmm/qemu/examples_test.go @@ -0,0 +1,85 @@ +/* +// Copyright contributors to the Virtual Machine Manager for Go project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ + +package qemu_test + +import ( + "time" + + "context" + + "github.com/kata-containers/govmm/qemu" +) + +func Example() { + params := make([]string, 0, 32) + + // Rootfs + params = append(params, "-drive", "file=/tmp/image.qcow2,if=virtio,aio=threads,format=qcow2") + // Network + params = append(params, "-net", "nic,model=virtio", "-net", "user") + // kvm + params = append(params, "-enable-kvm", "-cpu", "host") + // qmp socket + params = append(params, "-daemonize", "-qmp", "unix:/tmp/qmp-socket,server=on,wait=off") + // resources + params = append(params, "-m", "370", "-smp", "cpus=2") + + // LaunchCustomQemu should return as soon as the instance has launched as we + // are using the --daemonize flag. It will set up a unix domain socket + // called /tmp/qmp-socket that we can use to manage the instance. + _, err := qemu.LaunchCustomQemu(context.Background(), "", params, nil, nil, nil) + if err != nil { + panic(err) + } + + // This channel will be closed when the instance dies. + disconnectedCh := make(chan struct{}) + + // Set up our options. We don't want any logging or to receive any events. + cfg := qemu.QMPConfig{} + + // Start monitoring the qemu instance. This functon will block until we have + // connect to the QMP socket and received the welcome message. + q, _, err := qemu.QMPStart(context.Background(), "/tmp/qmp-socket", cfg, disconnectedCh) + if err != nil { + panic(err) + } + + // This has to be the first command executed in a QMP session. + err = q.ExecuteQMPCapabilities(context.Background()) + if err != nil { + panic(err) + } + + // Let's try to shutdown the VM. If it hasn't shutdown in 10 seconds we'll + // send a quit message. + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + err = q.ExecuteSystemPowerdown(ctx) + cancel() + if err != nil { + err = q.ExecuteQuit(context.Background()) + if err != nil { + panic(err) + } + } + + q.Shutdown() + + // disconnectedCh is closed when the VM exits. This line blocks until this + // event occurs. + <-disconnectedCh +} diff --git a/src/runtime/pkg/govmm/qemu/image.go b/src/runtime/pkg/govmm/qemu/image.go new file mode 100644 index 0000000000..ddee0670e1 --- /dev/null +++ b/src/runtime/pkg/govmm/qemu/image.go @@ -0,0 +1,77 @@ +/* +// Copyright contributors to the Virtual Machine Manager for Go project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ + +package qemu + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "syscall" +) + +// CreateCloudInitISO creates a cloud-init ConfigDrive ISO image. This is +// useful for configuring newly booted VMs. Before it can create the ISO +// image it needs to create a file tree with the various files that will +// make up the image. This directory is created under scratchDir and is +// deleted when when the function returns, successfully or otherwise. ctx is +// a context that can be used to timeout or cancel the image creation. +// isoPath contains the desired path of the ISO image to be created. The +// userdata and metadata parameters are byte slices that contain the +// ConfigDrive userdata and metadata that will be stored with the ISO image. +// The attrs parameter can be used to control aspects of the newly created +// qemu process, such as the user and group under which it runs. It may be nil. +func CreateCloudInitISO(ctx context.Context, scratchDir, isoPath string, + userData, metaData []byte, attr *syscall.SysProcAttr) error { + configDrivePath := path.Join(scratchDir, "clr-cloud-init") + dataDirPath := path.Join(configDrivePath, "openstack", "latest") + metaDataPath := path.Join(dataDirPath, "meta_data.json") + userDataPath := path.Join(dataDirPath, "user_data") + + defer func() { + /* #nosec */ + _ = os.RemoveAll(configDrivePath) + }() + + err := os.MkdirAll(dataDirPath, 0750) + if err != nil { + return fmt.Errorf("unable to create config drive directory %s : %v", + dataDirPath, err) + } + + err = ioutil.WriteFile(metaDataPath, metaData, 0644) + if err != nil { + return fmt.Errorf("unable to create %s : %v", metaDataPath, err) + } + + err = ioutil.WriteFile(userDataPath, userData, 0644) + if err != nil { + return fmt.Errorf("unable to create %s : %v", userDataPath, err) + } + + cmd := exec.CommandContext(ctx, "xorriso", "-as", "mkisofs", "-R", "-V", "config-2", + "-o", isoPath, configDrivePath) + cmd.SysProcAttr = attr + err = cmd.Run() + if err != nil { + return fmt.Errorf("unable to create cloudinit iso image %v", err) + } + + return nil +} diff --git a/src/runtime/pkg/govmm/qemu/qemu.go b/src/runtime/pkg/govmm/qemu/qemu.go new file mode 100644 index 0000000000..c43ff57955 --- /dev/null +++ b/src/runtime/pkg/govmm/qemu/qemu.go @@ -0,0 +1,3039 @@ +/* +// Copyright contributors to the Virtual Machine Manager for Go project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ + +// Package qemu provides methods and types for launching and managing QEMU +// instances. Instances can be launched with the LaunchQemu function and +// managed thereafter via QMPStart and the QMP object that this function +// returns. To manage a qemu instance after it has been launched you need +// to pass the -qmp option during launch requesting the qemu instance to create +// a QMP unix domain manageent socket, e.g., +// -qmp unix:/tmp/qmp-socket,server,nowait. For more information see the +// example below. +package qemu + +import ( + "bytes" + "fmt" + "log" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "syscall" + + "context" +) + +// Machine describes the machine type qemu will emulate. +type Machine struct { + // Type is the machine type to be used by qemu. + Type string + + // Acceleration are the machine acceleration options to be used by qemu. + Acceleration string + + // Options are options for the machine type + // For example gic-version=host and usb=off + Options string +} + +const ( + // MachineTypeMicrovm is the QEMU microvm machine type for amd64 + MachineTypeMicrovm string = "microvm" +) + +// Device is the qemu device interface. +type Device interface { + Valid() bool + QemuParams(config *Config) []string +} + +// DeviceDriver is the device driver string. +type DeviceDriver string + +const ( + // LegacySerial is the legacy serial device driver + LegacySerial DeviceDriver = "serial" + + // NVDIMM is the Non Volatile DIMM device driver. + NVDIMM DeviceDriver = "nvdimm" + + // VirtioNet is the virtio networking device driver. + VirtioNet DeviceDriver = "virtio-net" + + // VirtioNetPCI is the virt-io pci networking device driver. + VirtioNetPCI DeviceDriver = "virtio-net-pci" + + // VirtioNetCCW is the virt-io ccw networking device driver. + VirtioNetCCW DeviceDriver = "virtio-net-ccw" + + // VirtioBlock is the block device driver. + VirtioBlock DeviceDriver = "virtio-blk" + + // Console is the console device driver. + Console DeviceDriver = "virtconsole" + + // Virtio9P is the 9pfs device driver. + Virtio9P DeviceDriver = "virtio-9p" + + // VirtioSerial is the serial device driver. + VirtioSerial DeviceDriver = "virtio-serial" + + // VirtioSerialPort is the serial port device driver. + VirtioSerialPort DeviceDriver = "virtserialport" + + // VirtioRng is the paravirtualized RNG device driver. + VirtioRng DeviceDriver = "virtio-rng" + + // VirtioBalloon is the memory balloon device driver. + VirtioBalloon DeviceDriver = "virtio-balloon" + + //VhostUserSCSI represents a SCSI vhostuser device type. + VhostUserSCSI DeviceDriver = "vhost-user-scsi" + + //VhostUserNet represents a net vhostuser device type. + VhostUserNet DeviceDriver = "virtio-net" + + //VhostUserBlk represents a block vhostuser device type. + VhostUserBlk DeviceDriver = "vhost-user-blk" + + //VhostUserFS represents a virtio-fs vhostuser device type + VhostUserFS DeviceDriver = "vhost-user-fs" + + // PCIBridgeDriver represents a PCI bridge device type. + PCIBridgeDriver DeviceDriver = "pci-bridge" + + // PCIePCIBridgeDriver represents a PCIe to PCI bridge device type. + PCIePCIBridgeDriver DeviceDriver = "pcie-pci-bridge" + + // VfioPCI is the vfio driver with PCI transport. + VfioPCI DeviceDriver = "vfio-pci" + + // VfioCCW is the vfio driver with CCW transport. + VfioCCW DeviceDriver = "vfio-ccw" + + // VfioAP is the vfio driver with AP transport. + VfioAP DeviceDriver = "vfio-ap" + + // VHostVSockPCI is a generic Vsock vhost device with PCI transport. + VHostVSockPCI DeviceDriver = "vhost-vsock-pci" + + // PCIeRootPort is a PCIe Root Port, the PCIe device should be hotplugged to this port. + PCIeRootPort DeviceDriver = "pcie-root-port" + + // Loader is the Loader device driver. + Loader DeviceDriver = "loader" + + // SpaprTPMProxy is used for enabling guest to run in secure mode on ppc64le. + SpaprTPMProxy DeviceDriver = "spapr-tpm-proxy" +) + +func isDimmSupported(config *Config) bool { + switch runtime.GOARCH { + case "amd64", "386", "ppc64le", "arm64": + if config != nil && config.Machine.Type == MachineTypeMicrovm { + // microvm does not support NUMA + return false + } + return true + default: + return false + } +} + +// VirtioTransport is the transport in use for a virtio device. +type VirtioTransport string + +const ( + // TransportPCI is the PCI transport for virtio device. + TransportPCI VirtioTransport = "pci" + + // TransportCCW is the CCW transport for virtio devices. + TransportCCW VirtioTransport = "ccw" + + // TransportMMIO is the MMIO transport for virtio devices. + TransportMMIO VirtioTransport = "mmio" +) + +// defaultTransport returns the default transport for the current combination +// of host's architecture and QEMU machine type. +func (transport VirtioTransport) defaultTransport(config *Config) VirtioTransport { + switch runtime.GOARCH { + case "amd64", "386": + if config != nil && config.Machine.Type == MachineTypeMicrovm { + return TransportMMIO + } + return TransportPCI + case "s390x": + return TransportCCW + default: + return TransportPCI + } +} + +// isVirtioPCI returns true if the transport is PCI. +func (transport VirtioTransport) isVirtioPCI(config *Config) bool { + if transport == "" { + transport = transport.defaultTransport(config) + } + + return transport == TransportPCI +} + +// isVirtioCCW returns true if the transport is CCW. +func (transport VirtioTransport) isVirtioCCW(config *Config) bool { + if transport == "" { + transport = transport.defaultTransport(config) + } + + return transport == TransportCCW +} + +// getName returns the name of the current transport. +func (transport VirtioTransport) getName(config *Config) string { + if transport == "" { + transport = transport.defaultTransport(config) + } + + return string(transport) +} + +// disableModern returns the parameters with the disable-modern option. +// In case the device driver is not a PCI device and it doesn't have the option +// an empty string is returned. +func (transport VirtioTransport) disableModern(config *Config, disable bool) string { + if !transport.isVirtioPCI(config) { + return "" + } + + if disable { + return "disable-modern=true" + } + + return "disable-modern=false" +} + +// ObjectType is a string representing a qemu object type. +type ObjectType string + +const ( + // MemoryBackendFile represents a guest memory mapped file. + MemoryBackendFile ObjectType = "memory-backend-file" + + // MemoryBackendEPC represents a guest memory backend EPC for SGX. + MemoryBackendEPC ObjectType = "memory-backend-epc" + + // TDXGuest represents a TDX object + TDXGuest ObjectType = "tdx-guest" + + // SEVGuest represents an SEV guest object + SEVGuest ObjectType = "sev-guest" + + // SecExecGuest represents an s390x Secure Execution (Protected Virtualization in QEMU) object + SecExecGuest ObjectType = "s390-pv-guest" + // PEFGuest represent ppc64le PEF(Protected Execution Facility) object. + PEFGuest ObjectType = "pef-guest" +) + +// Object is a qemu object representation. +type Object struct { + // Driver is the qemu device driver + Driver DeviceDriver + + // Type is the qemu object type. + Type ObjectType + + // ID is the user defined object ID. + ID string + + // DeviceID is the user defined device ID. + DeviceID string + + // MemPath is the object's memory path. + // This is only relevant for memory objects + MemPath string + + // Size is the object size in bytes + Size uint64 + + // Debug this is a debug object + Debug bool + + // File is the device file + File string + + // CBitPos is the location of the C-bit in a guest page table entry + // This is only relevant for sev-guest objects + CBitPos uint32 + + // ReducedPhysBits is the reduction in the guest physical address space + // This is only relevant for sev-guest objects + ReducedPhysBits uint32 + + // ReadOnly specifies whether `MemPath` is opened read-only or read/write (default) + ReadOnly bool + + // Prealloc enables memory preallocation + Prealloc bool +} + +// Valid returns true if the Object structure is valid and complete. +func (object Object) Valid() bool { + switch object.Type { + case MemoryBackendFile: + return object.ID != "" && object.MemPath != "" && object.Size != 0 + case MemoryBackendEPC: + return object.ID != "" && object.Size != 0 + case TDXGuest: + return object.ID != "" && object.File != "" && object.DeviceID != "" + case SEVGuest: + return object.ID != "" && object.File != "" && object.CBitPos != 0 && object.ReducedPhysBits != 0 + case SecExecGuest: + return object.ID != "" + case PEFGuest: + return object.ID != "" && object.File != "" + + default: + return false + } +} + +// QemuParams returns the qemu parameters built out of this Object device. +func (object Object) QemuParams(config *Config) []string { + var objectParams []string + var deviceParams []string + var driveParams []string + var qemuParams []string + + switch object.Type { + case MemoryBackendFile: + objectParams = append(objectParams, string(object.Type)) + objectParams = append(objectParams, fmt.Sprintf("id=%s", object.ID)) + objectParams = append(objectParams, fmt.Sprintf("mem-path=%s", object.MemPath)) + objectParams = append(objectParams, fmt.Sprintf("size=%d", object.Size)) + + deviceParams = append(deviceParams, string(object.Driver)) + deviceParams = append(deviceParams, fmt.Sprintf("id=%s", object.DeviceID)) + deviceParams = append(deviceParams, fmt.Sprintf("memdev=%s", object.ID)) + + if object.ReadOnly { + objectParams = append(objectParams, "readonly=on") + deviceParams = append(deviceParams, "unarmed=on") + } + case MemoryBackendEPC: + objectParams = append(objectParams, string(object.Type)) + objectParams = append(objectParams, fmt.Sprintf("id=%s", object.ID)) + objectParams = append(objectParams, fmt.Sprintf("size=%d", object.Size)) + if object.Prealloc { + objectParams = append(objectParams, "prealloc=on") + } + + case TDXGuest: + objectParams = append(objectParams, string(object.Type)) + objectParams = append(objectParams, fmt.Sprintf("id=%s", object.ID)) + if object.Debug { + objectParams = append(objectParams, "debug=on") + } + deviceParams = append(deviceParams, string(object.Driver)) + deviceParams = append(deviceParams, fmt.Sprintf("id=%s", object.DeviceID)) + deviceParams = append(deviceParams, fmt.Sprintf("file=%s", object.File)) + case SEVGuest: + objectParams = append(objectParams, string(object.Type)) + objectParams = append(objectParams, fmt.Sprintf("id=%s", object.ID)) + objectParams = append(objectParams, fmt.Sprintf("cbitpos=%d", object.CBitPos)) + objectParams = append(objectParams, fmt.Sprintf("reduced-phys-bits=%d", object.ReducedPhysBits)) + + driveParams = append(driveParams, "if=pflash,format=raw,readonly=on") + driveParams = append(driveParams, fmt.Sprintf("file=%s", object.File)) + case SecExecGuest: + objectParams = append(objectParams, string(object.Type)) + objectParams = append(objectParams, fmt.Sprintf("id=%s", object.ID)) + case PEFGuest: + objectParams = append(objectParams, string(object.Type)) + objectParams = append(objectParams, fmt.Sprintf("id=%s", object.ID)) + + deviceParams = append(deviceParams, string(object.Driver)) + deviceParams = append(deviceParams, fmt.Sprintf("id=%s", object.DeviceID)) + deviceParams = append(deviceParams, fmt.Sprintf("host-path=%s", object.File)) + + } + + if len(deviceParams) > 0 { + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + } + + if len(objectParams) > 0 { + qemuParams = append(qemuParams, "-object") + qemuParams = append(qemuParams, strings.Join(objectParams, ",")) + } + + if len(driveParams) > 0 { + qemuParams = append(qemuParams, "-drive") + qemuParams = append(qemuParams, strings.Join(driveParams, ",")) + } + + return qemuParams +} + +// Virtio9PMultidev filesystem behaviour to deal +// with multiple devices being shared with a 9p export. +type Virtio9PMultidev string + +const ( + // Remap shares multiple devices with only one export. + Remap Virtio9PMultidev = "remap" + + // Warn assumes that only one device is shared by the same export. + // Only a warning message is logged (once) by qemu on host side. + // This is the default behaviour. + Warn Virtio9PMultidev = "warn" + + // Forbid like "warn" but also deny access to additional devices on guest. + Forbid Virtio9PMultidev = "forbid" +) + +// FSDriver represents a qemu filesystem driver. +type FSDriver string + +// SecurityModelType is a qemu filesystem security model type. +type SecurityModelType string + +const ( + // Local is the local qemu filesystem driver. + Local FSDriver = "local" + + // Handle is the handle qemu filesystem driver. + Handle FSDriver = "handle" + + // Proxy is the proxy qemu filesystem driver. + Proxy FSDriver = "proxy" +) + +const ( + // None is like passthrough without failure reports. + None SecurityModelType = "none" + + // PassThrough uses the same credentials on both the host and guest. + PassThrough SecurityModelType = "passthrough" + + // MappedXattr stores some files attributes as extended attributes. + MappedXattr SecurityModelType = "mapped-xattr" + + // MappedFile stores some files attributes in the .virtfs directory. + MappedFile SecurityModelType = "mapped-file" +) + +// FSDevice represents a qemu filesystem configuration. +type FSDevice struct { + // Driver is the qemu device driver + Driver DeviceDriver + + // FSDriver is the filesystem driver backend. + FSDriver FSDriver + + // ID is the filesystem identifier. + ID string + + // Path is the host root path for this filesystem. + Path string + + // MountTag is the device filesystem mount point tag. + MountTag string + + // SecurityModel is the security model for this filesystem device. + SecurityModel SecurityModelType + + // DisableModern prevents qemu from relying on fast MMIO. + DisableModern bool + + // ROMFile specifies the ROM file being used for this device. + ROMFile string + + // DevNo identifies the ccw devices for s390x architecture + DevNo string + + // Transport is the virtio transport for this device. + Transport VirtioTransport + + // Multidev is the filesystem behaviour to deal + // with multiple devices being shared with a 9p export + Multidev Virtio9PMultidev +} + +// Virtio9PTransport is a map of the virtio-9p device name that corresponds +// to each transport. +var Virtio9PTransport = map[VirtioTransport]string{ + TransportPCI: "virtio-9p-pci", + TransportCCW: "virtio-9p-ccw", + TransportMMIO: "virtio-9p-device", +} + +// Valid returns true if the FSDevice structure is valid and complete. +func (fsdev FSDevice) Valid() bool { + if fsdev.ID == "" || fsdev.Path == "" || fsdev.MountTag == "" { + return false + } + + return true +} + +// QemuParams returns the qemu parameters built out of this filesystem device. +func (fsdev FSDevice) QemuParams(config *Config) []string { + var fsParams []string + var deviceParams []string + var qemuParams []string + + deviceParams = append(deviceParams, fsdev.deviceName(config)) + if s := fsdev.Transport.disableModern(config, fsdev.DisableModern); s != "" { + deviceParams = append(deviceParams, s) + } + deviceParams = append(deviceParams, fmt.Sprintf("fsdev=%s", fsdev.ID)) + deviceParams = append(deviceParams, fmt.Sprintf("mount_tag=%s", fsdev.MountTag)) + if fsdev.Transport.isVirtioPCI(config) && fsdev.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", fsdev.ROMFile)) + } + if fsdev.Transport.isVirtioCCW(config) { + if config.Knobs.IOMMUPlatform { + deviceParams = append(deviceParams, ",iommu_platform=on") + } + deviceParams = append(deviceParams, fmt.Sprintf("devno=%s", fsdev.DevNo)) + } + + fsParams = append(fsParams, string(fsdev.FSDriver)) + fsParams = append(fsParams, fmt.Sprintf("id=%s", fsdev.ID)) + fsParams = append(fsParams, fmt.Sprintf("path=%s", fsdev.Path)) + fsParams = append(fsParams, fmt.Sprintf("security_model=%s", fsdev.SecurityModel)) + + if fsdev.Multidev != "" { + fsParams = append(fsParams, fmt.Sprintf("multidevs=%s", fsdev.Multidev)) + } + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + qemuParams = append(qemuParams, "-fsdev") + qemuParams = append(qemuParams, strings.Join(fsParams, ",")) + + return qemuParams +} + +// deviceName returns the QEMU shared filesystem device name for the current +// combination of driver and transport. +func (fsdev FSDevice) deviceName(config *Config) string { + if fsdev.Transport == "" { + fsdev.Transport = fsdev.Transport.defaultTransport(config) + } + + switch fsdev.Driver { + case Virtio9P: + return Virtio9PTransport[fsdev.Transport] + } + + return string(fsdev.Driver) +} + +// CharDeviceBackend is the character device backend for qemu +type CharDeviceBackend string + +const ( + // Pipe creates a 2 way connection to the guest. + Pipe CharDeviceBackend = "pipe" + + // Socket creates a 2 way stream socket (TCP or Unix). + Socket CharDeviceBackend = "socket" + + // CharConsole sends traffic from the guest to QEMU's standard output. + CharConsole CharDeviceBackend = "console" + + // Serial sends traffic from the guest to a serial device on the host. + Serial CharDeviceBackend = "serial" + + // TTY is an alias for Serial. + TTY CharDeviceBackend = "tty" + + // PTY creates a new pseudo-terminal on the host and connect to it. + PTY CharDeviceBackend = "pty" + + // File sends traffic from the guest to a file on the host. + File CharDeviceBackend = "file" +) + +// CharDevice represents a qemu character device. +type CharDevice struct { + Backend CharDeviceBackend + + // Driver is the qemu device driver + Driver DeviceDriver + + // Bus is the serial bus associated to this device. + Bus string + + // DeviceID is the user defined device ID. + DeviceID string + + ID string + Path string + Name string + + // DisableModern prevents qemu from relying on fast MMIO. + DisableModern bool + + // ROMFile specifies the ROM file being used for this device. + ROMFile string + + // DevNo identifies the ccw devices for s390x architecture + DevNo string + + // Transport is the virtio transport for this device. + Transport VirtioTransport +} + +// VirtioSerialTransport is a map of the virtio-serial device name that +// corresponds to each transport. +var VirtioSerialTransport = map[VirtioTransport]string{ + TransportPCI: "virtio-serial-pci", + TransportCCW: "virtio-serial-ccw", + TransportMMIO: "virtio-serial-device", +} + +// Valid returns true if the CharDevice structure is valid and complete. +func (cdev CharDevice) Valid() bool { + if cdev.ID == "" || cdev.Path == "" { + return false + } + + return true +} + +// QemuParams returns the qemu parameters built out of this character device. +func (cdev CharDevice) QemuParams(config *Config) []string { + var cdevParams []string + var deviceParams []string + var qemuParams []string + + deviceParams = append(deviceParams, cdev.deviceName(config)) + if cdev.Driver == VirtioSerial { + if s := cdev.Transport.disableModern(config, cdev.DisableModern); s != "" { + deviceParams = append(deviceParams, s) + } + } + if cdev.Bus != "" { + deviceParams = append(deviceParams, fmt.Sprintf("bus=%s", cdev.Bus)) + } + deviceParams = append(deviceParams, fmt.Sprintf("chardev=%s", cdev.ID)) + deviceParams = append(deviceParams, fmt.Sprintf("id=%s", cdev.DeviceID)) + if cdev.Name != "" { + deviceParams = append(deviceParams, fmt.Sprintf("name=%s", cdev.Name)) + } + if cdev.Driver == VirtioSerial && cdev.Transport.isVirtioPCI(config) && cdev.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", cdev.ROMFile)) + } + + if cdev.Driver == VirtioSerial && cdev.Transport.isVirtioCCW(config) { + if config.Knobs.IOMMUPlatform { + deviceParams = append(deviceParams, "iommu_platform=on") + } + deviceParams = append(deviceParams, fmt.Sprintf("devno=%s", cdev.DevNo)) + } + + cdevParams = append(cdevParams, string(cdev.Backend)) + cdevParams = append(cdevParams, fmt.Sprintf("id=%s", cdev.ID)) + if cdev.Backend == Socket { + cdevParams = append(cdevParams, fmt.Sprintf("path=%s,server=on,wait=off", cdev.Path)) + } else { + cdevParams = append(cdevParams, fmt.Sprintf("path=%s", cdev.Path)) + } + + // Legacy serial is special. It does not follow the device + driver model + if cdev.Driver != LegacySerial { + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + } + + qemuParams = append(qemuParams, "-chardev") + qemuParams = append(qemuParams, strings.Join(cdevParams, ",")) + + return qemuParams +} + +// deviceName returns the QEMU device name for the current combination of +// driver and transport. +func (cdev CharDevice) deviceName(config *Config) string { + if cdev.Transport == "" { + cdev.Transport = cdev.Transport.defaultTransport(config) + } + + switch cdev.Driver { + case VirtioSerial: + return VirtioSerialTransport[cdev.Transport] + } + + return string(cdev.Driver) +} + +// NetDeviceType is a qemu networking device type. +type NetDeviceType string + +const ( + // TAP is a TAP networking device type. + TAP NetDeviceType = "tap" + + // MACVTAP is a macvtap networking device type. + MACVTAP NetDeviceType = "macvtap" + + // IPVTAP is a ipvtap virtual networking device type. + IPVTAP NetDeviceType = "ipvtap" + + // VETHTAP is a veth-tap virtual networking device type. + VETHTAP NetDeviceType = "vethtap" + + // VFIO is a direct assigned PCI device or PCI VF + VFIO NetDeviceType = "VFIO" + + // VHOSTUSER is a vhost-user port (socket) + VHOSTUSER NetDeviceType = "vhostuser" +) + +// QemuNetdevParam converts to the QEMU -netdev parameter notation +func (n NetDeviceType) QemuNetdevParam(netdev *NetDevice, config *Config) string { + if netdev.Transport == "" { + netdev.Transport = netdev.Transport.defaultTransport(config) + } + + switch n { + case TAP: + return "tap" + case MACVTAP: + return "tap" + case IPVTAP: + return "tap" + case VETHTAP: + return "tap" // -netdev type=tap -device virtio-net-pci + case VFIO: + if netdev.Transport == TransportMMIO { + log.Fatal("vfio devices are not support with the MMIO transport") + } + return "" // -device vfio-pci (no netdev) + case VHOSTUSER: + if netdev.Transport == TransportCCW { + log.Fatal("vhost-user devices are not supported on IBM Z") + } + return "vhost-user" // -netdev type=vhost-user (no device) + default: + return "" + + } +} + +// QemuDeviceParam converts to the QEMU -device parameter notation +func (n NetDeviceType) QemuDeviceParam(netdev *NetDevice, config *Config) DeviceDriver { + if netdev.Transport == "" { + netdev.Transport = netdev.Transport.defaultTransport(config) + } + + var device string + + switch n { + case TAP: + device = "virtio-net" + case MACVTAP: + device = "virtio-net" + case IPVTAP: + device = "virtio-net" + case VETHTAP: + device = "virtio-net" // -netdev type=tap -device virtio-net-pci + case VFIO: + if netdev.Transport == TransportMMIO { + log.Fatal("vfio devices are not support with the MMIO transport") + } + device = "vfio" // -device vfio-pci (no netdev) + case VHOSTUSER: + if netdev.Transport == TransportCCW { + log.Fatal("vhost-user devices are not supported on IBM Z") + } + return "" // -netdev type=vhost-user (no device) + default: + return "" + } + + switch netdev.Transport { + case TransportPCI: + return DeviceDriver(device + "-pci") + case TransportCCW: + return DeviceDriver(device + "-ccw") + case TransportMMIO: + return DeviceDriver(device + "-device") + default: + return "" + } +} + +// NetDevice represents a guest networking device +type NetDevice struct { + // Type is the netdev type (e.g. tap). + Type NetDeviceType + + // Driver is the qemu device driver + Driver DeviceDriver + + // ID is the netdevice identifier. + ID string + + // IfName is the interface name, + IFName string + + // Bus is the bus path name of a PCI device. + Bus string + + // Addr is the address offset of a PCI device. + Addr string + + // DownScript is the tap interface deconfiguration script. + DownScript string + + // Script is the tap interface configuration script. + Script string + + // FDs represents the list of already existing file descriptors to be used. + // This is mostly useful for mq support. + FDs []*os.File + VhostFDs []*os.File + + // VHost enables virtio device emulation from the host kernel instead of from qemu. + VHost bool + + // MACAddress is the networking device interface MAC address. + MACAddress string + + // DisableModern prevents qemu from relying on fast MMIO. + DisableModern bool + + // ROMFile specifies the ROM file being used for this device. + ROMFile string + + // DevNo identifies the ccw devices for s390x architecture + DevNo string + + // Transport is the virtio transport for this device. + Transport VirtioTransport +} + +// VirtioNetTransport is a map of the virtio-net device name that corresponds +// to each transport. +var VirtioNetTransport = map[VirtioTransport]string{ + TransportPCI: "virtio-net-pci", + TransportCCW: "virtio-net-ccw", + TransportMMIO: "virtio-net-device", +} + +// Valid returns true if the NetDevice structure is valid and complete. +func (netdev NetDevice) Valid() bool { + if netdev.ID == "" || netdev.IFName == "" { + return false + } + + switch netdev.Type { + case TAP: + return true + case MACVTAP: + return true + default: + return false + } +} + +// mqParameter returns the parameters for multi-queue driver. If the driver is a PCI device then the +// vector flag is required. If the driver is a CCW type than the vector flag is not implemented and only +// multi-queue option mq needs to be activated. See comment in libvirt code at +// https://github.com/libvirt/libvirt/blob/6e7e965dcd3d885739129b1454ce19e819b54c25/src/qemu/qemu_command.c#L3633 +func (netdev NetDevice) mqParameter(config *Config) string { + p := []string{"mq=on"} + + if netdev.Transport.isVirtioPCI(config) { + // https://www.linux-kvm.org/page/Multiqueue + // -netdev tap,vhost=on,queues=N + // enable mq and specify msix vectors in qemu cmdline + // (2N+2 vectors, N for tx queues, N for rx queues, 1 for config, and one for possible control vq) + // -device virtio-net-pci,mq=on,vectors=2N+2... + // enable mq in guest by 'ethtool -L eth0 combined $queue_num' + // Clearlinux automatically sets up the queues properly + // The agent implementation should do this to ensure that it is + // always set + vectors := len(netdev.FDs)*2 + 2 + p = append(p, fmt.Sprintf("vectors=%d", vectors)) + } + + return strings.Join(p, ",") +} + +// QemuDeviceParams returns the -device parameters for this network device +func (netdev NetDevice) QemuDeviceParams(config *Config) []string { + var deviceParams []string + + driver := netdev.Type.QemuDeviceParam(&netdev, config) + if driver == "" { + return nil + } + + deviceParams = append(deviceParams, fmt.Sprintf("driver=%s", driver)) + deviceParams = append(deviceParams, fmt.Sprintf("netdev=%s", netdev.ID)) + deviceParams = append(deviceParams, fmt.Sprintf("mac=%s", netdev.MACAddress)) + + if netdev.Bus != "" { + deviceParams = append(deviceParams, fmt.Sprintf("bus=%s", netdev.Bus)) + } + + if netdev.Addr != "" { + addr, err := strconv.Atoi(netdev.Addr) + if err == nil && addr >= 0 { + deviceParams = append(deviceParams, fmt.Sprintf("addr=%x", addr)) + } + } + if s := netdev.Transport.disableModern(config, netdev.DisableModern); s != "" { + deviceParams = append(deviceParams, s) + } + + if len(netdev.FDs) > 0 { + // Note: We are appending to the device params here + deviceParams = append(deviceParams, netdev.mqParameter(config)) + } + + if netdev.Transport.isVirtioPCI(config) && netdev.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", netdev.ROMFile)) + } + + if netdev.Transport.isVirtioCCW(config) { + if config.Knobs.IOMMUPlatform { + deviceParams = append(deviceParams, "iommu_platform=on") + } + deviceParams = append(deviceParams, fmt.Sprintf("devno=%s", netdev.DevNo)) + } + + return deviceParams +} + +// QemuNetdevParams returns the -netdev parameters for this network device +func (netdev NetDevice) QemuNetdevParams(config *Config) []string { + var netdevParams []string + + netdevType := netdev.Type.QemuNetdevParam(&netdev, config) + if netdevType == "" { + return nil + } + + netdevParams = append(netdevParams, netdevType) + netdevParams = append(netdevParams, fmt.Sprintf("id=%s", netdev.ID)) + + if netdev.VHost { + netdevParams = append(netdevParams, "vhost=on") + if len(netdev.VhostFDs) > 0 { + var fdParams []string + qemuFDs := config.appendFDs(netdev.VhostFDs) + for _, fd := range qemuFDs { + fdParams = append(fdParams, fmt.Sprintf("%d", fd)) + } + netdevParams = append(netdevParams, fmt.Sprintf("vhostfds=%s", strings.Join(fdParams, ":"))) + } + } + + if len(netdev.FDs) > 0 { + var fdParams []string + + qemuFDs := config.appendFDs(netdev.FDs) + for _, fd := range qemuFDs { + fdParams = append(fdParams, fmt.Sprintf("%d", fd)) + } + + netdevParams = append(netdevParams, fmt.Sprintf("fds=%s", strings.Join(fdParams, ":"))) + + } else { + netdevParams = append(netdevParams, fmt.Sprintf("ifname=%s", netdev.IFName)) + if netdev.DownScript != "" { + netdevParams = append(netdevParams, fmt.Sprintf("downscript=%s", netdev.DownScript)) + } + if netdev.Script != "" { + netdevParams = append(netdevParams, fmt.Sprintf("script=%s", netdev.Script)) + } + } + return netdevParams +} + +// QemuParams returns the qemu parameters built out of this network device. +func (netdev NetDevice) QemuParams(config *Config) []string { + var netdevParams []string + var deviceParams []string + var qemuParams []string + + // Macvtap can only be connected via fds + if (netdev.Type == MACVTAP) && (len(netdev.FDs) == 0) { + return nil // implicit error + } + + if netdev.Type.QemuNetdevParam(&netdev, config) != "" { + netdevParams = netdev.QemuNetdevParams(config) + if netdevParams != nil { + qemuParams = append(qemuParams, "-netdev") + qemuParams = append(qemuParams, strings.Join(netdevParams, ",")) + } + } + + if netdev.Type.QemuDeviceParam(&netdev, config) != "" { + deviceParams = netdev.QemuDeviceParams(config) + if deviceParams != nil { + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + } + } + + return qemuParams +} + +// LegacySerialDevice represents a qemu legacy serial device. +type LegacySerialDevice struct { + // ID is the serial device identifier. + // This maps to the char dev associated with the device + // as serial does not have a notion of id + // e.g: + // -chardev stdio,id=char0,mux=on,logfile=serial.log,signal=off -serial chardev:char0 + // -chardev file,id=char0,path=serial.log -serial chardev:char0 + Chardev string +} + +// Valid returns true if the LegacySerialDevice structure is valid and complete. +func (dev LegacySerialDevice) Valid() bool { + return dev.Chardev != "" +} + +// QemuParams returns the qemu parameters built out of this serial device. +func (dev LegacySerialDevice) QemuParams(config *Config) []string { + var deviceParam string + var qemuParams []string + + deviceParam = fmt.Sprintf("chardev:%s", dev.Chardev) + + qemuParams = append(qemuParams, "-serial") + qemuParams = append(qemuParams, deviceParam) + + return qemuParams +} + +/* Not used currently +// deviceName returns the QEMU device name for the current combination of +// driver and transport. +func (dev LegacySerialDevice) deviceName(config *Config) string { + return dev.Chardev +} +*/ + +// SerialDevice represents a qemu serial device. +type SerialDevice struct { + // Driver is the qemu device driver + Driver DeviceDriver + + // ID is the serial device identifier. + ID string + + // DisableModern prevents qemu from relying on fast MMIO. + DisableModern bool + + // ROMFile specifies the ROM file being used for this device. + ROMFile string + + // DevNo identifies the ccw devices for s390x architecture + DevNo string + + // Transport is the virtio transport for this device. + Transport VirtioTransport + + // MaxPorts is the maximum number of ports for this device. + MaxPorts uint +} + +// Valid returns true if the SerialDevice structure is valid and complete. +func (dev SerialDevice) Valid() bool { + if dev.Driver == "" || dev.ID == "" { + return false + } + + return true +} + +// QemuParams returns the qemu parameters built out of this serial device. +func (dev SerialDevice) QemuParams(config *Config) []string { + var deviceParams []string + var qemuParams []string + + deviceParams = append(deviceParams, dev.deviceName(config)) + if s := dev.Transport.disableModern(config, dev.DisableModern); s != "" { + deviceParams = append(deviceParams, s) + } + deviceParams = append(deviceParams, fmt.Sprintf("id=%s", dev.ID)) + if dev.Transport.isVirtioPCI(config) && dev.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", dev.ROMFile)) + if dev.Driver == VirtioSerial && dev.MaxPorts != 0 { + deviceParams = append(deviceParams, fmt.Sprintf("max_ports=%d", dev.MaxPorts)) + } + } + + if dev.Transport.isVirtioCCW(config) { + if config.Knobs.IOMMUPlatform { + deviceParams = append(deviceParams, "iommu_platform=on") + } + deviceParams = append(deviceParams, fmt.Sprintf("devno=%s", dev.DevNo)) + } + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + return qemuParams +} + +// deviceName returns the QEMU device name for the current combination of +// driver and transport. +func (dev SerialDevice) deviceName(config *Config) string { + if dev.Transport == "" { + dev.Transport = dev.Transport.defaultTransport(config) + } + + switch dev.Driver { + case VirtioSerial: + return VirtioSerialTransport[dev.Transport] + } + + return string(dev.Driver) +} + +// BlockDeviceInterface defines the type of interface the device is connected to. +type BlockDeviceInterface string + +// BlockDeviceAIO defines the type of asynchronous I/O the block device should use. +type BlockDeviceAIO string + +// BlockDeviceFormat defines the image format used on a block device. +type BlockDeviceFormat string + +const ( + // NoInterface for block devices with no interfaces. + NoInterface BlockDeviceInterface = "none" + + // SCSI represents a SCSI block device interface. + SCSI BlockDeviceInterface = "scsi" +) + +const ( + // Threads is the pthread asynchronous I/O implementation. + Threads BlockDeviceAIO = "threads" + + // Native is the pthread asynchronous I/O implementation. + Native BlockDeviceAIO = "native" +) + +const ( + // QCOW2 is the Qemu Copy On Write v2 image format. + QCOW2 BlockDeviceFormat = "qcow2" +) + +// BlockDevice represents a qemu block device. +type BlockDevice struct { + Driver DeviceDriver + ID string + File string + Interface BlockDeviceInterface + AIO BlockDeviceAIO + Format BlockDeviceFormat + SCSI bool + WCE bool + + // DisableModern prevents qemu from relying on fast MMIO. + DisableModern bool + + // ROMFile specifies the ROM file being used for this device. + ROMFile string + + // DevNo identifies the ccw devices for s390x architecture + DevNo string + + // ShareRW enables multiple qemu instances to share the File + ShareRW bool + + // ReadOnly sets the block device in readonly mode + ReadOnly bool + + // Transport is the virtio transport for this device. + Transport VirtioTransport +} + +// VirtioBlockTransport is a map of the virtio-blk device name that corresponds +// to each transport. +var VirtioBlockTransport = map[VirtioTransport]string{ + TransportPCI: "virtio-blk-pci", + TransportCCW: "virtio-blk-ccw", + TransportMMIO: "virtio-blk-device", +} + +// Valid returns true if the BlockDevice structure is valid and complete. +func (blkdev BlockDevice) Valid() bool { + if blkdev.Driver == "" || blkdev.ID == "" || blkdev.File == "" { + return false + } + + return true +} + +// QemuParams returns the qemu parameters built out of this block device. +func (blkdev BlockDevice) QemuParams(config *Config) []string { + var blkParams []string + var deviceParams []string + var qemuParams []string + + deviceParams = append(deviceParams, blkdev.deviceName(config)) + if s := blkdev.Transport.disableModern(config, blkdev.DisableModern); s != "" { + deviceParams = append(deviceParams, s) + } + deviceParams = append(deviceParams, fmt.Sprintf("drive=%s", blkdev.ID)) + if !blkdev.SCSI { + deviceParams = append(deviceParams, "scsi=off") + } + + if !blkdev.WCE { + deviceParams = append(deviceParams, "config-wce=off") + } + + if blkdev.Transport.isVirtioPCI(config) && blkdev.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", blkdev.ROMFile)) + } + + if blkdev.Transport.isVirtioCCW(config) { + deviceParams = append(deviceParams, fmt.Sprintf("devno=%s", blkdev.DevNo)) + } + + if blkdev.ShareRW { + deviceParams = append(deviceParams, "share-rw=on") + } + + deviceParams = append(deviceParams, fmt.Sprintf("serial=%s", blkdev.ID)) + + blkParams = append(blkParams, fmt.Sprintf("id=%s", blkdev.ID)) + blkParams = append(blkParams, fmt.Sprintf("file=%s", blkdev.File)) + blkParams = append(blkParams, fmt.Sprintf("aio=%s", blkdev.AIO)) + blkParams = append(blkParams, fmt.Sprintf("format=%s", blkdev.Format)) + blkParams = append(blkParams, fmt.Sprintf("if=%s", blkdev.Interface)) + + if blkdev.ReadOnly { + blkParams = append(blkParams, "readonly=on") + } + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + qemuParams = append(qemuParams, "-drive") + qemuParams = append(qemuParams, strings.Join(blkParams, ",")) + + return qemuParams +} + +// deviceName returns the QEMU device name for the current combination of +// driver and transport. +func (blkdev BlockDevice) deviceName(config *Config) string { + if blkdev.Transport == "" { + blkdev.Transport = blkdev.Transport.defaultTransport(config) + } + + switch blkdev.Driver { + case VirtioBlock: + return VirtioBlockTransport[blkdev.Transport] + } + + return string(blkdev.Driver) +} + +// PVPanicDevice represents a qemu pvpanic device. +type PVPanicDevice struct { + NoShutdown bool +} + +// Valid always returns true for pvpanic device +func (dev PVPanicDevice) Valid() bool { + return true +} + +// QemuParams returns the qemu parameters built out of this serial device. +func (dev PVPanicDevice) QemuParams(config *Config) []string { + if dev.NoShutdown { + return []string{"-device", "pvpanic", "-no-shutdown"} + } + return []string{"-device", "pvpanic"} +} + +// LoaderDevice represents a qemu loader device. +type LoaderDevice struct { + File string + ID string +} + +// Valid returns true if there is a valid structure defined for LoaderDevice +func (dev LoaderDevice) Valid() bool { + if dev.File == "" { + return false + } + + if dev.ID == "" { + return false + } + + return true +} + +// QemuParams returns the qemu parameters built out of this loader device. +func (dev LoaderDevice) QemuParams(config *Config) []string { + var qemuParams []string + var deviceParams []string + + deviceParams = append(deviceParams, "loader") + deviceParams = append(deviceParams, fmt.Sprintf("file=%s", dev.File)) + deviceParams = append(deviceParams, fmt.Sprintf("id=%s", dev.ID)) + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + return qemuParams +} + +// VhostUserDevice represents a qemu vhost-user device meant to be passed +// in to the guest +type VhostUserDevice struct { + SocketPath string //path to vhostuser socket on host + CharDevID string + TypeDevID string //variable QEMU parameter based on value of VhostUserType + Address string //used for MAC address in net case + Tag string //virtio-fs volume id for mounting inside guest + CacheSize uint32 //virtio-fs DAX cache size in MiB + SharedVersions bool //enable virtio-fs shared version metadata + VhostUserType DeviceDriver + + // ROMFile specifies the ROM file being used for this device. + ROMFile string + + // DevNo identifies the CCW device for s390x. + DevNo string + + // Transport is the virtio transport for this device. + Transport VirtioTransport +} + +// VhostUserNetTransport is a map of the virtio-net device name that +// corresponds to each transport. +var VhostUserNetTransport = map[VirtioTransport]string{ + TransportPCI: "virtio-net-pci", + TransportCCW: "virtio-net-ccw", + TransportMMIO: "virtio-net-device", +} + +// VhostUserSCSITransport is a map of the vhost-user-scsi device name that +// corresponds to each transport. +var VhostUserSCSITransport = map[VirtioTransport]string{ + TransportPCI: "vhost-user-scsi-pci", + TransportCCW: "vhost-user-scsi-ccw", + TransportMMIO: "vhost-user-scsi-device", +} + +// VhostUserBlkTransport is a map of the vhost-user-blk device name that +// corresponds to each transport. +var VhostUserBlkTransport = map[VirtioTransport]string{ + TransportPCI: "vhost-user-blk-pci", + TransportCCW: "vhost-user-blk-ccw", + TransportMMIO: "vhost-user-blk-device", +} + +// VhostUserFSTransport is a map of the vhost-user-fs device name that +// corresponds to each transport. +var VhostUserFSTransport = map[VirtioTransport]string{ + TransportPCI: "vhost-user-fs-pci", + TransportCCW: "vhost-user-fs-ccw", + TransportMMIO: "vhost-user-fs-device", +} + +// Valid returns true if there is a valid structure defined for VhostUserDevice +func (vhostuserDev VhostUserDevice) Valid() bool { + + if vhostuserDev.SocketPath == "" || vhostuserDev.CharDevID == "" { + return false + } + + switch vhostuserDev.VhostUserType { + case VhostUserNet: + if vhostuserDev.TypeDevID == "" || vhostuserDev.Address == "" { + return false + } + case VhostUserSCSI: + if vhostuserDev.TypeDevID == "" { + return false + } + case VhostUserBlk: + case VhostUserFS: + if vhostuserDev.Tag == "" { + return false + } + default: + return false + } + + return true +} + +// QemuNetParams builds QEMU netdev and device parameters for a VhostUserNet device +func (vhostuserDev VhostUserDevice) QemuNetParams(config *Config) []string { + var qemuParams []string + var netParams []string + var deviceParams []string + + driver := vhostuserDev.deviceName(config) + if driver == "" { + return nil + } + + netParams = append(netParams, "type=vhost-user") + netParams = append(netParams, fmt.Sprintf("id=%s", vhostuserDev.TypeDevID)) + netParams = append(netParams, fmt.Sprintf("chardev=%s", vhostuserDev.CharDevID)) + netParams = append(netParams, "vhostforce") + + deviceParams = append(deviceParams, driver) + deviceParams = append(deviceParams, fmt.Sprintf("netdev=%s", vhostuserDev.TypeDevID)) + deviceParams = append(deviceParams, fmt.Sprintf("mac=%s", vhostuserDev.Address)) + + if vhostuserDev.Transport.isVirtioPCI(config) && vhostuserDev.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", vhostuserDev.ROMFile)) + } + + qemuParams = append(qemuParams, "-netdev") + qemuParams = append(qemuParams, strings.Join(netParams, ",")) + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + return qemuParams +} + +// QemuSCSIParams builds QEMU device parameters for a VhostUserSCSI device +func (vhostuserDev VhostUserDevice) QemuSCSIParams(config *Config) []string { + var qemuParams []string + var deviceParams []string + + driver := vhostuserDev.deviceName(config) + if driver == "" { + return nil + } + + deviceParams = append(deviceParams, driver) + deviceParams = append(deviceParams, fmt.Sprintf("id=%s", vhostuserDev.TypeDevID)) + deviceParams = append(deviceParams, fmt.Sprintf("chardev=%s", vhostuserDev.CharDevID)) + + if vhostuserDev.Transport.isVirtioPCI(config) && vhostuserDev.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", vhostuserDev.ROMFile)) + } + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + return qemuParams +} + +// QemuBlkParams builds QEMU device parameters for a VhostUserBlk device +func (vhostuserDev VhostUserDevice) QemuBlkParams(config *Config) []string { + var qemuParams []string + var deviceParams []string + + driver := vhostuserDev.deviceName(config) + if driver == "" { + return nil + } + + deviceParams = append(deviceParams, driver) + deviceParams = append(deviceParams, "logical_block_size=4096") + deviceParams = append(deviceParams, "size=512M") + deviceParams = append(deviceParams, fmt.Sprintf("chardev=%s", vhostuserDev.CharDevID)) + + if vhostuserDev.Transport.isVirtioPCI(config) && vhostuserDev.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", vhostuserDev.ROMFile)) + } + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + return qemuParams +} + +// QemuFSParams builds QEMU device parameters for a VhostUserFS device +func (vhostuserDev VhostUserDevice) QemuFSParams(config *Config) []string { + var qemuParams []string + var deviceParams []string + + driver := vhostuserDev.deviceName(config) + if driver == "" { + return nil + } + + deviceParams = append(deviceParams, driver) + deviceParams = append(deviceParams, fmt.Sprintf("chardev=%s", vhostuserDev.CharDevID)) + deviceParams = append(deviceParams, fmt.Sprintf("tag=%s", vhostuserDev.Tag)) + if vhostuserDev.CacheSize != 0 { + deviceParams = append(deviceParams, fmt.Sprintf("cache-size=%dM", vhostuserDev.CacheSize)) + } + if vhostuserDev.SharedVersions { + deviceParams = append(deviceParams, "versiontable=/dev/shm/fuse_shared_versions") + } + if vhostuserDev.Transport.isVirtioCCW(config) { + if config.Knobs.IOMMUPlatform { + deviceParams = append(deviceParams, "iommu_platform=on") + } + deviceParams = append(deviceParams, fmt.Sprintf("devno=%s", vhostuserDev.DevNo)) + } + if vhostuserDev.Transport.isVirtioPCI(config) && vhostuserDev.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", vhostuserDev.ROMFile)) + } + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + return qemuParams +} + +// QemuParams returns the qemu parameters built out of this vhostuser device. +func (vhostuserDev VhostUserDevice) QemuParams(config *Config) []string { + var qemuParams []string + var charParams []string + var deviceParams []string + + charParams = append(charParams, "socket") + charParams = append(charParams, fmt.Sprintf("id=%s", vhostuserDev.CharDevID)) + charParams = append(charParams, fmt.Sprintf("path=%s", vhostuserDev.SocketPath)) + + qemuParams = append(qemuParams, "-chardev") + qemuParams = append(qemuParams, strings.Join(charParams, ",")) + + switch vhostuserDev.VhostUserType { + case VhostUserNet: + deviceParams = vhostuserDev.QemuNetParams(config) + case VhostUserSCSI: + deviceParams = vhostuserDev.QemuSCSIParams(config) + case VhostUserBlk: + deviceParams = vhostuserDev.QemuBlkParams(config) + case VhostUserFS: + deviceParams = vhostuserDev.QemuFSParams(config) + default: + return nil + } + + if deviceParams != nil { + return append(qemuParams, deviceParams...) + } + + return nil +} + +// deviceName returns the QEMU device name for the current combination of +// driver and transport. +func (vhostuserDev VhostUserDevice) deviceName(config *Config) string { + if vhostuserDev.Transport == "" { + vhostuserDev.Transport = vhostuserDev.Transport.defaultTransport(config) + } + + switch vhostuserDev.VhostUserType { + case VhostUserNet: + return VhostUserNetTransport[vhostuserDev.Transport] + case VhostUserSCSI: + return VhostUserSCSITransport[vhostuserDev.Transport] + case VhostUserBlk: + return VhostUserBlkTransport[vhostuserDev.Transport] + case VhostUserFS: + return VhostUserFSTransport[vhostuserDev.Transport] + default: + return "" + } +} + +// PCIeRootPortDevice represents a memory balloon device. +type PCIeRootPortDevice struct { + ID string // format: rp{n}, n>=0 + + Bus string // default is pcie.0 + Chassis string // (slot, chassis) pair is mandatory and must be unique for each pcie-root-port, >=0, default is 0x00 + Slot string // >=0, default is 0x00 + + Multifunction bool // true => "on", false => "off", default is off + Addr string // >=0, default is 0x00 + + // The PCIE-PCI bridge can be hot-plugged only into pcie-root-port that has 'bus-reserve' property value to + // provide secondary bus for the hot-plugged bridge. + BusReserve string + Pref64Reserve string // reserve prefetched MMIO aperture, 64-bit + Pref32Reserve string // reserve prefetched MMIO aperture, 32-bit + MemReserve string // reserve non-prefetched MMIO aperture, 32-bit *only* + IOReserve string // IO reservation + + ROMFile string // ROMFile specifies the ROM file being used for this device. + + // Transport is the virtio transport for this device. + Transport VirtioTransport +} + +// QemuParams returns the qemu parameters built out of the PCIeRootPortDevice. +func (b PCIeRootPortDevice) QemuParams(config *Config) []string { + var qemuParams []string + var deviceParams []string + driver := PCIeRootPort + + deviceParams = append(deviceParams, fmt.Sprintf("%s,id=%s", driver, b.ID)) + + if b.Bus == "" { + b.Bus = "pcie.0" + } + deviceParams = append(deviceParams, fmt.Sprintf("bus=%s", b.Bus)) + + if b.Chassis == "" { + b.Chassis = "0x00" + } + deviceParams = append(deviceParams, fmt.Sprintf("chassis=%s", b.Chassis)) + + if b.Slot == "" { + b.Slot = "0x00" + } + deviceParams = append(deviceParams, fmt.Sprintf("slot=%s", b.Slot)) + + multifunction := "off" + if b.Multifunction { + multifunction = "on" + if b.Addr == "" { + b.Addr = "0x00" + } + deviceParams = append(deviceParams, fmt.Sprintf("addr=%s", b.Addr)) + } + deviceParams = append(deviceParams, fmt.Sprintf("multifunction=%v", multifunction)) + + if b.BusReserve != "" { + deviceParams = append(deviceParams, fmt.Sprintf("bus-reserve=%s", b.BusReserve)) + } + + if b.Pref64Reserve != "" { + deviceParams = append(deviceParams, fmt.Sprintf("pref64-reserve=%s", b.Pref64Reserve)) + } + + if b.Pref32Reserve != "" { + deviceParams = append(deviceParams, fmt.Sprintf("pref32-reserve=%s", b.Pref32Reserve)) + } + + if b.MemReserve != "" { + deviceParams = append(deviceParams, fmt.Sprintf("mem-reserve=%s", b.MemReserve)) + } + + if b.IOReserve != "" { + deviceParams = append(deviceParams, fmt.Sprintf("io-reserve=%s", b.IOReserve)) + } + + if b.Transport.isVirtioPCI(config) && b.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", b.ROMFile)) + } + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + return qemuParams +} + +// Valid returns true if the PCIeRootPortDevice structure is valid and complete. +func (b PCIeRootPortDevice) Valid() bool { + // the "pref32-reserve" and "pref64-reserve" hints are mutually exclusive. + if b.Pref64Reserve != "" && b.Pref32Reserve != "" { + return false + } + if b.ID == "" { + return false + } + return true +} + +// VFIODevice represents a qemu vfio device meant for direct access by guest OS. +type VFIODevice struct { + // Bus-Device-Function of device + BDF string + + // ROMFile specifies the ROM file being used for this device. + ROMFile string + + // DevNo identifies the ccw devices for s390x architecture + DevNo string + + // VendorID specifies vendor id + VendorID string + + // DeviceID specifies device id + DeviceID string + + // Bus specifies device bus + Bus string + + // Transport is the virtio transport for this device. + Transport VirtioTransport +} + +// VFIODeviceTransport is a map of the vfio device name that corresponds to +// each transport. +var VFIODeviceTransport = map[VirtioTransport]string{ + TransportPCI: "vfio-pci", + TransportCCW: "vfio-ccw", + TransportMMIO: "vfio-device", +} + +// Valid returns true if the VFIODevice structure is valid and complete. +func (vfioDev VFIODevice) Valid() bool { + return vfioDev.BDF != "" +} + +// QemuParams returns the qemu parameters built out of this vfio device. +func (vfioDev VFIODevice) QemuParams(config *Config) []string { + var qemuParams []string + var deviceParams []string + + driver := vfioDev.deviceName(config) + + deviceParams = append(deviceParams, fmt.Sprintf("%s,host=%s", driver, vfioDev.BDF)) + if vfioDev.Transport.isVirtioPCI(config) { + if vfioDev.VendorID != "" { + deviceParams = append(deviceParams, fmt.Sprintf("x-pci-vendor-id=%s", vfioDev.VendorID)) + } + if vfioDev.DeviceID != "" { + deviceParams = append(deviceParams, fmt.Sprintf("x-pci-device-id=%s", vfioDev.DeviceID)) + } + if vfioDev.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", vfioDev.ROMFile)) + } + } + + if vfioDev.Bus != "" { + deviceParams = append(deviceParams, fmt.Sprintf("bus=%s", vfioDev.Bus)) + } + + if vfioDev.Transport.isVirtioCCW(config) { + deviceParams = append(deviceParams, fmt.Sprintf("devno=%s", vfioDev.DevNo)) + } + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + return qemuParams +} + +// deviceName returns the QEMU device name for the current combination of +// driver and transport. +func (vfioDev VFIODevice) deviceName(config *Config) string { + if vfioDev.Transport == "" { + vfioDev.Transport = vfioDev.Transport.defaultTransport(config) + } + + return VFIODeviceTransport[vfioDev.Transport] +} + +// SCSIController represents a SCSI controller device. +type SCSIController struct { + ID string + + // Bus on which the SCSI controller is attached, this is optional + Bus string + + // Addr is the PCI address offset, this is optional + Addr string + + // DisableModern prevents qemu from relying on fast MMIO. + DisableModern bool + + // IOThread is the IO thread on which IO will be handled + IOThread string + + // ROMFile specifies the ROM file being used for this device. + ROMFile string + + // DevNo identifies the ccw devices for s390x architecture + DevNo string + + // Transport is the virtio transport for this device. + Transport VirtioTransport +} + +// SCSIControllerTransport is a map of the virtio-scsi device name that +// corresponds to each transport. +var SCSIControllerTransport = map[VirtioTransport]string{ + TransportPCI: "virtio-scsi-pci", + TransportCCW: "virtio-scsi-ccw", + TransportMMIO: "virtio-scsi-device", +} + +// Valid returns true if the SCSIController structure is valid and complete. +func (scsiCon SCSIController) Valid() bool { + return scsiCon.ID != "" +} + +// QemuParams returns the qemu parameters built out of this SCSIController device. +func (scsiCon SCSIController) QemuParams(config *Config) []string { + var qemuParams []string + var deviceParams []string + + driver := scsiCon.deviceName(config) + deviceParams = append(deviceParams, fmt.Sprintf("%s,id=%s", driver, scsiCon.ID)) + if scsiCon.Bus != "" { + deviceParams = append(deviceParams, fmt.Sprintf("bus=%s", scsiCon.Bus)) + } + if scsiCon.Addr != "" { + deviceParams = append(deviceParams, fmt.Sprintf("addr=%s", scsiCon.Addr)) + } + if s := scsiCon.Transport.disableModern(config, scsiCon.DisableModern); s != "" { + deviceParams = append(deviceParams, s) + } + if scsiCon.IOThread != "" { + deviceParams = append(deviceParams, fmt.Sprintf("iothread=%s", scsiCon.IOThread)) + } + if scsiCon.Transport.isVirtioPCI(config) && scsiCon.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", scsiCon.ROMFile)) + } + + if scsiCon.Transport.isVirtioCCW(config) { + if config.Knobs.IOMMUPlatform { + deviceParams = append(deviceParams, "iommu_platform=on") + } + deviceParams = append(deviceParams, fmt.Sprintf("devno=%s", scsiCon.DevNo)) + } + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + return qemuParams +} + +// deviceName returns the QEMU device name for the current combination of +// driver and transport. +func (scsiCon SCSIController) deviceName(config *Config) string { + if scsiCon.Transport == "" { + scsiCon.Transport = scsiCon.Transport.defaultTransport(config) + } + + return SCSIControllerTransport[scsiCon.Transport] +} + +// BridgeType is the type of the bridge +type BridgeType uint + +const ( + // PCIBridge is a pci bridge + PCIBridge BridgeType = iota + + // PCIEBridge is a pcie bridge + PCIEBridge +) + +// BridgeDevice represents a qemu bridge device like pci-bridge, pxb, etc. +type BridgeDevice struct { + // Type of the bridge + Type BridgeType + + // Bus number where the bridge is plugged, typically pci.0 or pcie.0 + Bus string + + // ID is used to identify the bridge in qemu + ID string + + // Chassis number + Chassis int + + // SHPC is used to enable or disable the standard hot plug controller + SHPC bool + + // PCI Slot + Addr string + + // ROMFile specifies the ROM file being used for this device. + ROMFile string + + // Address range reservations for devices behind the bridge + // NB: strings seem an odd choice, but if they were integers, + // they'd default to 0 by Go's rules in all the existing users + // who don't set them. 0 is a valid value for certain cases, + // but not you want by default. + IOReserve string + MemReserve string + Pref64Reserve string +} + +// Valid returns true if the BridgeDevice structure is valid and complete. +func (bridgeDev BridgeDevice) Valid() bool { + if bridgeDev.Type != PCIBridge && bridgeDev.Type != PCIEBridge { + return false + } + + if bridgeDev.Bus == "" { + return false + } + + if bridgeDev.ID == "" { + return false + } + + return true +} + +// QemuParams returns the qemu parameters built out of this bridge device. +func (bridgeDev BridgeDevice) QemuParams(config *Config) []string { + var qemuParams []string + var deviceParams []string + var driver DeviceDriver + + switch bridgeDev.Type { + case PCIEBridge: + driver = PCIePCIBridgeDriver + deviceParams = append(deviceParams, fmt.Sprintf("%s,bus=%s,id=%s", driver, bridgeDev.Bus, bridgeDev.ID)) + default: + driver = PCIBridgeDriver + shpc := "off" + if bridgeDev.SHPC { + shpc = "on" + } + deviceParams = append(deviceParams, fmt.Sprintf("%s,bus=%s,id=%s,chassis_nr=%d,shpc=%s", driver, bridgeDev.Bus, bridgeDev.ID, bridgeDev.Chassis, shpc)) + } + + if bridgeDev.Addr != "" { + addr, err := strconv.Atoi(bridgeDev.Addr) + if err == nil && addr >= 0 { + deviceParams = append(deviceParams, fmt.Sprintf("addr=%x", addr)) + } + } + + var transport VirtioTransport + if transport.isVirtioPCI(config) && bridgeDev.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", bridgeDev.ROMFile)) + } + + if bridgeDev.IOReserve != "" { + deviceParams = append(deviceParams, fmt.Sprintf("io-reserve=%s", bridgeDev.IOReserve)) + } + if bridgeDev.MemReserve != "" { + deviceParams = append(deviceParams, fmt.Sprintf("mem-reserve=%s", bridgeDev.MemReserve)) + } + if bridgeDev.Pref64Reserve != "" { + deviceParams = append(deviceParams, fmt.Sprintf("pref64-reserve=%s", bridgeDev.Pref64Reserve)) + } + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + return qemuParams +} + +// VSOCKDevice represents a AF_VSOCK socket. +type VSOCKDevice struct { + ID string + + ContextID uint64 + + // VHostFD vhost file descriptor that holds the ContextID + VHostFD *os.File + + // DisableModern prevents qemu from relying on fast MMIO. + DisableModern bool + + // ROMFile specifies the ROM file being used for this device. + ROMFile string + + // DevNo identifies the ccw devices for s390x architecture + DevNo string + + // Transport is the virtio transport for this device. + Transport VirtioTransport +} + +// VSOCKDeviceTransport is a map of the vhost-vsock device name that +// corresponds to each transport. +var VSOCKDeviceTransport = map[VirtioTransport]string{ + TransportPCI: "vhost-vsock-pci", + TransportCCW: "vhost-vsock-ccw", + TransportMMIO: "vhost-vsock-device", +} + +const ( + // MinimalGuestCID is the smallest valid context ID for a guest. + MinimalGuestCID uint64 = 3 + + // MaxGuestCID is the largest valid context ID for a guest. + MaxGuestCID uint64 = 1<<32 - 1 +) + +const ( + // VSOCKGuestCID is the VSOCK guest CID parameter. + VSOCKGuestCID = "guest-cid" +) + +// Valid returns true if the VSOCKDevice structure is valid and complete. +func (vsock VSOCKDevice) Valid() bool { + if vsock.ID == "" || vsock.ContextID < MinimalGuestCID || vsock.ContextID > MaxGuestCID { + return false + } + + return true +} + +// QemuParams returns the qemu parameters built out of the VSOCK device. +func (vsock VSOCKDevice) QemuParams(config *Config) []string { + var deviceParams []string + var qemuParams []string + + driver := vsock.deviceName(config) + deviceParams = append(deviceParams, driver) + if s := vsock.Transport.disableModern(config, vsock.DisableModern); s != "" { + deviceParams = append(deviceParams, s) + } + if vsock.VHostFD != nil { + qemuFDs := config.appendFDs([]*os.File{vsock.VHostFD}) + deviceParams = append(deviceParams, fmt.Sprintf("vhostfd=%d", qemuFDs[0])) + } + deviceParams = append(deviceParams, fmt.Sprintf("id=%s", vsock.ID)) + deviceParams = append(deviceParams, fmt.Sprintf("%s=%d", VSOCKGuestCID, vsock.ContextID)) + + if vsock.Transport.isVirtioPCI(config) && vsock.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", vsock.ROMFile)) + } + + if vsock.Transport.isVirtioCCW(config) { + if config.Knobs.IOMMUPlatform { + deviceParams = append(deviceParams, "iommu_platform=on") + } + deviceParams = append(deviceParams, fmt.Sprintf("devno=%s", vsock.DevNo)) + } + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + return qemuParams +} + +// deviceName returns the QEMU device name for the current combination of +// driver and transport. +func (vsock VSOCKDevice) deviceName(config *Config) string { + if vsock.Transport == "" { + vsock.Transport = vsock.Transport.defaultTransport(config) + } + + return VSOCKDeviceTransport[vsock.Transport] +} + +// RngDevice represents a random number generator device. +type RngDevice struct { + // ID is the device ID + ID string + // Filename is entropy source on the host + Filename string + // MaxBytes is the bytes allowed to guest to get from the host’s entropy per period + MaxBytes uint + // Period is duration of a read period in seconds + Period uint + // ROMFile specifies the ROM file being used for this device. + ROMFile string + // DevNo identifies the ccw devices for s390x architecture + DevNo string + // Transport is the virtio transport for this device. + Transport VirtioTransport +} + +// RngDeviceTransport is a map of the virtio-rng device name that corresponds +// to each transport. +var RngDeviceTransport = map[VirtioTransport]string{ + TransportPCI: "virtio-rng-pci", + TransportCCW: "virtio-rng-ccw", + TransportMMIO: "virtio-rng-device", +} + +// Valid returns true if the RngDevice structure is valid and complete. +func (v RngDevice) Valid() bool { + return v.ID != "" +} + +// QemuParams returns the qemu parameters built out of the RngDevice. +func (v RngDevice) QemuParams(config *Config) []string { + var qemuParams []string + + //-object rng-random,filename=/dev/hwrng,id=rng0 + var objectParams []string + //-device virtio-rng-pci,rng=rng0,max-bytes=1024,period=1000 + var deviceParams []string + + objectParams = append(objectParams, "rng-random") + objectParams = append(objectParams, "id="+v.ID) + + deviceParams = append(deviceParams, v.deviceName(config)) + deviceParams = append(deviceParams, "rng="+v.ID) + + if v.Transport.isVirtioPCI(config) && v.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", v.ROMFile)) + } + + if v.Transport.isVirtioCCW(config) { + if config.Knobs.IOMMUPlatform { + deviceParams = append(deviceParams, "iommu_platform=on") + } + deviceParams = append(deviceParams, fmt.Sprintf("devno=%s", v.DevNo)) + } + + if v.Filename != "" { + objectParams = append(objectParams, "filename="+v.Filename) + } + + if v.MaxBytes > 0 { + deviceParams = append(deviceParams, fmt.Sprintf("max-bytes=%d", v.MaxBytes)) + } + + if v.Period > 0 { + deviceParams = append(deviceParams, fmt.Sprintf("period=%d", v.Period)) + } + + qemuParams = append(qemuParams, "-object") + qemuParams = append(qemuParams, strings.Join(objectParams, ",")) + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + return qemuParams +} + +// deviceName returns the QEMU device name for the current combination of +// driver and transport. +func (v RngDevice) deviceName(config *Config) string { + if v.Transport == "" { + v.Transport = v.Transport.defaultTransport(config) + } + + return RngDeviceTransport[v.Transport] +} + +// BalloonDevice represents a memory balloon device. +type BalloonDevice struct { + DeflateOnOOM bool + DisableModern bool + ID string + + // ROMFile specifies the ROM file being used for this device. + ROMFile string + + // DevNo identifies the ccw devices for s390x architecture + DevNo string + + // Transport is the virtio transport for this device. + Transport VirtioTransport +} + +// BalloonDeviceTransport is a map of the virtio-balloon device name that +// corresponds to each transport. +var BalloonDeviceTransport = map[VirtioTransport]string{ + TransportPCI: "virtio-balloon-pci", + TransportCCW: "virtio-balloon-ccw", + TransportMMIO: "virtio-balloon-device", +} + +// QemuParams returns the qemu parameters built out of the BalloonDevice. +func (b BalloonDevice) QemuParams(config *Config) []string { + var qemuParams []string + var deviceParams []string + + deviceParams = append(deviceParams, b.deviceName(config)) + + if b.ID != "" { + deviceParams = append(deviceParams, "id="+b.ID) + } + + if b.Transport.isVirtioPCI(config) && b.ROMFile != "" { + deviceParams = append(deviceParams, fmt.Sprintf("romfile=%s", b.ROMFile)) + } + + if b.Transport.isVirtioCCW(config) { + deviceParams = append(deviceParams, fmt.Sprintf("devno=%s", b.DevNo)) + } + + if b.DeflateOnOOM { + deviceParams = append(deviceParams, "deflate-on-oom=on") + } else { + deviceParams = append(deviceParams, "deflate-on-oom=off") + } + if s := b.Transport.disableModern(config, b.DisableModern); s != "" { + deviceParams = append(deviceParams, s) + } + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + + return qemuParams +} + +// Valid returns true if the balloonDevice structure is valid and complete. +func (b BalloonDevice) Valid() bool { + return b.ID != "" +} + +// deviceName returns the QEMU device name for the current combination of +// driver and transport. +func (b BalloonDevice) deviceName(config *Config) string { + if b.Transport == "" { + b.Transport = b.Transport.defaultTransport(config) + } + + return BalloonDeviceTransport[b.Transport] +} + +// IommuDev represents a Intel IOMMU Device +type IommuDev struct { + Intremap bool + DeviceIotlb bool + CachingMode bool +} + +// Valid returns true if the IommuDev is valid +func (dev IommuDev) Valid() bool { + return true +} + +// deviceName the qemu device name +func (dev IommuDev) deviceName() string { + return "intel-iommu" +} + +// QemuParams returns the qemu parameters built out of the IommuDev. +func (dev IommuDev) QemuParams(_ *Config) []string { + var qemuParams []string + var deviceParams []string + + deviceParams = append(deviceParams, dev.deviceName()) + if dev.Intremap { + deviceParams = append(deviceParams, "intremap=on") + } else { + deviceParams = append(deviceParams, "intremap=off") + } + + if dev.DeviceIotlb { + deviceParams = append(deviceParams, "device-iotlb=on") + } else { + deviceParams = append(deviceParams, "device-iotlb=off") + } + + if dev.CachingMode { + deviceParams = append(deviceParams, "caching-mode=on") + } else { + deviceParams = append(deviceParams, "caching-mode=off") + } + + qemuParams = append(qemuParams, "-device") + qemuParams = append(qemuParams, strings.Join(deviceParams, ",")) + return qemuParams +} + +// RTCBaseType is the qemu RTC base time type. +type RTCBaseType string + +// RTCClock is the qemu RTC clock type. +type RTCClock string + +// RTCDriftFix is the qemu RTC drift fix type. +type RTCDriftFix string + +const ( + // UTC is the UTC base time for qemu RTC. + UTC RTCBaseType = "utc" + + // LocalTime is the local base time for qemu RTC. + LocalTime RTCBaseType = "localtime" +) + +const ( + // Host is for using the host clock as a reference. + Host RTCClock = "host" + + // RT is for using the host monotonic clock as a reference. + RT RTCClock = "rt" + + // VM is for using the guest clock as a reference + VM RTCClock = "vm" +) + +const ( + // Slew is the qemu RTC Drift fix mechanism. + Slew RTCDriftFix = "slew" + + // NoDriftFix means we don't want/need to fix qemu's RTC drift. + NoDriftFix RTCDriftFix = "none" +) + +// RTC represents a qemu Real Time Clock configuration. +type RTC struct { + // Base is the RTC start time. + Base RTCBaseType + + // Clock is the is the RTC clock driver. + Clock RTCClock + + // DriftFix is the drift fixing mechanism. + DriftFix RTCDriftFix +} + +// Valid returns true if the RTC structure is valid and complete. +func (rtc RTC) Valid() bool { + if rtc.Clock != Host && rtc.Clock != RT && rtc.Clock != VM { + return false + } + + if rtc.DriftFix != Slew && rtc.DriftFix != NoDriftFix { + return false + } + + return true +} + +// QMPSocketType is the type of socket used for QMP communication. +type QMPSocketType string + +const ( + // Unix socket for QMP. + Unix QMPSocketType = "unix" +) + +// QMPSocket represents a qemu QMP socket configuration. +type QMPSocket struct { + // Type is the socket type (e.g. "unix"). + Type QMPSocketType + + // Name is the socket name. + Name string + + // Server tells if this is a server socket. + Server bool + + // NoWait tells if qemu should block waiting for a client to connect. + NoWait bool +} + +// Valid returns true if the QMPSocket structure is valid and complete. +func (qmp QMPSocket) Valid() bool { + if qmp.Type == "" || qmp.Name == "" { + return false + } + + if qmp.Type != Unix { + return false + } + + return true +} + +// SMP is the multi processors configuration structure. +type SMP struct { + // CPUs is the number of VCPUs made available to qemu. + CPUs uint32 + + // Cores is the number of cores made available to qemu. + Cores uint32 + + // Threads is the number of threads made available to qemu. + Threads uint32 + + // Sockets is the number of sockets made available to qemu. + Sockets uint32 + + // MaxCPUs is the maximum number of VCPUs that a VM can have. + // This value, if non-zero, MUST BE equal to or greater than CPUs + MaxCPUs uint32 +} + +// Memory is the guest memory configuration structure. +type Memory struct { + // Size is the amount of memory made available to the guest. + // It should be suffixed with M or G for sizes in megabytes or + // gigabytes respectively. + Size string + + // Slots is the amount of memory slots made available to the guest. + Slots uint8 + + // MaxMem is the maximum amount of memory that can be made available + // to the guest through e.g. hot pluggable memory. + MaxMem string + + // Path is the file path of the memory device. It points to a local + // file path used by FileBackedMem. + Path string +} + +// Kernel is the guest kernel configuration structure. +type Kernel struct { + // Path is the guest kernel path on the host filesystem. + Path string + + // InitrdPath is the guest initrd path on the host filesystem. + InitrdPath string + + // Params is the kernel parameters string. + Params string +} + +// FwCfg allows QEMU to pass entries to the guest +// File and Str are mutually exclusive +type FwCfg struct { + Name string + File string + Str string +} + +// Valid returns true if the FwCfg structure is valid and complete. +func (fwcfg FwCfg) Valid() bool { + if fwcfg.Name == "" { + return false + } + + if fwcfg.File != "" && fwcfg.Str != "" { + return false + } + + if fwcfg.File == "" && fwcfg.Str == "" { + return false + } + + return true +} + +// QemuParams returns the qemu parameters built out of the FwCfg object +func (fwcfg FwCfg) QemuParams(config *Config) []string { + var fwcfgParams []string + var qemuParams []string + + for _, f := range config.FwCfg { + if f.Name != "" { + fwcfgParams = append(fwcfgParams, fmt.Sprintf("name=%s", f.Name)) + + if f.File != "" { + fwcfgParams = append(fwcfgParams, fmt.Sprintf("file=%s", f.File)) + } + + if f.Str != "" { + fwcfgParams = append(fwcfgParams, fmt.Sprintf("string=%s", f.Str)) + } + } + + qemuParams = append(qemuParams, "-fw_cfg") + qemuParams = append(qemuParams, strings.Join(fwcfgParams, ",")) + } + + return qemuParams +} + +// Knobs regroups a set of qemu boolean settings +type Knobs struct { + // NoUserConfig prevents qemu from loading user config files. + NoUserConfig bool + + // NoDefaults prevents qemu from creating default devices. + NoDefaults bool + + // NoGraphic completely disables graphic output. + NoGraphic bool + + // Daemonize will turn the qemu process into a daemon + Daemonize bool + + // Both HugePages and MemPrealloc require the Memory.Size of the VM + // to be set, as they need to reserve the memory upfront in order + // for the VM to boot without errors. + // + // HugePages always results in memory pre-allocation. + // However the setup is different from normal pre-allocation. + // Hence HugePages has precedence over MemPrealloc + // HugePages will pre-allocate all the RAM from huge pages + HugePages bool + + // MemPrealloc will allocate all the RAM upfront + MemPrealloc bool + + // FileBackedMem requires Memory.Size and Memory.Path of the VM to + // be set. + FileBackedMem bool + + // MemShared will set the memory device as shared. + MemShared bool + + // Mlock will control locking of memory + Mlock bool + + // Stopped will not start guest CPU at startup + Stopped bool + + // Exit instead of rebooting + // Prevents QEMU from rebooting in the event of a Triple Fault. + NoReboot bool + + // Don’t exit QEMU on guest shutdown, but instead only stop the emulation. + NoShutdown bool + + // IOMMUPlatform will enable IOMMU for supported devices + IOMMUPlatform bool +} + +// IOThread allows IO to be performed on a separate thread. +type IOThread struct { + ID string +} + +const ( + // MigrationFD is the migration incoming type based on open file descriptor. + // Skip default 0 so that it must be set on purpose. + MigrationFD = 1 + // MigrationExec is the migration incoming type based on commands. + MigrationExec = 2 + // MigrationDefer is the defer incoming type + MigrationDefer = 3 +) + +// Incoming controls migration source preparation +type Incoming struct { + // Possible values are MigrationFD, MigrationExec + MigrationType int + // Only valid if MigrationType == MigrationFD + FD *os.File + // Only valid if MigrationType == MigrationExec + Exec string +} + +// Config is the qemu configuration structure. +// It allows for passing custom settings and parameters to the qemu API. +type Config struct { + // Path is the qemu binary path. + Path string + + // Ctx is the context used when launching qemu. + Ctx context.Context + + // User ID. + Uid uint32 + // Group ID. + Gid uint32 + // Supplementary group IDs. + Groups []uint32 + + // Name is the qemu guest name + Name string + + // UUID is the qemu process UUID. + UUID string + + // CPUModel is the CPU model to be used by qemu. + CPUModel string + + // SeccompSandbox is the qemu function which enables the seccomp feature + SeccompSandbox string + + // Machine + Machine Machine + + // QMPSockets is a slice of QMP socket description. + QMPSockets []QMPSocket + + // Devices is a list of devices for qemu to create and drive. + Devices []Device + + // RTC is the qemu Real Time Clock configuration + RTC RTC + + // VGA is the qemu VGA mode. + VGA string + + // Kernel is the guest kernel configuration. + Kernel Kernel + + // Memory is the guest memory configuration. + Memory Memory + + // SMP is the quest multi processors configuration. + SMP SMP + + // GlobalParam is the -global parameter. + GlobalParam string + + // Knobs is a set of qemu boolean settings. + Knobs Knobs + + // Bios is the -bios parameter + Bios string + + // PFlash specifies the parallel flash images (-pflash parameter) + PFlash []string + + // Incoming controls migration source preparation + Incoming Incoming + + // fds is a list of open file descriptors to be passed to the spawned qemu process + fds []*os.File + + // FwCfg is the -fw_cfg parameter + FwCfg []FwCfg + + IOThreads []IOThread + + // PidFile is the -pidfile parameter + PidFile string + + // LogFile is the -D parameter + LogFile string + + qemuParams []string +} + +// appendFDs append a list of file descriptors to the qemu configuration and +// returns a slice of offset file descriptors that will be seen by the qemu process. +func (config *Config) appendFDs(fds []*os.File) []int { + var fdInts []int + + oldLen := len(config.fds) + + config.fds = append(config.fds, fds...) + + // The magic 3 offset comes from https://golang.org/src/os/exec/exec.go: + // ExtraFiles specifies additional open files to be inherited by the + // new process. It does not include standard input, standard output, or + // standard error. If non-nil, entry i becomes file descriptor 3+i. + for i := range fds { + fdInts = append(fdInts, oldLen+3+i) + } + + return fdInts +} + +func (config *Config) appendSeccompSandbox() { + if config.SeccompSandbox != "" { + config.qemuParams = append(config.qemuParams, "-sandbox") + config.qemuParams = append(config.qemuParams, config.SeccompSandbox) + } +} + +func (config *Config) appendName() { + if config.Name != "" { + config.qemuParams = append(config.qemuParams, "-name") + config.qemuParams = append(config.qemuParams, config.Name) + } +} + +func (config *Config) appendMachine() { + if config.Machine.Type != "" { + var machineParams []string + + machineParams = append(machineParams, config.Machine.Type) + + if config.Machine.Acceleration != "" { + machineParams = append(machineParams, fmt.Sprintf("accel=%s", config.Machine.Acceleration)) + } + + if config.Machine.Options != "" { + machineParams = append(machineParams, config.Machine.Options) + } + + config.qemuParams = append(config.qemuParams, "-machine") + config.qemuParams = append(config.qemuParams, strings.Join(machineParams, ",")) + } +} + +func (config *Config) appendCPUModel() { + if config.CPUModel != "" { + config.qemuParams = append(config.qemuParams, "-cpu") + config.qemuParams = append(config.qemuParams, config.CPUModel) + } +} + +func (config *Config) appendQMPSockets() { + for _, q := range config.QMPSockets { + if !q.Valid() { + continue + } + + qmpParams := append([]string{}, fmt.Sprintf("%s:%s", q.Type, q.Name)) + if q.Server { + qmpParams = append(qmpParams, "server=on") + if q.NoWait { + qmpParams = append(qmpParams, "wait=off") + } + } + + config.qemuParams = append(config.qemuParams, "-qmp") + config.qemuParams = append(config.qemuParams, strings.Join(qmpParams, ",")) + } +} + +func (config *Config) appendDevices() { + for _, d := range config.Devices { + if !d.Valid() { + continue + } + + config.qemuParams = append(config.qemuParams, d.QemuParams(config)...) + } +} + +func (config *Config) appendUUID() { + if config.UUID != "" { + config.qemuParams = append(config.qemuParams, "-uuid") + config.qemuParams = append(config.qemuParams, config.UUID) + } +} + +func (config *Config) appendMemory() { + if config.Memory.Size != "" { + var memoryParams []string + + memoryParams = append(memoryParams, config.Memory.Size) + + if config.Memory.Slots > 0 { + memoryParams = append(memoryParams, fmt.Sprintf("slots=%d", config.Memory.Slots)) + } + + if config.Memory.MaxMem != "" { + memoryParams = append(memoryParams, fmt.Sprintf("maxmem=%s", config.Memory.MaxMem)) + } + + config.qemuParams = append(config.qemuParams, "-m") + config.qemuParams = append(config.qemuParams, strings.Join(memoryParams, ",")) + } +} + +func (config *Config) appendCPUs() error { + if config.SMP.CPUs > 0 { + var SMPParams []string + + SMPParams = append(SMPParams, fmt.Sprintf("%d", config.SMP.CPUs)) + + if config.SMP.Cores > 0 { + SMPParams = append(SMPParams, fmt.Sprintf("cores=%d", config.SMP.Cores)) + } + + if config.SMP.Threads > 0 { + SMPParams = append(SMPParams, fmt.Sprintf("threads=%d", config.SMP.Threads)) + } + + if config.SMP.Sockets > 0 { + SMPParams = append(SMPParams, fmt.Sprintf("sockets=%d", config.SMP.Sockets)) + } + + if config.SMP.MaxCPUs > 0 { + if config.SMP.MaxCPUs < config.SMP.CPUs { + return fmt.Errorf("MaxCPUs %d must be equal to or greater than CPUs %d", + config.SMP.MaxCPUs, config.SMP.CPUs) + } + SMPParams = append(SMPParams, fmt.Sprintf("maxcpus=%d", config.SMP.MaxCPUs)) + } + + config.qemuParams = append(config.qemuParams, "-smp") + config.qemuParams = append(config.qemuParams, strings.Join(SMPParams, ",")) + } + + return nil +} + +func (config *Config) appendRTC() { + if !config.RTC.Valid() { + return + } + + var RTCParams []string + + RTCParams = append(RTCParams, fmt.Sprintf("base=%s", string(config.RTC.Base))) + + if config.RTC.DriftFix != "" { + RTCParams = append(RTCParams, fmt.Sprintf("driftfix=%s", config.RTC.DriftFix)) + } + + if config.RTC.Clock != "" { + RTCParams = append(RTCParams, fmt.Sprintf("clock=%s", config.RTC.Clock)) + } + + config.qemuParams = append(config.qemuParams, "-rtc") + config.qemuParams = append(config.qemuParams, strings.Join(RTCParams, ",")) +} + +func (config *Config) appendGlobalParam() { + if config.GlobalParam != "" { + config.qemuParams = append(config.qemuParams, "-global") + config.qemuParams = append(config.qemuParams, config.GlobalParam) + } +} + +func (config *Config) appendPFlashParam() { + for _, p := range config.PFlash { + config.qemuParams = append(config.qemuParams, "-pflash") + config.qemuParams = append(config.qemuParams, p) + } +} + +func (config *Config) appendVGA() { + if config.VGA != "" { + config.qemuParams = append(config.qemuParams, "-vga") + config.qemuParams = append(config.qemuParams, config.VGA) + } +} + +func (config *Config) appendKernel() { + if config.Kernel.Path != "" { + config.qemuParams = append(config.qemuParams, "-kernel") + config.qemuParams = append(config.qemuParams, config.Kernel.Path) + + if config.Kernel.InitrdPath != "" { + config.qemuParams = append(config.qemuParams, "-initrd") + config.qemuParams = append(config.qemuParams, config.Kernel.InitrdPath) + } + + if config.Kernel.Params != "" { + config.qemuParams = append(config.qemuParams, "-append") + config.qemuParams = append(config.qemuParams, config.Kernel.Params) + } + } +} + +func (config *Config) appendMemoryKnobs() { + if config.Memory.Size == "" { + return + } + var objMemParam, numaMemParam string + dimmName := "dimm1" + if config.Knobs.HugePages { + objMemParam = "memory-backend-file,id=" + dimmName + ",size=" + config.Memory.Size + ",mem-path=/dev/hugepages" + numaMemParam = "node,memdev=" + dimmName + } else if config.Knobs.FileBackedMem && config.Memory.Path != "" { + objMemParam = "memory-backend-file,id=" + dimmName + ",size=" + config.Memory.Size + ",mem-path=" + config.Memory.Path + numaMemParam = "node,memdev=" + dimmName + } else { + objMemParam = "memory-backend-ram,id=" + dimmName + ",size=" + config.Memory.Size + numaMemParam = "node,memdev=" + dimmName + } + + if config.Knobs.MemShared { + objMemParam += ",share=on" + } + if config.Knobs.MemPrealloc { + objMemParam += ",prealloc=on" + } + config.qemuParams = append(config.qemuParams, "-object") + config.qemuParams = append(config.qemuParams, objMemParam) + + if isDimmSupported(config) { + config.qemuParams = append(config.qemuParams, "-numa") + config.qemuParams = append(config.qemuParams, numaMemParam) + } else { + config.qemuParams = append(config.qemuParams, "-machine") + config.qemuParams = append(config.qemuParams, "memory-backend="+dimmName) + } +} + +func (config *Config) appendKnobs() { + if config.Knobs.NoUserConfig { + config.qemuParams = append(config.qemuParams, "-no-user-config") + } + + if config.Knobs.NoDefaults { + config.qemuParams = append(config.qemuParams, "-nodefaults") + } + + if config.Knobs.NoGraphic { + config.qemuParams = append(config.qemuParams, "-nographic") + } + + if config.Knobs.NoReboot { + config.qemuParams = append(config.qemuParams, "--no-reboot") + } + + if config.Knobs.NoShutdown { + config.qemuParams = append(config.qemuParams, "--no-shutdown") + } + + if config.Knobs.Daemonize { + config.qemuParams = append(config.qemuParams, "-daemonize") + } + + config.appendMemoryKnobs() + + if config.Knobs.Mlock { + config.qemuParams = append(config.qemuParams, "-overcommit") + config.qemuParams = append(config.qemuParams, "mem-lock=on") + } + + if config.Knobs.Stopped { + config.qemuParams = append(config.qemuParams, "-S") + } +} + +func (config *Config) appendBios() { + if config.Bios != "" { + config.qemuParams = append(config.qemuParams, "-bios") + config.qemuParams = append(config.qemuParams, config.Bios) + } +} + +func (config *Config) appendIOThreads() { + for _, t := range config.IOThreads { + if t.ID != "" { + config.qemuParams = append(config.qemuParams, "-object") + config.qemuParams = append(config.qemuParams, fmt.Sprintf("iothread,id=%s", t.ID)) + } + } +} + +func (config *Config) appendIncoming() { + var uri string + switch config.Incoming.MigrationType { + case MigrationExec: + uri = fmt.Sprintf("exec:%s", config.Incoming.Exec) + case MigrationFD: + chFDs := config.appendFDs([]*os.File{config.Incoming.FD}) + uri = fmt.Sprintf("fd:%d", chFDs[0]) + case MigrationDefer: + uri = "defer" + default: + return + } + config.qemuParams = append(config.qemuParams, "-S", "-incoming", uri) +} + +func (config *Config) appendPidFile() { + if config.PidFile != "" { + config.qemuParams = append(config.qemuParams, "-pidfile") + config.qemuParams = append(config.qemuParams, config.PidFile) + } +} + +func (config *Config) appendLogFile() { + if config.LogFile != "" { + config.qemuParams = append(config.qemuParams, "-D") + config.qemuParams = append(config.qemuParams, config.LogFile) + } +} + +func (config *Config) appendFwCfg(logger QMPLog) { + if logger == nil { + logger = qmpNullLogger{} + } + + for _, f := range config.FwCfg { + if !f.Valid() { + logger.Errorf("fw_cfg is not valid: %+v", config.FwCfg) + continue + } + + config.qemuParams = append(config.qemuParams, f.QemuParams(config)...) + } +} + +// LaunchQemu can be used to launch a new qemu instance. +// +// The Config parameter contains a set of qemu parameters and settings. +// +// This function writes its log output via logger parameter. +// +// The function will block until the launched qemu process exits. "", nil +// will be returned if the launch succeeds. Otherwise a string containing +// the contents of stderr + a Go error object will be returned. +func LaunchQemu(config Config, logger QMPLog) (string, error) { + config.appendName() + config.appendUUID() + config.appendMachine() + config.appendCPUModel() + config.appendQMPSockets() + config.appendMemory() + config.appendDevices() + config.appendRTC() + config.appendGlobalParam() + config.appendPFlashParam() + config.appendVGA() + config.appendKnobs() + config.appendKernel() + config.appendBios() + config.appendIOThreads() + config.appendIncoming() + config.appendPidFile() + config.appendLogFile() + config.appendFwCfg(logger) + config.appendSeccompSandbox() + + if err := config.appendCPUs(); err != nil { + return "", err + } + + ctx := config.Ctx + if ctx == nil { + ctx = context.Background() + } + + attr := syscall.SysProcAttr{} + attr.Credential = &syscall.Credential{ + Uid: config.Uid, + Gid: config.Gid, + Groups: config.Groups, + } + + return LaunchCustomQemu(ctx, config.Path, config.qemuParams, + config.fds, &attr, logger) +} + +// LaunchCustomQemu can be used to launch a new qemu instance. +// +// The path parameter is used to pass the qemu executable path. +// +// params is a slice of options to pass to qemu-system-x86_64 and fds is a +// list of open file descriptors that are to be passed to the spawned qemu +// process. The attrs parameter can be used to control aspects of the +// newly created qemu process, such as the user and group under which it +// runs. It may be nil. +// +// This function writes its log output via logger parameter. +// +// The function will block until the launched qemu process exits. "", nil +// will be returned if the launch succeeds. Otherwise a string containing +// the contents of stderr + a Go error object will be returned. +func LaunchCustomQemu(ctx context.Context, path string, params []string, fds []*os.File, + attr *syscall.SysProcAttr, logger QMPLog) (string, error) { + if logger == nil { + logger = qmpNullLogger{} + } + + errStr := "" + + if path == "" { + path = "qemu-system-x86_64" + } + + /* #nosec */ + cmd := exec.CommandContext(ctx, path, params...) + if len(fds) > 0 { + logger.Infof("Adding extra file %v", fds) + cmd.ExtraFiles = fds + } + + cmd.SysProcAttr = attr + + var stderr bytes.Buffer + cmd.Stderr = &stderr + logger.Infof("launching %s with: %v", path, params) + + err := cmd.Run() + if err != nil { + logger.Errorf("Unable to launch %s: %v", path, err) + errStr = stderr.String() + logger.Errorf("%s", errStr) + } + return errStr, err +} diff --git a/src/runtime/pkg/govmm/qemu/qemu_arch_base_test.go b/src/runtime/pkg/govmm/qemu/qemu_arch_base_test.go new file mode 100644 index 0000000000..6676097071 --- /dev/null +++ b/src/runtime/pkg/govmm/qemu/qemu_arch_base_test.go @@ -0,0 +1,175 @@ +//go:build !s390x +// +build !s390x + +/* +// Copyright contributors to the Virtual Machine Manager for Go project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ + +package qemu + +import "testing" + +var ( + deviceFSString = "-device virtio-9p-pci,disable-modern=true,fsdev=workload9p,mount_tag=rootfs,romfile=efi-virtio.rom -fsdev local,id=workload9p,path=/var/lib/docker/devicemapper/mnt/e31ebda2,security_model=none,multidevs=remap" + deviceNetworkString = "-netdev tap,id=tap0,vhost=on,ifname=ceth0,downscript=no,script=no -device driver=virtio-net-pci,netdev=tap0,mac=01:02:de:ad:be:ef,disable-modern=true,romfile=efi-virtio.rom" + deviceNetworkStringMq = "-netdev tap,id=tap0,vhost=on,fds=3:4 -device driver=virtio-net-pci,netdev=tap0,mac=01:02:de:ad:be:ef,disable-modern=true,mq=on,vectors=6,romfile=efi-virtio.rom" + deviceSerialString = "-device virtio-serial-pci,disable-modern=true,id=serial0,romfile=efi-virtio.rom,max_ports=2" + deviceVhostUserNetString = "-chardev socket,id=char1,path=/tmp/nonexistentsocket.socket -netdev type=vhost-user,id=net1,chardev=char1,vhostforce -device virtio-net-pci,netdev=net1,mac=00:11:22:33:44:55,romfile=efi-virtio.rom" + deviceVSOCKString = "-device vhost-vsock-pci,disable-modern=true,id=vhost-vsock-pci0,guest-cid=4,romfile=efi-virtio.rom" + deviceVFIOString = "-device vfio-pci,host=02:10.0,x-pci-vendor-id=0x1234,x-pci-device-id=0x5678,romfile=efi-virtio.rom" + devicePCIeRootPortSimpleString = "-device pcie-root-port,id=rp1,bus=pcie.0,chassis=0x00,slot=0x00,multifunction=off" + devicePCIeRootPortFullString = "-device pcie-root-port,id=rp2,bus=pcie.0,chassis=0x0,slot=0x1,addr=0x2,multifunction=on,bus-reserve=0x3,pref64-reserve=16G,mem-reserve=1G,io-reserve=512M,romfile=efi-virtio.rom" + deviceVFIOPCIeSimpleString = "-device vfio-pci,host=02:00.0,bus=rp0" + deviceVFIOPCIeFullString = "-device vfio-pci,host=02:00.0,x-pci-vendor-id=0x10de,x-pci-device-id=0x15f8,romfile=efi-virtio.rom,bus=rp1" + deviceSCSIControllerStr = "-device virtio-scsi-pci,id=foo,disable-modern=false,romfile=efi-virtio.rom" + deviceSCSIControllerBusAddrStr = "-device virtio-scsi-pci,id=foo,bus=pci.0,addr=00:04.0,disable-modern=true,iothread=iothread1,romfile=efi-virtio.rom" + deviceVhostUserSCSIString = "-chardev socket,id=char1,path=/tmp/nonexistentsocket.socket -device vhost-user-scsi-pci,id=scsi1,chardev=char1,romfile=efi-virtio.rom" + deviceVhostUserBlkString = "-chardev socket,id=char2,path=/tmp/nonexistentsocket.socket -device vhost-user-blk-pci,logical_block_size=4096,size=512M,chardev=char2,romfile=efi-virtio.rom" + deviceBlockString = "-device virtio-blk-pci,disable-modern=true,drive=hd0,scsi=off,config-wce=off,romfile=efi-virtio.rom,share-rw=on,serial=hd0 -drive id=hd0,file=/var/lib/vm.img,aio=threads,format=qcow2,if=none,readonly=on" + devicePCIBridgeString = "-device pci-bridge,bus=/pci-bus/pcie.0,id=mybridge,chassis_nr=5,shpc=on,addr=ff,romfile=efi-virtio.rom" + devicePCIBridgeStringReserved = "-device pci-bridge,bus=/pci-bus/pcie.0,id=mybridge,chassis_nr=5,shpc=off,addr=ff,romfile=efi-virtio.rom,io-reserve=4k,mem-reserve=1m,pref64-reserve=1m" + devicePCIEBridgeString = "-device pcie-pci-bridge,bus=/pci-bus/pcie.0,id=mybridge,addr=ff,romfile=efi-virtio.rom" + romfile = "efi-virtio.rom" +) + +func TestAppendDeviceVhostUser(t *testing.T) { + + vhostuserBlkDevice := VhostUserDevice{ + SocketPath: "/tmp/nonexistentsocket.socket", + CharDevID: "char2", + TypeDevID: "", + Address: "", + VhostUserType: VhostUserBlk, + ROMFile: romfile, + } + testAppend(vhostuserBlkDevice, deviceVhostUserBlkString, t) + + vhostuserSCSIDevice := VhostUserDevice{ + SocketPath: "/tmp/nonexistentsocket.socket", + CharDevID: "char1", + TypeDevID: "scsi1", + Address: "", + VhostUserType: VhostUserSCSI, + ROMFile: romfile, + } + testAppend(vhostuserSCSIDevice, deviceVhostUserSCSIString, t) + + vhostuserNetDevice := VhostUserDevice{ + SocketPath: "/tmp/nonexistentsocket.socket", + CharDevID: "char1", + TypeDevID: "net1", + Address: "00:11:22:33:44:55", + VhostUserType: VhostUserNet, + ROMFile: romfile, + } + testAppend(vhostuserNetDevice, deviceVhostUserNetString, t) +} + +func TestAppendVirtioBalloon(t *testing.T) { + balloonDevice := BalloonDevice{ + ID: "balloon", + ROMFile: romfile, + } + + var deviceString = "-device " + string(VirtioBalloon) + "-" + string(TransportPCI) + deviceString += ",id=" + balloonDevice.ID + ",romfile=" + balloonDevice.ROMFile + + var OnDeflateOnOMM = ",deflate-on-oom=on" + var OffDeflateOnOMM = ",deflate-on-oom=off" + + var OnDisableModern = ",disable-modern=true" + var OffDisableModern = ",disable-modern=false" + + testAppend(balloonDevice, deviceString+OffDeflateOnOMM+OffDisableModern, t) + + balloonDevice.DeflateOnOOM = true + testAppend(balloonDevice, deviceString+OnDeflateOnOMM+OffDisableModern, t) + + balloonDevice.DisableModern = true + testAppend(balloonDevice, deviceString+OnDeflateOnOMM+OnDisableModern, t) + +} + +func TestAppendDevicePCIeRootPort(t *testing.T) { + var pcieRootPortID string + + // test empty ID + pcieRootPortDevice := PCIeRootPortDevice{} + if pcieRootPortDevice.Valid() { + t.Fatalf("failed to validdate empty ID") + } + + // test pref64_reserve and pre64_reserve + pcieRootPortID = "rp0" + pcieRootPortDevice = PCIeRootPortDevice{ + ID: pcieRootPortID, + Pref64Reserve: "16G", + Pref32Reserve: "256M", + } + if pcieRootPortDevice.Valid() { + t.Fatalf("failed to validate pref32-reserve and pref64-reserve for %v", pcieRootPortID) + } + + // default test + pcieRootPortID = "rp1" + pcieRootPortDevice = PCIeRootPortDevice{ + ID: pcieRootPortID, + } + if !pcieRootPortDevice.Valid() { + t.Fatalf("failed to validate for %v", pcieRootPortID) + } + testAppend(pcieRootPortDevice, devicePCIeRootPortSimpleString, t) + + // full test + pcieRootPortID = "rp2" + pcieRootPortDevice = PCIeRootPortDevice{ + ID: pcieRootPortID, + Multifunction: true, + Bus: "pcie.0", + Chassis: "0x0", + Slot: "0x1", + Addr: "0x2", + Pref64Reserve: "16G", + IOReserve: "512M", + MemReserve: "1G", + BusReserve: "0x3", + ROMFile: romfile, + } + if !pcieRootPortDevice.Valid() { + t.Fatalf("failed to validate for %v", pcieRootPortID) + } + testAppend(pcieRootPortDevice, devicePCIeRootPortFullString, t) +} + +func TestAppendDeviceVFIOPCIe(t *testing.T) { + // default test + pcieRootPortID := "rp0" + vfioDevice := VFIODevice{ + BDF: "02:00.0", + Bus: pcieRootPortID, + } + testAppend(vfioDevice, deviceVFIOPCIeSimpleString, t) + + // full test + pcieRootPortID = "rp1" + vfioDevice = VFIODevice{ + BDF: "02:00.0", + Bus: pcieRootPortID, + ROMFile: romfile, + VendorID: "0x10de", + DeviceID: "0x15f8", + } + testAppend(vfioDevice, deviceVFIOPCIeFullString, t) +} diff --git a/src/runtime/pkg/govmm/qemu/qemu_s390x_test.go b/src/runtime/pkg/govmm/qemu/qemu_s390x_test.go new file mode 100644 index 0000000000..4b99f7ea56 --- /dev/null +++ b/src/runtime/pkg/govmm/qemu/qemu_s390x_test.go @@ -0,0 +1,108 @@ +// +build s390x + +/* +// Copyright contributors to the Virtual Machine Manager for Go project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ + +package qemu + +import "testing" + +// -pci devices don't play well with Z hence replace them with corresponding -ccw devices +// See https://wiki.qemu.org/Documentation/Platforms/S390X +var ( + deviceFSString = "-device virtio-9p-ccw,fsdev=workload9p,mount_tag=rootfs,devno=" + DevNo + " -fsdev local,id=workload9p,path=/var/lib/docker/devicemapper/mnt/e31ebda2,security_model=none,multidevs=remap" + deviceFSIOMMUString = "-device virtio-9p-ccw,fsdev=workload9p,mount_tag=rootfs,iommu_platform=on,devno=" + DevNo + " -fsdev local,id=workload9p,path=/var/lib/docker/devicemapper/mnt/e31ebda2,security_model=none,multidevs=remap" + deviceNetworkString = "-netdev tap,id=tap0,vhost=on,ifname=ceth0,downscript=no,script=no -device driver=virtio-net-ccw,netdev=tap0,mac=01:02:de:ad:be:ef,devno=" + DevNo + deviceNetworkStringMq = "-netdev tap,id=tap0,vhost=on,fds=3:4 -device driver=virtio-net-ccw,netdev=tap0,mac=01:02:de:ad:be:ef,mq=on,devno=" + DevNo + deviceSerialString = "-device virtio-serial-ccw,id=serial0,devno=" + DevNo + deviceVSOCKString = "-device vhost-vsock-ccw,id=vhost-vsock-pci0,guest-cid=4,devno=" + DevNo + deviceVFIOString = "-device vfio-ccw,host=02:10.0,devno=" + DevNo + deviceSCSIControllerStr = "-device virtio-scsi-ccw,id=foo,devno=" + DevNo + deviceSCSIControllerBusAddrStr = "-device virtio-scsi-ccw,id=foo,bus=pci.0,addr=00:04.0,iothread=iothread1,devno=" + DevNo + deviceBlockString = "-device virtio-blk-ccw,drive=hd0,scsi=off,config-wce=off,devno=" + DevNo + ",share-rw=on,serial=hd0 -drive id=hd0,file=/var/lib/vm.img,aio=threads,format=qcow2,if=none,readonly" + devicePCIBridgeString = "-device pci-bridge,bus=/pci-bus/pcie.0,id=mybridge,chassis_nr=5,shpc=on,addr=ff" + devicePCIEBridgeString = "-device pcie-pci-bridge,bus=/pci-bus/pcie.0,id=mybridge,addr=ff" + romfile = "" +) + +func TestAppendVirtioBalloon(t *testing.T) { + balloonDevice := BalloonDevice{ + ID: "balloon", + } + + var deviceString = "-device " + string(VirtioBalloon) + "-" + string(TransportCCW) + deviceString += ",id=" + balloonDevice.ID + balloonDevice.DevNo = DevNo + devnoOptios := ",devno=" + DevNo + + var OnDeflateOnOMM = ",deflate-on-oom=on" + var OffDeflateOnOMM = ",deflate-on-oom=off" + testAppend(balloonDevice, deviceString+devnoOptios+OffDeflateOnOMM, t) + + balloonDevice.DeflateOnOOM = true + testAppend(balloonDevice, deviceString+devnoOptios+OnDeflateOnOMM, t) +} + +func TestAppendDeviceFSCCW(t *testing.T) { + defaultKnobs := Knobs{ + NoUserConfig: true, + } + + fsdev := FSDevice{ + Driver: Virtio9P, + FSDriver: Local, + ID: "workload9p", + Path: "/var/lib/docker/devicemapper/mnt/e31ebda2", + MountTag: "rootfs", + SecurityModel: None, + DisableModern: true, + ROMFile: "efi-virtio.rom", + Multidev: Remap, + Transport: TransportCCW, + DevNo: DevNo, + } + + var config Config + config.Knobs = defaultKnobs + + testConfigAppend(&config, fsdev, deviceFSString, t) +} + +func TestAppendDeviceFSCCWIOMMU(t *testing.T) { + defaultKnobs := Knobs{ + NoUserConfig: true, + IOMMUPlatform: true, + } + + fsdev := FSDevice{ + Driver: Virtio9P, + FSDriver: Local, + ID: "workload9p", + Path: "/var/lib/docker/devicemapper/mnt/e31ebda2", + MountTag: "rootfs", + SecurityModel: None, + DisableModern: true, + ROMFile: "efi-virtio.rom", + Multidev: Remap, + Transport: TransportCCW, + DevNo: DevNo, + } + + var config Config + config.Knobs = defaultKnobs + + testConfigAppend(&config, fsdev, deviceFSIOMMUString, t) +} diff --git a/src/runtime/pkg/govmm/qemu/qemu_test.go b/src/runtime/pkg/govmm/qemu/qemu_test.go new file mode 100644 index 0000000000..2e37b0e166 --- /dev/null +++ b/src/runtime/pkg/govmm/qemu/qemu_test.go @@ -0,0 +1,1337 @@ +/* +// Copyright contributors to the Virtual Machine Manager for Go project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ + +package qemu + +import ( + "fmt" + "io/ioutil" + "os" + "reflect" + "strings" + "testing" +) + +const agentUUID = "4cb19522-1e18-439a-883a-f9b2a3a95f5e" +const volumeUUID = "67d86208-b46c-4465-9018-e14187d4010" + +var ( + deviceNetworkPCIString = "-netdev tap,id=tap0,vhost=on,ifname=ceth0,downscript=no,script=no -device driver=virtio-net-pci,netdev=tap0,mac=01:02:de:ad:be:ef,bus=/pci-bus/pcie.0,addr=ff,disable-modern=true,romfile=efi-virtio.rom" + deviceNetworkPCIStringMq = "-netdev tap,id=tap0,vhost=on,fds=3:4 -device driver=virtio-net-pci,netdev=tap0,mac=01:02:de:ad:be:ef,bus=/pci-bus/pcie.0,addr=ff,disable-modern=true,mq=on,vectors=6,romfile=efi-virtio.rom" +) + +const DevNo = "fe.1.1234" + +func testAppend(structure interface{}, expected string, t *testing.T) { + var config Config + testConfigAppend(&config, structure, expected, t) +} + +func testConfigAppend(config *Config, structure interface{}, expected string, t *testing.T) { + switch s := structure.(type) { + case Machine: + config.Machine = s + config.appendMachine() + case FwCfg: + config.FwCfg = []FwCfg{s} + config.appendFwCfg(nil) + + case Device: + config.Devices = []Device{s} + config.appendDevices() + + case Knobs: + config.Knobs = s + config.appendKnobs() + + case Kernel: + config.Kernel = s + config.appendKernel() + + case Memory: + config.Memory = s + config.appendMemory() + + case SMP: + config.SMP = s + if err := config.appendCPUs(); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + case QMPSocket: + config.QMPSockets = []QMPSocket{s} + config.appendQMPSockets() + + case []QMPSocket: + config.QMPSockets = s + config.appendQMPSockets() + + case RTC: + config.RTC = s + config.appendRTC() + + case IOThread: + config.IOThreads = []IOThread{s} + config.appendIOThreads() + case Incoming: + config.Incoming = s + config.appendIncoming() + } + + result := strings.Join(config.qemuParams, " ") + if result != expected { + t.Fatalf("Failed to append parameters [%s] != [%s]", result, expected) + } +} + +func TestAppendMachine(t *testing.T) { + machineString := "-machine pc-lite,accel=kvm,kernel_irqchip,nvdimm" + machine := Machine{ + Type: "pc-lite", + Acceleration: "kvm,kernel_irqchip,nvdimm", + } + testAppend(machine, machineString, t) + + machineString = "-machine pc-lite,accel=kvm,kernel_irqchip,nvdimm,gic-version=host,usb=off" + machine = Machine{ + Type: "pc-lite", + Acceleration: "kvm,kernel_irqchip,nvdimm", + Options: "gic-version=host,usb=off", + } + testAppend(machine, machineString, t) + + machineString = "-machine microvm,accel=kvm,pic=off,pit=off" + machine = Machine{ + Type: "microvm", + Acceleration: "kvm", + Options: "pic=off,pit=off", + } + testAppend(machine, machineString, t) +} + +func TestAppendEmptyMachine(t *testing.T) { + machine := Machine{} + + testAppend(machine, "", t) +} + +var deviceNVDIMMString = "-device nvdimm,id=nv0,memdev=mem0,unarmed=on -object memory-backend-file,id=mem0,mem-path=/root,size=65536,readonly=on" + +func TestAppendDeviceNVDIMM(t *testing.T) { + object := Object{ + Driver: NVDIMM, + Type: MemoryBackendFile, + DeviceID: "nv0", + ID: "mem0", + MemPath: "/root", + Size: 1 << 16, + ReadOnly: true, + } + + testAppend(object, deviceNVDIMMString, t) +} + +var objectEPCString = "-object memory-backend-epc,id=epc0,size=65536,prealloc=on" + +func TestAppendEPCObject(t *testing.T) { + object := Object{ + Type: MemoryBackendEPC, + ID: "epc0", + Size: 1 << 16, + Prealloc: true, + } + + testAppend(object, objectEPCString, t) +} + +func TestAppendDeviceFS(t *testing.T) { + fsdev := FSDevice{ + Driver: Virtio9P, + FSDriver: Local, + ID: "workload9p", + Path: "/var/lib/docker/devicemapper/mnt/e31ebda2", + MountTag: "rootfs", + SecurityModel: None, + DisableModern: true, + ROMFile: "efi-virtio.rom", + Multidev: Remap, + } + + if fsdev.Transport.isVirtioCCW(nil) { + fsdev.DevNo = DevNo + } + + testAppend(fsdev, deviceFSString, t) +} + +func TestAppendDeviceNetwork(t *testing.T) { + netdev := NetDevice{ + Driver: VirtioNet, + Type: TAP, + ID: "tap0", + IFName: "ceth0", + Script: "no", + DownScript: "no", + VHost: true, + MACAddress: "01:02:de:ad:be:ef", + DisableModern: true, + ROMFile: "efi-virtio.rom", + } + + if netdev.Transport.isVirtioCCW(nil) { + netdev.DevNo = DevNo + } + + testAppend(netdev, deviceNetworkString, t) +} + +func TestAppendDeviceNetworkMq(t *testing.T) { + foo, _ := ioutil.TempFile(os.TempDir(), "govmm-qemu-test") + bar, _ := ioutil.TempFile(os.TempDir(), "govmm-qemu-test") + + defer func() { + _ = foo.Close() + _ = bar.Close() + _ = os.Remove(foo.Name()) + _ = os.Remove(bar.Name()) + }() + + netdev := NetDevice{ + Driver: VirtioNet, + Type: TAP, + ID: "tap0", + IFName: "ceth0", + Script: "no", + DownScript: "no", + FDs: []*os.File{foo, bar}, + VHost: true, + MACAddress: "01:02:de:ad:be:ef", + DisableModern: true, + ROMFile: "efi-virtio.rom", + } + if netdev.Transport.isVirtioCCW(nil) { + netdev.DevNo = DevNo + } + + testAppend(netdev, deviceNetworkStringMq, t) +} + +func TestAppendDeviceNetworkPCI(t *testing.T) { + + netdev := NetDevice{ + Driver: VirtioNet, + Type: TAP, + ID: "tap0", + IFName: "ceth0", + Bus: "/pci-bus/pcie.0", + Addr: "255", + Script: "no", + DownScript: "no", + VHost: true, + MACAddress: "01:02:de:ad:be:ef", + DisableModern: true, + ROMFile: romfile, + } + + if !netdev.Transport.isVirtioPCI(nil) { + t.Skip("Test valid only for PCI devices") + } + + testAppend(netdev, deviceNetworkPCIString, t) +} + +func TestAppendDeviceNetworkPCIMq(t *testing.T) { + foo, _ := ioutil.TempFile(os.TempDir(), "govmm-qemu-test") + bar, _ := ioutil.TempFile(os.TempDir(), "govmm-qemu-test") + + defer func() { + _ = foo.Close() + _ = bar.Close() + _ = os.Remove(foo.Name()) + _ = os.Remove(bar.Name()) + }() + + netdev := NetDevice{ + Driver: VirtioNet, + Type: TAP, + ID: "tap0", + IFName: "ceth0", + Bus: "/pci-bus/pcie.0", + Addr: "255", + Script: "no", + DownScript: "no", + FDs: []*os.File{foo, bar}, + VHost: true, + MACAddress: "01:02:de:ad:be:ef", + DisableModern: true, + ROMFile: romfile, + } + + if !netdev.Transport.isVirtioPCI(nil) { + t.Skip("Test valid only for PCI devices") + } + + testAppend(netdev, deviceNetworkPCIStringMq, t) +} + +var deviceLegacySerialString = "-serial chardev:tlserial0" + +func TestAppendLegacySerial(t *testing.T) { + sdev := LegacySerialDevice{ + Chardev: "tlserial0", + } + + testAppend(sdev, deviceLegacySerialString, t) +} + +var deviceLegacySerialPortString = "-chardev file,id=char0,path=/tmp/serial.log" + +func TestAppendDeviceLegacySerialPort(t *testing.T) { + chardev := CharDevice{ + Driver: LegacySerial, + Backend: File, + ID: "char0", + Path: "/tmp/serial.log", + } + testAppend(chardev, deviceLegacySerialPortString, t) +} + +func TestAppendDeviceSerial(t *testing.T) { + sdev := SerialDevice{ + Driver: VirtioSerial, + ID: "serial0", + DisableModern: true, + ROMFile: romfile, + MaxPorts: 2, + } + if sdev.Transport.isVirtioCCW(nil) { + sdev.DevNo = DevNo + } + + testAppend(sdev, deviceSerialString, t) +} + +var deviceSerialPortString = "-device virtserialport,chardev=char0,id=channel0,name=channel.0 -chardev socket,id=char0,path=/tmp/char.sock,server=on,wait=off" + +func TestAppendDeviceSerialPort(t *testing.T) { + chardev := CharDevice{ + Driver: VirtioSerialPort, + Backend: Socket, + ID: "char0", + DeviceID: "channel0", + Path: "/tmp/char.sock", + Name: "channel.0", + } + if chardev.Transport.isVirtioCCW(nil) { + chardev.DevNo = DevNo + } + testAppend(chardev, deviceSerialPortString, t) +} + +func TestAppendDeviceBlock(t *testing.T) { + blkdev := BlockDevice{ + Driver: VirtioBlock, + ID: "hd0", + File: "/var/lib/vm.img", + AIO: Threads, + Format: QCOW2, + Interface: NoInterface, + SCSI: false, + WCE: false, + DisableModern: true, + ROMFile: romfile, + ShareRW: true, + ReadOnly: true, + } + if blkdev.Transport.isVirtioCCW(nil) { + blkdev.DevNo = DevNo + } + testAppend(blkdev, deviceBlockString, t) +} + +func TestAppendDeviceVFIO(t *testing.T) { + vfioDevice := VFIODevice{ + BDF: "02:10.0", + ROMFile: romfile, + VendorID: "0x1234", + DeviceID: "0x5678", + } + + if vfioDevice.Transport.isVirtioCCW(nil) { + vfioDevice.DevNo = DevNo + } + + testAppend(vfioDevice, deviceVFIOString, t) +} + +func TestAppendVSOCK(t *testing.T) { + vsockDevice := VSOCKDevice{ + ID: "vhost-vsock-pci0", + ContextID: 4, + VHostFD: nil, + DisableModern: true, + ROMFile: romfile, + } + + if vsockDevice.Transport.isVirtioCCW(nil) { + vsockDevice.DevNo = DevNo + } + + testAppend(vsockDevice, deviceVSOCKString, t) +} + +func TestVSOCKValid(t *testing.T) { + vsockDevice := VSOCKDevice{ + ID: "vhost-vsock-pci0", + ContextID: MinimalGuestCID - 1, + VHostFD: nil, + DisableModern: true, + } + + if vsockDevice.Valid() { + t.Fatalf("VSOCK Context ID is not valid") + } + + vsockDevice.ContextID = MaxGuestCID + 1 + + if vsockDevice.Valid() { + t.Fatalf("VSOCK Context ID is not valid") + } + + vsockDevice.ID = "" + + if vsockDevice.Valid() { + t.Fatalf("VSOCK ID is not valid") + } +} + +func TestAppendVirtioRng(t *testing.T) { + var objectString = "-object rng-random,id=rng0" + var deviceString = "-device " + string(VirtioRng) + + rngDevice := RngDevice{ + ID: "rng0", + ROMFile: romfile, + } + + deviceString += "-" + rngDevice.Transport.getName(nil) + ",rng=rng0" + if romfile != "" { + deviceString = deviceString + ",romfile=efi-virtio.rom" + } + + if rngDevice.Transport.isVirtioCCW(nil) { + rngDevice.DevNo = DevNo + deviceString += ",devno=" + rngDevice.DevNo + } + + testAppend(rngDevice, objectString+" "+deviceString, t) + + rngDevice.Filename = "/dev/urandom" + objectString += ",filename=" + rngDevice.Filename + + testAppend(rngDevice, objectString+" "+deviceString, t) + + rngDevice.MaxBytes = 20 + + deviceString += fmt.Sprintf(",max-bytes=%d", rngDevice.MaxBytes) + testAppend(rngDevice, objectString+" "+deviceString, t) + + rngDevice.Period = 500 + + deviceString += fmt.Sprintf(",period=%d", rngDevice.Period) + testAppend(rngDevice, objectString+" "+deviceString, t) + +} + +func TestVirtioRngValid(t *testing.T) { + rng := RngDevice{ + ID: "", + } + + if rng.Valid() { + t.Fatalf("rng should be not valid when ID is empty") + } + + rng.ID = "rng0" + if !rng.Valid() { + t.Fatalf("rng should be valid") + } + +} + +func TestVirtioBalloonValid(t *testing.T) { + balloon := BalloonDevice{ + ID: "", + } + + if balloon.Valid() { + t.Fatalf("balloon should be not valid when ID is empty") + } + + balloon.ID = "balloon0" + if !balloon.Valid() { + t.Fatalf("balloon should be valid") + } +} + +func TestAppendDeviceSCSIController(t *testing.T) { + scsiCon := SCSIController{ + ID: "foo", + ROMFile: romfile, + } + + if scsiCon.Transport.isVirtioCCW(nil) { + scsiCon.DevNo = DevNo + } + + testAppend(scsiCon, deviceSCSIControllerStr, t) + + scsiCon.Bus = "pci.0" + scsiCon.Addr = "00:04.0" + scsiCon.DisableModern = true + scsiCon.IOThread = "iothread1" + testAppend(scsiCon, deviceSCSIControllerBusAddrStr, t) +} + +func TestAppendPCIBridgeDevice(t *testing.T) { + + bridge := BridgeDevice{ + Type: PCIBridge, + ID: "mybridge", + Bus: "/pci-bus/pcie.0", + Addr: "255", + Chassis: 5, + SHPC: true, + ROMFile: romfile, + } + + testAppend(bridge, devicePCIBridgeString, t) +} + +func TestAppendPCIBridgeDeviceWithReservations(t *testing.T) { + + bridge := BridgeDevice{ + Type: PCIBridge, + ID: "mybridge", + Bus: "/pci-bus/pcie.0", + Addr: "255", + Chassis: 5, + SHPC: false, + ROMFile: romfile, + IOReserve: "4k", + MemReserve: "1m", + Pref64Reserve: "1m", + } + + testAppend(bridge, devicePCIBridgeStringReserved, t) +} + +func TestAppendPCIEBridgeDevice(t *testing.T) { + + bridge := BridgeDevice{ + Type: PCIEBridge, + ID: "mybridge", + Bus: "/pci-bus/pcie.0", + Addr: "255", + ROMFile: "efi-virtio.rom", + } + + testAppend(bridge, devicePCIEBridgeString, t) +} + +func TestAppendEmptyDevice(t *testing.T) { + device := SerialDevice{} + + testAppend(device, "", t) +} + +func TestAppendKnobsAllTrue(t *testing.T) { + var knobsString = "-no-user-config -nodefaults -nographic --no-reboot -daemonize -overcommit mem-lock=on -S" + knobs := Knobs{ + NoUserConfig: true, + NoDefaults: true, + NoGraphic: true, + NoReboot: true, + Daemonize: true, + MemPrealloc: true, + FileBackedMem: true, + MemShared: true, + Mlock: true, + Stopped: true, + } + + testAppend(knobs, knobsString, t) +} + +func TestAppendKnobsAllFalse(t *testing.T) { + var knobsString = "" + knobs := Knobs{ + NoUserConfig: false, + NoDefaults: false, + NoGraphic: false, + NoReboot: false, + MemPrealloc: false, + FileBackedMem: false, + MemShared: false, + Mlock: false, + Stopped: false, + } + + testAppend(knobs, knobsString, t) +} + +func TestAppendMemoryHugePages(t *testing.T) { + conf := &Config{ + Memory: Memory{ + Size: "1G", + Slots: 8, + MaxMem: "3G", + Path: "foobar", + }, + } + memString := "-m 1G,slots=8,maxmem=3G" + testConfigAppend(conf, conf.Memory, memString, t) + + knobs := Knobs{ + HugePages: true, + MemPrealloc: true, + FileBackedMem: true, + MemShared: true, + } + objMemString := "-object memory-backend-file,id=dimm1,size=1G,mem-path=/dev/hugepages,share=on,prealloc=on" + numaMemString := "-numa node,memdev=dimm1" + memBackendString := "-machine memory-backend=dimm1" + + knobsString := objMemString + " " + if isDimmSupported(nil) { + knobsString += numaMemString + } else { + knobsString += memBackendString + } + + testConfigAppend(conf, knobs, memString+" "+knobsString, t) +} + +func TestAppendMemoryMemPrealloc(t *testing.T) { + conf := &Config{ + Memory: Memory{ + Size: "1G", + Slots: 8, + MaxMem: "3G", + Path: "foobar", + }, + } + memString := "-m 1G,slots=8,maxmem=3G" + testConfigAppend(conf, conf.Memory, memString, t) + + knobs := Knobs{ + MemPrealloc: true, + MemShared: true, + } + objMemString := "-object memory-backend-ram,id=dimm1,size=1G,share=on,prealloc=on" + numaMemString := "-numa node,memdev=dimm1" + memBackendString := "-machine memory-backend=dimm1" + + knobsString := objMemString + " " + if isDimmSupported(nil) { + knobsString += numaMemString + } else { + knobsString += memBackendString + } + + testConfigAppend(conf, knobs, memString+" "+knobsString, t) +} + +func TestAppendMemoryMemShared(t *testing.T) { + conf := &Config{ + Memory: Memory{ + Size: "1G", + Slots: 8, + MaxMem: "3G", + Path: "foobar", + }, + } + memString := "-m 1G,slots=8,maxmem=3G" + testConfigAppend(conf, conf.Memory, memString, t) + + knobs := Knobs{ + FileBackedMem: true, + MemShared: true, + } + objMemString := "-object memory-backend-file,id=dimm1,size=1G,mem-path=foobar,share=on" + numaMemString := "-numa node,memdev=dimm1" + memBackendString := "-machine memory-backend=dimm1" + + knobsString := objMemString + " " + if isDimmSupported(nil) { + knobsString += numaMemString + } else { + knobsString += memBackendString + } + + testConfigAppend(conf, knobs, memString+" "+knobsString, t) +} + +func TestAppendMemoryFileBackedMem(t *testing.T) { + conf := &Config{ + Memory: Memory{ + Size: "1G", + Slots: 8, + MaxMem: "3G", + Path: "foobar", + }, + } + memString := "-m 1G,slots=8,maxmem=3G" + testConfigAppend(conf, conf.Memory, memString, t) + + knobs := Knobs{ + FileBackedMem: true, + MemShared: false, + } + objMemString := "-object memory-backend-file,id=dimm1,size=1G,mem-path=foobar" + numaMemString := "-numa node,memdev=dimm1" + memBackendString := "-machine memory-backend=dimm1" + + knobsString := objMemString + " " + if isDimmSupported(nil) { + knobsString += numaMemString + } else { + knobsString += memBackendString + } + + testConfigAppend(conf, knobs, memString+" "+knobsString, t) +} + +func TestAppendMemoryFileBackedMemPrealloc(t *testing.T) { + conf := &Config{ + Memory: Memory{ + Size: "1G", + Slots: 8, + MaxMem: "3G", + Path: "foobar", + }, + } + memString := "-m 1G,slots=8,maxmem=3G" + testConfigAppend(conf, conf.Memory, memString, t) + + knobs := Knobs{ + FileBackedMem: true, + MemShared: true, + MemPrealloc: true, + } + objMemString := "-object memory-backend-file,id=dimm1,size=1G,mem-path=foobar,share=on,prealloc=on" + numaMemString := "-numa node,memdev=dimm1" + memBackendString := "-machine memory-backend=dimm1" + + knobsString := objMemString + " " + if isDimmSupported(nil) { + knobsString += numaMemString + } else { + knobsString += memBackendString + } + + testConfigAppend(conf, knobs, memString+" "+knobsString, t) +} + +func TestNoRebootKnob(t *testing.T) { + conf := &Config{} + + knobs := Knobs{ + NoReboot: true, + } + knobsString := "--no-reboot" + + testConfigAppend(conf, knobs, knobsString, t) +} + +var kernelString = "-kernel /opt/vmlinux.container -initrd /opt/initrd.container -append root=/dev/pmem0p1 rootflags=dax,data=ordered,errors=remount-ro rw rootfstype=ext4 tsc=reliable" + +func TestAppendKernel(t *testing.T) { + kernel := Kernel{ + Path: "/opt/vmlinux.container", + InitrdPath: "/opt/initrd.container", + Params: "root=/dev/pmem0p1 rootflags=dax,data=ordered,errors=remount-ro rw rootfstype=ext4 tsc=reliable", + } + + testAppend(kernel, kernelString, t) +} + +var memoryString = "-m 2G,slots=2,maxmem=3G" + +func TestAppendMemory(t *testing.T) { + memory := Memory{ + Size: "2G", + Slots: 2, + MaxMem: "3G", + Path: "", + } + + testAppend(memory, memoryString, t) +} + +var cpusString = "-smp 2,cores=1,threads=2,sockets=2,maxcpus=6" + +func TestAppendCPUs(t *testing.T) { + smp := SMP{ + CPUs: 2, + Sockets: 2, + Cores: 1, + Threads: 2, + MaxCPUs: 6, + } + + testAppend(smp, cpusString, t) +} + +func TestFailToAppendCPUs(t *testing.T) { + config := Config{ + SMP: SMP{ + CPUs: 2, + Sockets: 2, + Cores: 1, + Threads: 2, + MaxCPUs: 1, + }, + } + + if err := config.appendCPUs(); err == nil { + t.Fatalf("Expected appendCPUs to fail") + } +} + +var qmpSingleSocketServerString = "-qmp unix:cc-qmp,server=on,wait=off" +var qmpSingleSocketString = "-qmp unix:cc-qmp" + +func TestAppendSingleQMPSocketServer(t *testing.T) { + qmp := QMPSocket{ + Type: "unix", + Name: "cc-qmp", + Server: true, + NoWait: true, + } + + testAppend(qmp, qmpSingleSocketServerString, t) +} + +func TestAppendSingleQMPSocket(t *testing.T) { + qmp := QMPSocket{ + Type: Unix, + Name: "cc-qmp", + Server: false, + } + + testAppend(qmp, qmpSingleSocketString, t) +} + +var qmpSocketServerString = "-qmp unix:cc-qmp-1,server=on,wait=off -qmp unix:cc-qmp-2,server=on,wait=off" + +func TestAppendQMPSocketServer(t *testing.T) { + qmp := []QMPSocket{ + { + Type: "unix", + Name: "cc-qmp-1", + Server: true, + NoWait: true, + }, + { + Type: "unix", + Name: "cc-qmp-2", + Server: true, + NoWait: true, + }, + } + + testAppend(qmp, qmpSocketServerString, t) +} + +var pidfile = "/run/vc/vm/iamsandboxid/pidfile" +var logfile = "/run/vc/vm/iamsandboxid/logfile" +var qemuString = "-name cc-qemu -cpu host -uuid " + agentUUID + " -pidfile " + pidfile + " -D " + logfile + +func TestAppendStrings(t *testing.T) { + config := Config{ + Path: "qemu", + Name: "cc-qemu", + UUID: agentUUID, + CPUModel: "host", + PidFile: pidfile, + LogFile: logfile, + } + + config.appendName() + config.appendCPUModel() + config.appendUUID() + config.appendPidFile() + config.appendLogFile() + + result := strings.Join(config.qemuParams, " ") + if result != qemuString { + t.Fatalf("Failed to append parameters [%s] != [%s]", result, qemuString) + } +} + +var rtcString = "-rtc base=utc,driftfix=slew,clock=host" + +func TestAppendRTC(t *testing.T) { + rtc := RTC{ + Base: UTC, + Clock: Host, + DriftFix: Slew, + } + + testAppend(rtc, rtcString, t) +} + +var ioThreadString = "-object iothread,id=iothread1" + +func TestAppendIOThread(t *testing.T) { + ioThread := IOThread{ + ID: "iothread1", + } + + testAppend(ioThread, ioThreadString, t) +} + +var incomingStringFD = "-S -incoming fd:3" + +func TestAppendIncomingFD(t *testing.T) { + source := Incoming{ + MigrationType: MigrationFD, + FD: os.Stdout, + } + + testAppend(source, incomingStringFD, t) +} + +var incomingStringExec = "-S -incoming exec:test migration cmd" + +func TestAppendIncomingExec(t *testing.T) { + source := Incoming{ + MigrationType: MigrationExec, + Exec: "test migration cmd", + } + + testAppend(source, incomingStringExec, t) +} + +var incomingStringDefer = "-S -incoming defer" + +func TestAppendIncomingDefer(t *testing.T) { + source := Incoming{ + MigrationType: MigrationDefer, + } + + testAppend(source, incomingStringDefer, t) +} + +func TestBadName(t *testing.T) { + c := &Config{} + c.appendName() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestBadMachine(t *testing.T) { + c := &Config{} + c.appendMachine() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestBadCPUModel(t *testing.T) { + c := &Config{} + c.appendCPUModel() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestBadQMPSockets(t *testing.T) { + c := &Config{} + c.appendQMPSockets() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } + + c = &Config{ + QMPSockets: []QMPSocket{{}}, + } + + c.appendQMPSockets() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } + + c = &Config{ + QMPSockets: []QMPSocket{{Name: "test"}}, + } + + c.appendQMPSockets() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } + + c = &Config{ + QMPSockets: []QMPSocket{ + { + Name: "test", + Type: QMPSocketType("ip"), + }, + }, + } + + c.appendQMPSockets() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestBadDevices(t *testing.T) { + c := &Config{} + c.appendDevices() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } + + c = &Config{ + Devices: []Device{ + FSDevice{}, + FSDevice{ + ID: "id0", + MountTag: "tag", + }, + CharDevice{}, + CharDevice{ + ID: "id1", + }, + NetDevice{}, + NetDevice{ + ID: "id1", + IFName: "if", + Type: IPVTAP, + }, + SerialDevice{}, + SerialDevice{ + ID: "id0", + }, + BlockDevice{}, + BlockDevice{ + Driver: "drv", + ID: "id1", + }, + VhostUserDevice{}, + VhostUserDevice{ + CharDevID: "devid", + }, + VhostUserDevice{ + CharDevID: "devid", + SocketPath: "/var/run/sock", + }, + VhostUserDevice{ + CharDevID: "devid", + SocketPath: "/var/run/sock", + VhostUserType: VhostUserNet, + }, + VhostUserDevice{ + CharDevID: "devid", + SocketPath: "/var/run/sock", + VhostUserType: VhostUserSCSI, + }, + }, + } + + c.appendDevices() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestBadRTC(t *testing.T) { + c := &Config{} + c.appendRTC() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } + + c = &Config{ + RTC: RTC{ + Clock: RTCClock("invalid"), + }, + } + c.appendRTC() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } + + c = &Config{ + RTC: RTC{ + Clock: Host, + DriftFix: RTCDriftFix("invalid"), + }, + } + c.appendRTC() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestBadGlobalParam(t *testing.T) { + c := &Config{} + c.appendGlobalParam() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestBadPFlash(t *testing.T) { + c := &Config{} + c.appendPFlashParam() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestValidPFlash(t *testing.T) { + c := &Config{} + c.PFlash = []string{"flash0", "flash1"} + c.appendPFlashParam() + expected := []string{"-pflash", "flash0", "-pflash", "flash1"} + ok := reflect.DeepEqual(expected, c.qemuParams) + if !ok { + t.Errorf("Expected %v, found %v", expected, c.qemuParams) + } +} + +func TestBadSeccompSandbox(t *testing.T) { + c := &Config{} + c.appendSeccompSandbox() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestValidSeccompSandbox(t *testing.T) { + c := &Config{} + c.SeccompSandbox = string("on,obsolete=deny") + c.appendSeccompSandbox() + expected := []string{"-sandbox", "on,obsolete=deny"} + ok := reflect.DeepEqual(expected, c.qemuParams) + if !ok { + t.Errorf("Expected %v, found %v", expected, c.qemuParams) + } +} + +func TestBadVGA(t *testing.T) { + c := &Config{} + c.appendVGA() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestBadKernel(t *testing.T) { + c := &Config{} + c.appendKernel() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestBadMemoryKnobs(t *testing.T) { + c := &Config{} + c.appendMemoryKnobs() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } + + c = &Config{ + Knobs: Knobs{ + HugePages: true, + }, + } + c.appendMemoryKnobs() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } + + c = &Config{ + Knobs: Knobs{ + MemShared: true, + }, + } + c.appendMemoryKnobs() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } + + c = &Config{ + Knobs: Knobs{ + MemPrealloc: true, + }, + } + c.appendMemoryKnobs() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestBadBios(t *testing.T) { + c := &Config{} + c.appendBios() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestBadIOThreads(t *testing.T) { + c := &Config{} + c.appendIOThreads() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } + + c = &Config{ + IOThreads: []IOThread{{ID: ""}}, + } + c.appendIOThreads() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestBadIncoming(t *testing.T) { + c := &Config{} + c.appendIncoming() + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +func TestBadCPUs(t *testing.T) { + c := &Config{} + if err := c.appendCPUs(); err != nil { + t.Fatalf("No error expected got %v", err) + } + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } + + c = &Config{ + SMP: SMP{ + MaxCPUs: 1, + CPUs: 2, + }, + } + if c.appendCPUs() == nil { + t.Errorf("Error expected") + } +} + +func TestBadFwcfg(t *testing.T) { + c := &Config{} + c.appendFwCfg(nil) + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } + + c = &Config{ + FwCfg: []FwCfg{ + { + Name: "name=opt/com.mycompany/blob", + File: "./my_blob.bin", + Str: "foo", + }, + }, + } + c.appendFwCfg(nil) + if len(c.qemuParams) != 0 { + t.Errorf("Expected empty qemuParams, found %s", c.qemuParams) + } +} + +var ( + vIommuString = "-device intel-iommu,intremap=on,device-iotlb=on,caching-mode=on" + vIommuNoCacheString = "-device intel-iommu,intremap=on,device-iotlb=on,caching-mode=off" +) + +func TestIommu(t *testing.T) { + iommu := IommuDev{ + Intremap: true, + DeviceIotlb: true, + CachingMode: true, + } + + if !iommu.Valid() { + t.Fatalf("iommu should be valid") + } + + testAppend(iommu, vIommuString, t) + + iommu.CachingMode = false + + testAppend(iommu, vIommuNoCacheString, t) + +} + +func TestAppendFwcfg(t *testing.T) { + fwcfgString := "-fw_cfg name=opt/com.mycompany/blob,file=./my_blob.bin" + fwcfg := FwCfg{ + Name: "opt/com.mycompany/blob", + File: "./my_blob.bin", + } + testAppend(fwcfg, fwcfgString, t) + + fwcfgString = "-fw_cfg name=opt/com.mycompany/blob,string=foo" + fwcfg = FwCfg{ + Name: "opt/com.mycompany/blob", + Str: "foo", + } + testAppend(fwcfg, fwcfgString, t) +} + +func TestAppendPVPanicDevice(t *testing.T) { + testCases := []struct { + dev Device + out string + }{ + {nil, ""}, + {PVPanicDevice{}, "-device pvpanic"}, + {PVPanicDevice{NoShutdown: true}, "-device pvpanic -no-shutdown"}, + } + + for _, tc := range testCases { + testAppend(tc.dev, tc.out, t) + } +} + +func TestLoaderDevice(t *testing.T) { + testCases := []struct { + dev Device + out string + }{ + {nil, ""}, + {LoaderDevice{}, ""}, + {LoaderDevice{File: "f"}, ""}, + {LoaderDevice{ID: "id"}, ""}, + {LoaderDevice{File: "f", ID: "id"}, "-device loader,file=f,id=id"}, + } + + for _, tc := range testCases { + testAppend(tc.dev, tc.out, t) + } +} diff --git a/src/runtime/pkg/govmm/qemu/qmp.go b/src/runtime/pkg/govmm/qemu/qmp.go new file mode 100644 index 0000000000..2e30c2ba9d --- /dev/null +++ b/src/runtime/pkg/govmm/qemu/qmp.go @@ -0,0 +1,1664 @@ +/* +// Copyright contributors to the Virtual Machine Manager for Go project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ + +package qemu + +import ( + "bufio" + "container/list" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "os" + "strconv" + "syscall" + "time" + + "context" + "strings" +) + +// QMPLog is a logging interface used by the qemu package to log various +// interesting pieces of information. Rather than introduce a dependency +// on a given logging package, qemu presents this interface that allows +// clients to provide their own logging type which they can use to +// seamlessly integrate qemu's logs into their own logs. A QMPLog +// implementation can be specified in the QMPConfig structure. +type QMPLog interface { + // V returns true if the given argument is less than or equal + // to the implementation's defined verbosity level. + V(int32) bool + + // Infof writes informational output to the log. A newline will be + // added to the output if one is not provided. + Infof(string, ...interface{}) + + // Warningf writes warning output to the log. A newline will be + // added to the output if one is not provided. + Warningf(string, ...interface{}) + + // Errorf writes error output to the log. A newline will be + // added to the output if one is not provided. + Errorf(string, ...interface{}) +} + +type qmpNullLogger struct{} + +func (l qmpNullLogger) V(level int32) bool { + return false +} + +func (l qmpNullLogger) Infof(format string, v ...interface{}) { +} + +func (l qmpNullLogger) Warningf(format string, v ...interface{}) { +} + +func (l qmpNullLogger) Errorf(format string, v ...interface{}) { +} + +// QMPConfig is a configuration structure that can be used to specify a +// logger and a channel to which logs and QMP events are to be sent. If +// neither of these fields are specified, or are set to nil, no logs will be +// written and no QMP events will be reported to the client. +type QMPConfig struct { + // eventCh can be specified by clients who wish to receive QMP + // events. + EventCh chan<- QMPEvent + + // logger is used by the qmpStart function and all the go routines + // it spawns to log information. + Logger QMPLog + + // specify the capacity of buffer used by receive QMP response. + MaxCapacity int +} + +type qmpEventFilter struct { + eventName string + dataKey string + dataValue string +} + +// QMPEvent contains a single QMP event, sent on the QMPConfig.EventCh channel. +type QMPEvent struct { + // The name of the event, e.g., DEVICE_DELETED + Name string + + // The data associated with the event. The contents of this map are + // unprocessed by the qemu package. It is simply the result of + // unmarshalling the QMP json event. Here's an example map + // map[string]interface{}{ + // "driver": "virtio-blk-pci", + // "drive": "drive_3437843748734873483", + // } + Data map[string]interface{} + + // The event's timestamp converted to a time.Time object. + Timestamp time.Time +} + +type qmpResult struct { + response interface{} + err error +} + +type qmpCommand struct { + ctx context.Context + res chan qmpResult + name string + args map[string]interface{} + filter *qmpEventFilter + resultReceived bool + oob []byte +} + +// QMP is a structure that contains the internal state used by startQMPLoop and +// the go routines it spwans. All the contents of this structure are private. +type QMP struct { + cmdCh chan qmpCommand + conn io.ReadWriteCloser + cfg QMPConfig + connectedCh chan<- *QMPVersion + disconnectedCh chan struct{} + version *QMPVersion +} + +// QMPVersion contains the version number and the capabailities of a QEMU +// instance, as reported in the QMP greeting message. +type QMPVersion struct { + Major int + Minor int + Micro int + Capabilities []string +} + +// CPUProperties contains the properties of a CPU instance +type CPUProperties struct { + Node int `json:"node-id"` + Socket int `json:"socket-id"` + Die int `json:"die-id"` + Core int `json:"core-id"` + Thread int `json:"thread-id"` +} + +// HotpluggableCPU represents a hotpluggable CPU +type HotpluggableCPU struct { + Type string `json:"type"` + VcpusCount int `json:"vcpus-count"` + Properties CPUProperties `json:"props"` + QOMPath string `json:"qom-path"` +} + +// MemoryDevicesData cotains the data describes a memory device +type MemoryDevicesData struct { + Slot int `json:"slot"` + Node int `json:"node"` + Addr uint64 `json:"addr"` + Memdev string `json:"memdev"` + ID string `json:"id"` + Hotpluggable bool `json:"hotpluggable"` + Hotplugged bool `json:"hotplugged"` + Size uint64 `json:"size"` +} + +// MemoryDevices represents memory devices of vm +type MemoryDevices struct { + Data MemoryDevicesData `json:"data"` + Type string `json:"type"` +} + +// CPUInfo represents information about each virtual CPU +type CPUInfo struct { + CPU int `json:"CPU"` + Current bool `json:"current"` + Halted bool `json:"halted"` + QomPath string `json:"qom_path"` + Arch string `json:"arch"` + Pc int `json:"pc"` + ThreadID int `json:"thread_id"` + Props CPUProperties `json:"props"` +} + +// CPUInfoFast represents information about each virtual CPU +type CPUInfoFast struct { + CPUIndex int `json:"cpu-index"` + QomPath string `json:"qom-path"` + Arch string `json:"arch"` + ThreadID int `json:"thread-id"` + Target string `json:"target"` + Props CPUProperties `json:"props"` +} + +// MigrationRAM represents migration ram status +type MigrationRAM struct { + Total int64 `json:"total"` + Remaining int64 `json:"remaining"` + Transferred int64 `json:"transferred"` + TotalTime int64 `json:"total-time"` + SetupTime int64 `json:"setup-time"` + ExpectedDowntime int64 `json:"expected-downtime"` + Duplicate int64 `json:"duplicate"` + Normal int64 `json:"normal"` + NormalBytes int64 `json:"normal-bytes"` + DirtySyncCount int64 `json:"dirty-sync-count"` +} + +// MigrationDisk represents migration disk status +type MigrationDisk struct { + Total int64 `json:"total"` + Remaining int64 `json:"remaining"` + Transferred int64 `json:"transferred"` +} + +// MigrationXbzrleCache represents migration XbzrleCache status +type MigrationXbzrleCache struct { + CacheSize int64 `json:"cache-size"` + Bytes int64 `json:"bytes"` + Pages int64 `json:"pages"` + CacheMiss int64 `json:"cache-miss"` + CacheMissRate int64 `json:"cache-miss-rate"` + Overflow int64 `json:"overflow"` +} + +// MigrationStatus represents migration status of a vm +type MigrationStatus struct { + Status string `json:"status"` + Capabilities []map[string]interface{} `json:"capabilities,omitempty"` + RAM MigrationRAM `json:"ram,omitempty"` + Disk MigrationDisk `json:"disk,omitempty"` + XbzrleCache MigrationXbzrleCache `json:"xbzrle-cache,omitempty"` +} + +// SchemaInfo represents all QMP wire ABI +type SchemaInfo struct { + MetaType string `json:"meta-type"` + Name string `json:"name"` +} + +// StatusInfo represents guest running status +type StatusInfo struct { + Running bool `json:"running"` + SingleStep bool `json:"singlestep"` + Status string `json:"status"` +} + +func (q *QMP) readLoop(fromVMCh chan<- []byte) { + scanner := bufio.NewScanner(q.conn) + if q.cfg.MaxCapacity > 0 { + buffer := make([]byte, q.cfg.MaxCapacity) + scanner.Buffer(buffer, q.cfg.MaxCapacity) + } + + for scanner.Scan() { + line := scanner.Bytes() + // Since []byte channel type transfer slice info(include slice underlying array pointer, len, cap) + // between channel sender and receiver. scanner.Bytes() returned slice's underlying array + // may point to data that will be overwritten by a subsequent call to Scan(reference from: + // https://golang.org/pkg/bufio/#Scanner.Bytes), which may make receiver read mixed data, + // so we need to copy line to new allocated space and then send to channel receiver + sendLine := make([]byte, len(line)) + copy(sendLine, line) + + fromVMCh <- sendLine + } + q.cfg.Logger.Infof("scanner return error: %v", scanner.Err()) + close(fromVMCh) +} + +func (q *QMP) processQMPEvent(cmdQueue *list.List, name interface{}, data interface{}, + timestamp interface{}) { + + strname, ok := name.(string) + if !ok { + return + } + + var eventData map[string]interface{} + if data != nil { + eventData, _ = data.(map[string]interface{}) + } + + cmdEl := cmdQueue.Front() + if cmdEl != nil { + cmd := cmdEl.Value.(*qmpCommand) + filter := cmd.filter + if filter != nil { + if filter.eventName == strname { + match := filter.dataKey == "" + if !match && eventData != nil { + match = eventData[filter.dataKey] == filter.dataValue + } + if match { + if cmd.resultReceived { + q.finaliseCommand(cmdEl, cmdQueue, true) + } else { + cmd.filter = nil + } + } + } + } + } + + if q.cfg.EventCh != nil { + ev := QMPEvent{ + Name: strname, + Data: eventData, + } + if timestamp != nil { + timestamp, ok := timestamp.(map[string]interface{}) + if ok { + seconds, _ := timestamp["seconds"].(float64) + microseconds, _ := timestamp["microseconds"].(float64) + ev.Timestamp = time.Unix(int64(seconds), int64(microseconds)) + } + } + + q.cfg.EventCh <- ev + } +} + +func (q *QMP) finaliseCommandWithResponse(cmdEl *list.Element, cmdQueue *list.List, succeeded bool, response interface{}) { + cmd := cmdEl.Value.(*qmpCommand) + cmdQueue.Remove(cmdEl) + select { + case <-cmd.ctx.Done(): + default: + if succeeded { + cmd.res <- qmpResult{response: response} + } else { + cmd.res <- qmpResult{err: fmt.Errorf("QMP command failed: %v", response)} + } + } + if cmdQueue.Len() > 0 { + q.writeNextQMPCommand(cmdQueue) + } +} + +func (q *QMP) finaliseCommand(cmdEl *list.Element, cmdQueue *list.List, succeeded bool) { + q.finaliseCommandWithResponse(cmdEl, cmdQueue, succeeded, nil) +} + +func (q *QMP) errorDesc(errorData interface{}) (string, error) { + // convert error to json + data, err := json.Marshal(errorData) + if err != nil { + return "", fmt.Errorf("unable to extract error information: %v", err) + } + + // see: https://github.com/qemu/qemu/blob/stable-2.12/qapi/qmp-dispatch.c#L125 + var qmpErr map[string]string + // convert json to qmpError + if err = json.Unmarshal(data, &qmpErr); err != nil { + return "", fmt.Errorf("unable to convert json to qmpError: %v", err) + } + + return qmpErr["desc"], nil +} + +func (q *QMP) processQMPInput(line []byte, cmdQueue *list.List) { + var vmData map[string]interface{} + err := json.Unmarshal(line, &vmData) + if err != nil { + q.cfg.Logger.Warningf("Unable to decode response [%s] from VM: %v", + string(line), err) + return + } + if evname, found := vmData["event"]; found { + q.processQMPEvent(cmdQueue, evname, vmData["data"], vmData["timestamp"]) + return + } + + response, succeeded := vmData["return"] + errData, failed := vmData["error"] + + if !succeeded && !failed { + return + } + + cmdEl := cmdQueue.Front() + if cmdEl == nil { + q.cfg.Logger.Warningf("Unexpected command response received [%s] from VM", + string(line)) + return + } + cmd := cmdEl.Value.(*qmpCommand) + if failed || cmd.filter == nil { + if errData != nil { + desc, err := q.errorDesc(errData) + if err != nil { + q.cfg.Logger.Infof("Get error description failed: %v", err) + } else { + response = desc + } + } + q.finaliseCommandWithResponse(cmdEl, cmdQueue, succeeded, response) + } else { + cmd.resultReceived = true + } +} + +func currentCommandDoneCh(cmdQueue *list.List) <-chan struct{} { + cmdEl := cmdQueue.Front() + if cmdEl == nil { + return nil + } + cmd := cmdEl.Value.(*qmpCommand) + return cmd.ctx.Done() +} + +func (q *QMP) writeNextQMPCommand(cmdQueue *list.List) { + cmdEl := cmdQueue.Front() + cmd := cmdEl.Value.(*qmpCommand) + cmdData := make(map[string]interface{}) + cmdData["execute"] = cmd.name + if cmd.args != nil { + cmdData["arguments"] = cmd.args + } + encodedCmd, err := json.Marshal(&cmdData) + if err != nil { + cmd.res <- qmpResult{ + err: fmt.Errorf("unable to marhsall command %s: %v", + cmd.name, err), + } + cmdQueue.Remove(cmdEl) + } + encodedCmd = append(encodedCmd, '\n') + if unixConn, ok := q.conn.(*net.UnixConn); ok && len(cmd.oob) > 0 { + _, _, err = unixConn.WriteMsgUnix(encodedCmd, cmd.oob, nil) + } else { + _, err = q.conn.Write(encodedCmd) + } + + if err != nil { + cmd.res <- qmpResult{ + err: fmt.Errorf("unable to write command to qmp socket %v", err), + } + cmdQueue.Remove(cmdEl) + } +} + +func failOutstandingCommands(cmdQueue *list.List) { + for e := cmdQueue.Front(); e != nil; e = e.Next() { + cmd := e.Value.(*qmpCommand) + select { + case cmd.res <- qmpResult{ + err: errors.New("exitting QMP loop, command cancelled"), + }: + case <-cmd.ctx.Done(): + } + } +} + +func (q *QMP) cancelCurrentCommand(cmdQueue *list.List) { + cmdEl := cmdQueue.Front() + cmd := cmdEl.Value.(*qmpCommand) + if cmd.resultReceived { + q.finaliseCommand(cmdEl, cmdQueue, false) + } else { + cmd.filter = nil + } +} + +func (q *QMP) parseVersion(version []byte) *QMPVersion { + var qmp map[string]interface{} + err := json.Unmarshal(version, &qmp) + if err != nil { + q.cfg.Logger.Errorf("Invalid QMP greeting: %s", string(version)) + return nil + } + + versionMap := qmp + for _, k := range []string{"QMP", "version", "qemu"} { + versionMap, _ = versionMap[k].(map[string]interface{}) + if versionMap == nil { + return nil + } + } + + micro, _ := versionMap["micro"].(float64) + minor, _ := versionMap["minor"].(float64) + major, _ := versionMap["major"].(float64) + capabilities, _ := qmp["QMP"].(map[string]interface{})["capabilities"].([]interface{}) + stringcaps := make([]string, 0, len(capabilities)) + for _, c := range capabilities { + if cap, ok := c.(string); ok { + stringcaps = append(stringcaps, cap) + } + } + return &QMPVersion{Major: int(major), + Minor: int(minor), + Micro: int(micro), + Capabilities: stringcaps, + } +} + +// The qemu package allows multiple QMP commands to be submitted concurrently +// from different Go routines. Unfortunately, QMP doesn't really support parallel +// commands as there is no way reliable way to associate a command response +// with a request. For this reason we need to submit our commands to +// QMP serially. The qemu package performs this serialisation using a +// queue (cmdQueue owned by mainLoop). We use a queue rather than a simple +// mutex so we can support cancelling of commands (see below) and ordered +// execution of commands, i.e., if command B is issued before command C, +// it should be executed before command C even if both commands are initially +// blocked waiting for command A to finish. This would be hard to achieve with +// a simple mutex. +// +// Cancelling is a little tricky. Commands such as ExecuteQMPCapabilities +// can be cancelled by cancelling or timing out their contexts. When a +// command is cancelled the calling function, e.g., ExecuteQMPCapabilities, +// will return but we may not be able to remove the command's entry from +// the command queue or issue the next command. There are two scenarios +// here. +// +// 1. The command has been processed by QMP, i.e., we have received a +// return or an error, but is still blocking as it is waiting for +// an event. For example, the ExecuteDeviceDel blocks until a DEVICE_DELETED +// event is received. When such a command is cancelled we can remove it +// from the queue and start issuing the next command. When the DEVICE_DELETED +// event eventually arrives it will just be ignored. +// +// 2. The command has not been processed by QMP. In this case the command +// needs to remain on the cmdQueue until the response to this command is +// received from QMP. During this time no new commands can be issued. When the +// response is received, it is discarded (as no one is interested in the result +// any more), the entry is removed from the cmdQueue and we can proceed to +// execute the next command. + +func (q *QMP) mainLoop() { + cmdQueue := list.New().Init() + fromVMCh := make(chan []byte) + go q.readLoop(fromVMCh) + + defer func() { + if q.cfg.EventCh != nil { + close(q.cfg.EventCh) + } + /* #nosec */ + _ = q.conn.Close() + <-fromVMCh + failOutstandingCommands(cmdQueue) + close(q.disconnectedCh) + }() + + var cmdDoneCh <-chan struct{} + var version *QMPVersion + ready := false + + for { + select { + case cmd, ok := <-q.cmdCh: + if !ok { + return + } + _ = cmdQueue.PushBack(&cmd) + + // We only want to execute the new cmd if QMP is + // ready and there are no other commands pending. + // If there are commands pending our new command + // will get run when the pending commands complete. + if ready && cmdQueue.Len() == 1 { + q.writeNextQMPCommand(cmdQueue) + cmdDoneCh = currentCommandDoneCh(cmdQueue) + } + + case line, ok := <-fromVMCh: + if !ok { + return + } + + if !ready { + // Not ready yet. Check if line is the QMP version. + // Sometimes QMP events are thrown before the QMP version, + // hence it's not a guarantee that the first data read from + // the channel is the QMP version. + version = q.parseVersion(line) + if version != nil { + q.connectedCh <- version + ready = true + } + // Do not process QMP input to avoid deadlocks. + break + } + + q.processQMPInput(line, cmdQueue) + cmdDoneCh = currentCommandDoneCh(cmdQueue) + + case <-cmdDoneCh: + q.cancelCurrentCommand(cmdQueue) + cmdDoneCh = currentCommandDoneCh(cmdQueue) + } + } +} + +func startQMPLoop(conn io.ReadWriteCloser, cfg QMPConfig, + connectedCh chan<- *QMPVersion, disconnectedCh chan struct{}) *QMP { + q := &QMP{ + cmdCh: make(chan qmpCommand), + conn: conn, + cfg: cfg, + connectedCh: connectedCh, + disconnectedCh: disconnectedCh, + } + go q.mainLoop() + return q +} + +func (q *QMP) executeCommandWithResponse(ctx context.Context, name string, args map[string]interface{}, + oob []byte, filter *qmpEventFilter) (interface{}, error) { + var err error + var response interface{} + resCh := make(chan qmpResult) + select { + case <-q.disconnectedCh: + err = errors.New("exitting QMP loop, command cancelled") + case q.cmdCh <- qmpCommand{ + ctx: ctx, + res: resCh, + name: name, + args: args, + filter: filter, + oob: oob, + }: + } + + if err != nil { + return response, err + } + + select { + case res := <-resCh: + err = res.err + response = res.response + case <-ctx.Done(): + err = ctx.Err() + } + + return response, err +} + +func (q *QMP) executeCommand(ctx context.Context, name string, args map[string]interface{}, + filter *qmpEventFilter) error { + + _, err := q.executeCommandWithResponse(ctx, name, args, nil, filter) + return err +} + +// QMPStart connects to a unix domain socket maintained by a QMP instance. It +// waits to receive the QMP welcome message via the socket and spawns some go +// routines to manage the socket. The function returns a *QMP which can be +// used by callers to send commands to the QEMU instance or to close the +// socket and all the go routines that have been spawned to monitor it. A +// *QMPVersion is also returned. This structure contains the version and +// capabilities information returned by the QEMU instance in its welcome +// message. +// +// socket contains the path to the domain socket. cfg contains some options +// that can be specified by the caller, namely where the qemu package should +// send logs and QMP events. disconnectedCh is a channel that must be supplied +// by the caller. It is closed when an error occurs openning or writing to +// or reading from the unix domain socket. This implies that the QEMU instance +// that opened the socket has closed. +// +// If this function returns without error, callers should call QMP.Shutdown +// when they wish to stop monitoring the QMP instance. This is not strictly +// necessary if the QEMU instance exits and the disconnectedCh is closed, but +// doing so will not cause any problems. +// +// Commands can be sent to the QEMU instance via the QMP.Execute methods. +// These commands are executed serially, even if the QMP.Execute methods +// are called from different go routines. The QMP.Execute methods will +// block until they have received a success or failure message from QMP, +// i.e., {"return": {}} or {"error":{}}, and in some cases certain events +// are received. +// +// QEMU currently requires that the "qmp_capabilties" command is sent before any +// other command. Therefore you must call qmp.ExecuteQMPCapabilities() before +// you execute any other command. +func QMPStart(ctx context.Context, socket string, cfg QMPConfig, disconnectedCh chan struct{}) (*QMP, *QMPVersion, error) { + if cfg.Logger == nil { + cfg.Logger = qmpNullLogger{} + } + dialer := net.Dialer{Cancel: ctx.Done()} + conn, err := dialer.Dial("unix", socket) + if err != nil { + cfg.Logger.Warningf("Unable to connect to unix socket (%s): %v", socket, err) + close(disconnectedCh) + return nil, nil, err + } + + connectedCh := make(chan *QMPVersion) + + q := startQMPLoop(conn, cfg, connectedCh, disconnectedCh) + select { + case <-ctx.Done(): + q.Shutdown() + <-disconnectedCh + return nil, nil, fmt.Errorf("canceled by caller") + case <-disconnectedCh: + return nil, nil, fmt.Errorf("lost connection to VM") + case q.version = <-connectedCh: + if q.version == nil { + return nil, nil, fmt.Errorf("failed to find QMP version information") + } + } + + if q.version.Major < 5 { + return nil, nil, fmt.Errorf("govmm requires qemu version 5.0 or later, this is qemu (%d.%d)", q.version.Major, q.version.Minor) + } + + return q, q.version, nil +} + +// Shutdown closes the domain socket used to monitor a QEMU instance and +// terminates all the go routines spawned by QMPStart to manage that instance. +// QMP.Shutdown does not shut down the running instance. Calling QMP.Shutdown +// will result in the disconnectedCh channel being closed, indicating that we +// have lost connection to the QMP instance. In this case it does not indicate +// that the instance has quit. +// +// QMP.Shutdown should not be called concurrently with other QMP methods. It +// should not be called twice on the same QMP instance. +// +// Calling QMP.Shutdown after the disconnectedCh channel is closed is permitted but +// will not have any effect. +func (q *QMP) Shutdown() { + close(q.cmdCh) +} + +// ExecuteQMPCapabilities executes the qmp_capabilities command on the instance. +func (q *QMP) ExecuteQMPCapabilities(ctx context.Context) error { + return q.executeCommand(ctx, "qmp_capabilities", nil, nil) +} + +// ExecuteStop sends the stop command to the instance. +func (q *QMP) ExecuteStop(ctx context.Context) error { + return q.executeCommand(ctx, "stop", nil, nil) +} + +// ExecuteCont sends the cont command to the instance. +func (q *QMP) ExecuteCont(ctx context.Context) error { + return q.executeCommand(ctx, "cont", nil, nil) +} + +// ExecuteSystemPowerdown sends the system_powerdown command to the instance. +// This function will block until the SHUTDOWN event is received. +func (q *QMP) ExecuteSystemPowerdown(ctx context.Context) error { + filter := &qmpEventFilter{ + eventName: "POWERDOWN", + } + return q.executeCommand(ctx, "system_powerdown", nil, filter) +} + +// ExecuteQuit sends the quit command to the instance, terminating +// the QMP instance immediately. +func (q *QMP) ExecuteQuit(ctx context.Context) error { + return q.executeCommand(ctx, "quit", nil, nil) +} + +func (q *QMP) blockdevAddBaseArgs(driver, device, blockdevID string, ro bool) (map[string]interface{}, map[string]interface{}) { + var args map[string]interface{} + + blockdevArgs := map[string]interface{}{ + "driver": "raw", + "read-only": ro, + "file": map[string]interface{}{ + "driver": driver, + "filename": device, + }, + } + + blockdevArgs["node-name"] = blockdevID + args = blockdevArgs + + return args, blockdevArgs +} + +// ExecuteBlockdevAdd sends a blockdev-add to the QEMU instance. device is the +// path of the device to add, e.g., /dev/rdb0, and blockdevID is an identifier +// used to name the device. As this identifier will be passed directly to QMP, +// it must obey QMP's naming rules, e,g., it must start with a letter. +func (q *QMP) ExecuteBlockdevAdd(ctx context.Context, device, blockdevID string, ro bool) error { + args, _ := q.blockdevAddBaseArgs("host_device", device, blockdevID, ro) + + return q.executeCommand(ctx, "blockdev-add", args, nil) +} + +// ExecuteBlockdevAddWithCache has two more parameters direct and noFlush +// than ExecuteBlockdevAdd. +// They are cache-related options for block devices that are described in +// https://github.com/qemu/qemu/blob/master/qapi/block-core.json. +// direct denotes whether use of O_DIRECT (bypass the host page cache) +// is enabled. noFlush denotes whether flush requests for the device are +// ignored. +func (q *QMP) ExecuteBlockdevAddWithCache(ctx context.Context, device, blockdevID string, direct, noFlush, ro bool) error { + args, blockdevArgs := q.blockdevAddBaseArgs("host_device", device, blockdevID, ro) + + blockdevArgs["cache"] = map[string]interface{}{ + "direct": direct, + "no-flush": noFlush, + } + + return q.executeCommand(ctx, "blockdev-add", args, nil) +} + +// ExecuteBlockdevAddWithDriverCache has three one parameter driver +// than ExecuteBlockdevAddWithCache. +// Parameter driver can set the driver of block device. +func (q *QMP) ExecuteBlockdevAddWithDriverCache(ctx context.Context, driver, device, blockdevID string, direct, noFlush, ro bool) error { + args, blockdevArgs := q.blockdevAddBaseArgs(driver, device, blockdevID, ro) + + blockdevArgs["cache"] = map[string]interface{}{ + "direct": direct, + "no-flush": noFlush, + } + + return q.executeCommand(ctx, "blockdev-add", args, nil) +} + +// ExecuteDeviceAdd adds the guest portion of a device to a QEMU instance +// using the device_add command. blockdevID should match the blockdevID passed +// to a previous call to ExecuteBlockdevAdd. devID is the id of the device to +// add. Both strings must be valid QMP identifiers. driver is the name of the +// driver,e.g., virtio-blk-pci, and bus is the name of the bus. bus is optional. +// shared denotes if the drive can be shared allowing it to be passed more than once. +// disableModern indicates if virtio version 1.0 should be replaced by the +// former version 0.9, as there is a KVM bug that occurs when using virtio +// 1.0 in nested environments. +func (q *QMP) ExecuteDeviceAdd(ctx context.Context, blockdevID, devID, driver, bus, romfile string, shared, disableModern bool) error { + args := map[string]interface{}{ + "id": devID, + "driver": driver, + "drive": blockdevID, + } + + var transport VirtioTransport + + if transport.isVirtioCCW(nil) { + args["devno"] = bus + } else if bus != "" { + args["bus"] = bus + } + + if shared { + args["share-rw"] = "on" + } + if transport.isVirtioPCI(nil) { + args["romfile"] = romfile + + if disableModern { + args["disable-modern"] = disableModern + } + } + + return q.executeCommand(ctx, "device_add", args, nil) +} + +// ExecuteSCSIDeviceAdd adds the guest portion of a block device to a QEMU instance +// using a SCSI driver with the device_add command. blockdevID should match the +// blockdevID passed to a previous call to ExecuteBlockdevAdd. devID is the id of +// the device to add. Both strings must be valid QMP identifiers. driver is the name of the +// scsi driver,e.g., scsi-hd, and bus is the name of a SCSI controller bus. +// scsiID is the SCSI id, lun is logical unit number. scsiID and lun are optional, a negative value +// for scsiID and lun is ignored. shared denotes if the drive can be shared allowing it +// to be passed more than once. +// disableModern indicates if virtio version 1.0 should be replaced by the +// former version 0.9, as there is a KVM bug that occurs when using virtio +// 1.0 in nested environments. +func (q *QMP) ExecuteSCSIDeviceAdd(ctx context.Context, blockdevID, devID, driver, bus, romfile string, scsiID, lun int, shared, disableModern bool) error { + // TBD: Add drivers for scsi passthrough like scsi-generic and scsi-block + drivers := []string{"scsi-hd", "scsi-cd", "scsi-disk"} + + isSCSIDriver := false + for _, d := range drivers { + if driver == d { + isSCSIDriver = true + break + } + } + + if !isSCSIDriver { + return fmt.Errorf("invalid SCSI driver provided %s", driver) + } + + args := map[string]interface{}{ + "id": devID, + "driver": driver, + "drive": blockdevID, + "bus": bus, + } + + if scsiID >= 0 { + args["scsi-id"] = scsiID + } + if lun >= 0 { + args["lun"] = lun + } + if shared { + args["share-rw"] = "on" + } + + return q.executeCommand(ctx, "device_add", args, nil) +} + +// ExecuteBlockdevDel deletes a block device by sending blockdev-del +// command. blockdevID is the id of the block device to be deleted. +// Typically, this will match the id passed to ExecuteBlockdevAdd. It +// must be a valid QMP id. +func (q *QMP) ExecuteBlockdevDel(ctx context.Context, blockdevID string) error { + args := map[string]interface{}{} + + args["node-name"] = blockdevID + return q.executeCommand(ctx, "blockdev-del", args, nil) +} + +// ExecuteChardevDel deletes a char device by sending a chardev-remove command. +// chardevID is the id of the char device to be deleted. Typically, this will +// match the id passed to ExecuteCharDevUnixSocketAdd. It must be a valid QMP id. +func (q *QMP) ExecuteChardevDel(ctx context.Context, chardevID string) error { + args := map[string]interface{}{ + "id": chardevID, + } + + return q.executeCommand(ctx, "chardev-remove", args, nil) +} + +// ExecuteNetdevAdd adds a Net device to a QEMU instance +// using the netdev_add command. netdevID is the id of the device to add. +// Must be valid QMP identifier. +func (q *QMP) ExecuteNetdevAdd(ctx context.Context, netdevType, netdevID, ifname, downscript, script string, queues int) error { + args := map[string]interface{}{ + "type": netdevType, + "id": netdevID, + "ifname": ifname, + "downscript": downscript, + "script": script, + } + if queues > 1 { + args["queues"] = queues + } + + return q.executeCommand(ctx, "netdev_add", args, nil) +} + +// ExecuteNetdevChardevAdd adds a Net device to a QEMU instance +// using the netdev_add command. netdevID is the id of the device to add. +// Must be valid QMP identifier. +func (q *QMP) ExecuteNetdevChardevAdd(ctx context.Context, netdevType, netdevID, chardev string, queues int) error { + args := map[string]interface{}{ + "type": netdevType, + "id": netdevID, + "chardev": chardev, + } + if queues > 1 { + args["queues"] = queues + } + + return q.executeCommand(ctx, "netdev_add", args, nil) +} + +// ExecuteNetdevAddByFds adds a Net device to a QEMU instance +// using the netdev_add command by fds and vhostfds. netdevID is the id of the device to add. +// Must be valid QMP identifier. +func (q *QMP) ExecuteNetdevAddByFds(ctx context.Context, netdevType, netdevID string, fdNames, vhostFdNames []string) error { + fdNameStr := strings.Join(fdNames, ":") + args := map[string]interface{}{ + "type": netdevType, + "id": netdevID, + "fds": fdNameStr, + } + if len(vhostFdNames) > 0 { + vhostFdNameStr := strings.Join(vhostFdNames, ":") + args["vhost"] = true + args["vhostfds"] = vhostFdNameStr + } + + return q.executeCommand(ctx, "netdev_add", args, nil) +} + +// ExecuteNetdevDel deletes a Net device from a QEMU instance +// using the netdev_del command. netdevID is the id of the device to delete. +func (q *QMP) ExecuteNetdevDel(ctx context.Context, netdevID string) error { + args := map[string]interface{}{ + "id": netdevID, + } + return q.executeCommand(ctx, "netdev_del", args, nil) +} + +// ExecuteNetPCIDeviceAdd adds a Net PCI device to a QEMU instance +// using the device_add command. devID is the id of the device to add. +// Must be valid QMP identifier. netdevID is the id of nic added by previous netdev_add. +// queues is the number of queues of a nic. +// disableModern indicates if virtio version 1.0 should be replaced by the +// former version 0.9, as there is a KVM bug that occurs when using virtio +// 1.0 in nested environments. +func (q *QMP) ExecuteNetPCIDeviceAdd(ctx context.Context, netdevID, devID, macAddr, addr, bus, romfile string, queues int, disableModern bool) error { + args := map[string]interface{}{ + "id": devID, + "driver": VirtioNetPCI, + "romfile": romfile, + } + + if bus != "" { + args["bus"] = bus + } + if addr != "" { + args["addr"] = addr + } + if macAddr != "" { + args["mac"] = macAddr + } + if netdevID != "" { + args["netdev"] = netdevID + } + if disableModern { + args["disable-modern"] = disableModern + } + + if queues > 0 { + // (2N+2 vectors, N for tx queues, N for rx queues, 1 for config, and one for possible control vq) + // -device virtio-net-pci,mq=on,vectors=2N+2... + // enable mq in guest by 'ethtool -L eth0 combined $queue_num' + // Clearlinux automatically sets up the queues properly + // The agent implementation should do this to ensure that it is + // always set + args["mq"] = "on" + args["vectors"] = 2*queues + 2 + } + + return q.executeCommand(ctx, "device_add", args, nil) +} + +// ExecuteNetCCWDeviceAdd adds a Net CCW device to a QEMU instance +// using the device_add command. devID is the id of the device to add. +// Must be valid QMP identifier. netdevID is the id of nic added by previous netdev_add. +// queues is the number of queues of a nic. +func (q *QMP) ExecuteNetCCWDeviceAdd(ctx context.Context, netdevID, devID, macAddr, bus string, queues int) error { + args := map[string]interface{}{ + "id": devID, + "driver": VirtioNetCCW, + "netdev": netdevID, + "mac": macAddr, + "devno": bus, + } + + if queues > 0 { + args["mq"] = "on" + } + + return q.executeCommand(ctx, "device_add", args, nil) +} + +// ExecuteDeviceDel deletes guest portion of a QEMU device by sending a +// device_del command. devId is the identifier of the device to delete. +// Typically it would match the devID parameter passed to an earlier call +// to ExecuteDeviceAdd. It must be a valid QMP identidier. +// +// This method blocks until a DEVICE_DELETED event is received for devID. +func (q *QMP) ExecuteDeviceDel(ctx context.Context, devID string) error { + args := map[string]interface{}{ + "id": devID, + } + filter := &qmpEventFilter{ + eventName: "DEVICE_DELETED", + dataKey: "device", + dataValue: devID, + } + return q.executeCommand(ctx, "device_del", args, filter) +} + +// ExecutePCIDeviceAdd is the PCI version of ExecuteDeviceAdd. This function can be used +// to hot plug PCI devices on PCI(E) bridges, unlike ExecuteDeviceAdd this function receive the +// device address on its parent bus. bus is optional. queues specifies the number of queues of +// a block device. shared denotes if the drive can be shared allowing it to be passed more than once. +// disableModern indicates if virtio version 1.0 should be replaced by the +// former version 0.9, as there is a KVM bug that occurs when using virtio +// 1.0 in nested environments. +func (q *QMP) ExecutePCIDeviceAdd(ctx context.Context, blockdevID, devID, driver, addr, bus, romfile string, queues int, shared, disableModern bool) error { + args := map[string]interface{}{ + "id": devID, + "driver": driver, + "drive": blockdevID, + "addr": addr, + } + if bus != "" { + args["bus"] = bus + } + if shared { + args["share-rw"] = "on" + } + if queues > 0 { + args["num-queues"] = strconv.Itoa(queues) + } + + var transport VirtioTransport + + if transport.isVirtioPCI(nil) { + args["romfile"] = romfile + + if disableModern { + args["disable-modern"] = disableModern + } + } + + return q.executeCommand(ctx, "device_add", args, nil) +} + +// ExecutePCIVhostUserDevAdd adds a vhost-user device to a QEMU instance using the device_add command. +// This function can be used to hot plug vhost-user devices on PCI(E) bridges. +// It receives the bus and the device address on its parent bus. bus is optional. +// devID is the id of the device to add.Must be valid QMP identifier. chardevID +// is the QMP identifier of character device using a unix socket as backend. +// driver is the name of vhost-user driver, like vhost-user-blk-pci. +func (q *QMP) ExecutePCIVhostUserDevAdd(ctx context.Context, driver, devID, chardevID, addr, bus string) error { + args := map[string]interface{}{ + "driver": driver, + "id": devID, + "chardev": chardevID, + "addr": addr, + } + + if bus != "" { + args["bus"] = bus + } + + return q.executeCommand(ctx, "device_add", args, nil) +} + +// ExecuteVFIODeviceAdd adds a VFIO device to a QEMU instance using the device_add command. +// devID is the id of the device to add. Must be valid QMP identifier. +// bdf is the PCI bus-device-function of the pci device. +// bus is optional. When hot plugging a PCIe device, the bus can be the ID of the pcie-root-port. +func (q *QMP) ExecuteVFIODeviceAdd(ctx context.Context, devID, bdf, bus, romfile string) error { + var driver string + var transport VirtioTransport + + if transport.isVirtioCCW(nil) { + driver = string(VfioCCW) + } else { + driver = string(VfioPCI) + } + + args := map[string]interface{}{ + "id": devID, + "driver": driver, + "host": bdf, + "romfile": romfile, + } + if bus != "" { + args["bus"] = bus + } + return q.executeCommand(ctx, "device_add", args, nil) +} + +// ExecutePCIVFIODeviceAdd adds a VFIO device to a QEMU instance using the device_add command. +// This function can be used to hot plug VFIO devices on PCI(E) bridges, unlike +// ExecuteVFIODeviceAdd this function receives the bus and the device address on its parent bus. +// bus is optional. devID is the id of the device to add.Must be valid QMP identifier. bdf is the +// PCI bus-device-function of the pci device. +func (q *QMP) ExecutePCIVFIODeviceAdd(ctx context.Context, devID, bdf, addr, bus, romfile string) error { + args := map[string]interface{}{ + "id": devID, + "driver": VfioPCI, + "host": bdf, + "addr": addr, + "romfile": romfile, + } + + if bus != "" { + args["bus"] = bus + } + return q.executeCommand(ctx, "device_add", args, nil) +} + +// ExecutePCIVFIOMediatedDeviceAdd adds a VFIO mediated device to a QEMU instance using the device_add command. +// This function can be used to hot plug VFIO mediated devices on PCI(E) bridges or root bus, unlike +// ExecuteVFIODeviceAdd this function receives the bus and the device address on its parent bus. +// devID is the id of the device to add. Must be valid QMP identifier. sysfsdev is the VFIO mediated device. +// Both bus and addr are optional. If they are both set to be empty, the system will pick up an empty slot on root bus. +func (q *QMP) ExecutePCIVFIOMediatedDeviceAdd(ctx context.Context, devID, sysfsdev, addr, bus, romfile string) error { + args := map[string]interface{}{ + "id": devID, + "driver": VfioPCI, + "sysfsdev": sysfsdev, + "romfile": romfile, + } + + if bus != "" { + args["bus"] = bus + } + if addr != "" { + args["addr"] = addr + } + return q.executeCommand(ctx, "device_add", args, nil) +} + +// ExecuteAPVFIOMediatedDeviceAdd adds a VFIO mediated AP device to a QEMU instance using the device_add command. +func (q *QMP) ExecuteAPVFIOMediatedDeviceAdd(ctx context.Context, sysfsdev string) error { + args := map[string]interface{}{ + "driver": VfioAP, + "sysfsdev": sysfsdev, + } + return q.executeCommand(ctx, "device_add", args, nil) +} + +// isSocketIDSupported returns if the cpu driver supports the socket id option +func isSocketIDSupported(driver string) bool { + if driver == "host-s390x-cpu" || driver == "host-powerpc64-cpu" { + return false + } + return true +} + +// isThreadIDSupported returns if the cpu driver supports the thread id option +func isThreadIDSupported(driver string) bool { + if driver == "host-s390x-cpu" || driver == "host-powerpc64-cpu" { + return false + } + return true +} + +// isDieIDSupported returns if the cpu driver and the qemu version support the die id option +func (q *QMP) isDieIDSupported(driver string) bool { + return driver == "host-x86_64-cpu" +} + +// ExecuteCPUDeviceAdd adds a CPU to a QEMU instance using the device_add command. +// driver is the CPU model, cpuID must be a unique ID to identify the CPU, socketID is the socket number within +// node/board the CPU belongs to, coreID is the core number within socket the CPU belongs to, threadID is the +// thread number within core the CPU belongs to. Note that socketID and threadID are not a requirement for +// architecures like ppc64le. +func (q *QMP) ExecuteCPUDeviceAdd(ctx context.Context, driver, cpuID, socketID, dieID, coreID, threadID, romfile string) error { + args := map[string]interface{}{ + "driver": driver, + "id": cpuID, + "core-id": coreID, + } + + if socketID != "" && isSocketIDSupported(driver) { + args["socket-id"] = socketID + } + + if threadID != "" && isThreadIDSupported(driver) { + args["thread-id"] = threadID + } + + if q.isDieIDSupported(driver) { + if dieID != "" { + args["die-id"] = dieID + } + } + + return q.executeCommand(ctx, "device_add", args, nil) +} + +// ExecuteQueryHotpluggableCPUs returns a slice with the list of hotpluggable CPUs +func (q *QMP) ExecuteQueryHotpluggableCPUs(ctx context.Context) ([]HotpluggableCPU, error) { + response, err := q.executeCommandWithResponse(ctx, "query-hotpluggable-cpus", nil, nil, nil) + if err != nil { + return nil, err + } + + // convert response to json + data, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("unable to extract CPU information: %v", err) + } + + var cpus []HotpluggableCPU + // convert json to []HotpluggableCPU + if err = json.Unmarshal(data, &cpus); err != nil { + return nil, fmt.Errorf("unable to convert json to hotpluggable CPU: %v", err) + } + + return cpus, nil +} + +// ExecSetMigrationCaps sets migration capabilities +func (q *QMP) ExecSetMigrationCaps(ctx context.Context, caps []map[string]interface{}) error { + args := map[string]interface{}{ + "capabilities": caps, + } + + return q.executeCommand(ctx, "migrate-set-capabilities", args, nil) +} + +// ExecSetMigrateArguments sets the command line used for migration +func (q *QMP) ExecSetMigrateArguments(ctx context.Context, url string) error { + args := map[string]interface{}{ + "uri": url, + } + + return q.executeCommand(ctx, "migrate", args, nil) +} + +// ExecQueryMemoryDevices returns a slice with the list of memory devices +func (q *QMP) ExecQueryMemoryDevices(ctx context.Context) ([]MemoryDevices, error) { + response, err := q.executeCommandWithResponse(ctx, "query-memory-devices", nil, nil, nil) + if err != nil { + return nil, err + } + + // convert response to json + data, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("unable to extract memory devices information: %v", err) + } + + var memoryDevices []MemoryDevices + // convert json to []MemoryDevices + if err = json.Unmarshal(data, &memoryDevices); err != nil { + return nil, fmt.Errorf("unable to convert json to memory devices: %v", err) + } + + return memoryDevices, nil +} + +// ExecQueryCpus returns a slice with the list of `CpuInfo` +// Since qemu 2.12, we have `query-cpus-fast` as a better choice in production +// we can still choose `ExecQueryCpus` for compatibility though not recommended. +func (q *QMP) ExecQueryCpus(ctx context.Context) ([]CPUInfo, error) { + response, err := q.executeCommandWithResponse(ctx, "query-cpus", nil, nil, nil) + if err != nil { + return nil, err + } + + // convert response to json + data, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("unable to extract memory devices information: %v", err) + } + + var cpuInfo []CPUInfo + // convert json to []CPUInfo + if err = json.Unmarshal(data, &cpuInfo); err != nil { + return nil, fmt.Errorf("unable to convert json to CPUInfo: %v", err) + } + + return cpuInfo, nil +} + +// ExecQueryCpusFast returns a slice with the list of `CpuInfoFast` +// This is introduced since 2.12, it does not incur a performance penalty and +// should be used in production instead of query-cpus. +func (q *QMP) ExecQueryCpusFast(ctx context.Context) ([]CPUInfoFast, error) { + response, err := q.executeCommandWithResponse(ctx, "query-cpus-fast", nil, nil, nil) + if err != nil { + return nil, err + } + + // convert response to json + data, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("unable to extract memory devices information: %v", err) + } + + var cpuInfoFast []CPUInfoFast + // convert json to []CPUInfoFast + if err = json.Unmarshal(data, &cpuInfoFast); err != nil { + return nil, fmt.Errorf("unable to convert json to CPUInfoFast: %v", err) + } + + return cpuInfoFast, nil +} + +// ExecMemdevAdd adds size of MiB memory device to the guest +func (q *QMP) ExecMemdevAdd(ctx context.Context, qomtype, id, mempath string, size int, share bool, driver, driverID, addr, bus string) error { + args := map[string]interface{}{ + "qom-type": qomtype, + "id": id, + "size": uint64(size) << 20, + } + if mempath != "" { + args["mem-path"] = mempath + } + if share { + args["share"] = true + } + err := q.executeCommand(ctx, "object-add", args, nil) + if err != nil { + return err + } + + defer func() { + if err != nil { + q.cfg.Logger.Errorf("Unable to add memory device %s: %v", id, err) + err = q.executeCommand(ctx, "object-del", map[string]interface{}{"id": id}, nil) + if err != nil { + q.cfg.Logger.Warningf("Unable to clean up memory object %s: %v", id, err) + } + } + }() + + args = map[string]interface{}{ + "driver": driver, + "id": driverID, + "memdev": id, + } + + if bus != "" { + args["bus"] = bus + } + if addr != "" { + args["addr"] = addr + } + + err = q.executeCommand(ctx, "device_add", args, nil) + + return err +} + +// ExecHotplugMemory adds size of MiB memory to the guest +func (q *QMP) ExecHotplugMemory(ctx context.Context, qomtype, id, mempath string, size int, share bool) error { + return q.ExecMemdevAdd(ctx, qomtype, id, mempath, size, share, "pc-dimm", "dimm"+id, "", "") +} + +// ExecuteNVDIMMDeviceAdd adds a block device to a QEMU instance using +// a NVDIMM driver with the device_add command. +// id is the id of the device to add. It must be a valid QMP identifier. +// mempath is the path of the device to add, e.g., /dev/rdb0. size is +// the data size of the device. pmem is to guarantee the persistence of QEMU writes +// to the vNVDIMM backend. +func (q *QMP) ExecuteNVDIMMDeviceAdd(ctx context.Context, id, mempath string, size int64, pmem *bool) error { + args := map[string]interface{}{ + "qom-type": "memory-backend-file", + "id": "nvdimmbackmem" + id, + "mem-path": mempath, + "size": size, + "share": true, + } + + if pmem != nil { + args["pmem"] = *pmem + } + + err := q.executeCommand(ctx, "object-add", args, nil) + if err != nil { + return err + } + + args = map[string]interface{}{ + "driver": "nvdimm", + "id": "nvdimm" + id, + "memdev": "nvdimmbackmem" + id, + } + if err = q.executeCommand(ctx, "device_add", args, nil); err != nil { + q.cfg.Logger.Errorf("Unable to hotplug NVDIMM device: %v", err) + err2 := q.executeCommand(ctx, "object-del", map[string]interface{}{"id": "nvdimmbackmem" + id}, nil) + if err2 != nil { + q.cfg.Logger.Warningf("Unable to clean up memory object: %v", err2) + } + } + + return err +} + +// ExecuteBalloon sets the size of the balloon, hence updates the memory +// allocated for the VM. +func (q *QMP) ExecuteBalloon(ctx context.Context, bytes uint64) error { + args := map[string]interface{}{ + "value": bytes, + } + return q.executeCommand(ctx, "balloon", args, nil) +} + +// ExecutePCIVSockAdd adds a vhost-vsock-pci bus +// disableModern indicates if virtio version 1.0 should be replaced by the +// former version 0.9, as there is a KVM bug that occurs when using virtio +// 1.0 in nested environments. +func (q *QMP) ExecutePCIVSockAdd(ctx context.Context, id, guestCID, vhostfd, addr, bus, romfile string, disableModern bool) error { + args := map[string]interface{}{ + "driver": VHostVSockPCI, + "id": id, + "guest-cid": guestCID, + "vhostfd": vhostfd, + "addr": addr, + "romfile": romfile, + } + + if bus != "" { + args["bus"] = bus + } + + if disableModern { + args["disable-modern"] = disableModern + } + + return q.executeCommand(ctx, "device_add", args, nil) +} + +// ExecuteGetFD sends a file descriptor via SCM rights and assigns it a name +func (q *QMP) ExecuteGetFD(ctx context.Context, fdname string, fd *os.File) error { + oob := syscall.UnixRights(int(fd.Fd())) + args := map[string]interface{}{ + "fdname": fdname, + } + + _, err := q.executeCommandWithResponse(ctx, "getfd", args, oob, nil) + return err +} + +// ExecuteCharDevUnixSocketAdd adds a character device using as backend a unix socket, +// id is an identifier for the device, path specifies the local path of the unix socket, +// wait is to block waiting for a client to connect, server specifies that the socket is a listening socket. +func (q *QMP) ExecuteCharDevUnixSocketAdd(ctx context.Context, id, path string, wait, server bool) error { + data := map[string]interface{}{ + "server": server, + "addr": map[string]interface{}{ + "type": "unix", + "data": map[string]interface{}{ + "path": path, + }, + }, + } + + // wait is only valid for server mode + if server { + data["wait"] = wait + } + + args := map[string]interface{}{ + "id": id, + "backend": map[string]interface{}{ + "type": "socket", + "data": data, + }, + } + return q.executeCommand(ctx, "chardev-add", args, nil) +} + +// ExecuteVirtSerialPortAdd adds a virtserialport. +// id is an identifier for the virtserialport, name is a name for the virtserialport and +// it will be visible in the VM, chardev is the character device id previously added. +func (q *QMP) ExecuteVirtSerialPortAdd(ctx context.Context, id, name, chardev string) error { + args := map[string]interface{}{ + "driver": VirtioSerialPort, + "id": id, + "name": name, + "chardev": chardev, + } + + return q.executeCommand(ctx, "device_add", args, nil) +} + +// ExecuteQueryMigration queries migration progress. +func (q *QMP) ExecuteQueryMigration(ctx context.Context) (MigrationStatus, error) { + response, err := q.executeCommandWithResponse(ctx, "query-migrate", nil, nil, nil) + if err != nil { + return MigrationStatus{}, err + } + + data, err := json.Marshal(response) + if err != nil { + return MigrationStatus{}, fmt.Errorf("unable to extract migrate status information: %v", err) + } + + var status MigrationStatus + if err = json.Unmarshal(data, &status); err != nil { + return MigrationStatus{}, fmt.Errorf("unable to convert migrate status information: %v", err) + } + + return status, nil +} + +// ExecuteMigrationIncoming start migration from incoming uri. +func (q *QMP) ExecuteMigrationIncoming(ctx context.Context, uri string) error { + args := map[string]interface{}{ + "uri": uri, + } + return q.executeCommand(ctx, "migrate-incoming", args, nil) +} + +// ExecQueryQmpSchema query all QMP wire ABI and returns a slice +func (q *QMP) ExecQueryQmpSchema(ctx context.Context) ([]SchemaInfo, error) { + response, err := q.executeCommandWithResponse(ctx, "query-qmp-schema", nil, nil, nil) + if err != nil { + return nil, err + } + + // convert response to json + data, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("unable to extract memory devices information: %v", err) + } + + var schemaInfo []SchemaInfo + if err = json.Unmarshal(data, &schemaInfo); err != nil { + return nil, fmt.Errorf("unable to convert json to schemaInfo: %v", err) + } + + return schemaInfo, nil +} + +// ExecuteQueryStatus queries guest status +func (q *QMP) ExecuteQueryStatus(ctx context.Context) (StatusInfo, error) { + response, err := q.executeCommandWithResponse(ctx, "query-status", nil, nil, nil) + if err != nil { + return StatusInfo{}, err + } + + data, err := json.Marshal(response) + if err != nil { + return StatusInfo{}, fmt.Errorf("unable to extract migrate status information: %v", err) + } + + var status StatusInfo + if err = json.Unmarshal(data, &status); err != nil { + return StatusInfo{}, fmt.Errorf("unable to convert migrate status information: %v", err) + } + + return status, nil +} + +// ExecQomSet qom-set path property value +func (q *QMP) ExecQomSet(ctx context.Context, path, property string, value uint64) error { + args := map[string]interface{}{ + "path": path, + "property": property, + "value": value, + } + + return q.executeCommand(ctx, "qom-set", args, nil) +} + +// ExecQomGet qom-get path property +func (q *QMP) ExecQomGet(ctx context.Context, path, property string) (interface{}, error) { + args := map[string]interface{}{ + "path": path, + "property": property, + } + + response, err := q.executeCommandWithResponse(ctx, "qom-get", args, nil, nil) + if err != nil { + return "", err + } + + return response, nil +} + +// ExecuteDumpGuestMemory dump guest memory to host +func (q *QMP) ExecuteDumpGuestMemory(ctx context.Context, protocol string, paging bool, format string) error { + args := map[string]interface{}{ + "protocol": protocol, + "paging": paging, + "format": format, + } + + return q.executeCommand(ctx, "dump-guest-memory", args, nil) +} diff --git a/src/runtime/pkg/govmm/qemu/qmp_test.go b/src/runtime/pkg/govmm/qemu/qmp_test.go new file mode 100644 index 0000000000..83259290bb --- /dev/null +++ b/src/runtime/pkg/govmm/qemu/qmp_test.go @@ -0,0 +1,1806 @@ +/* +// Copyright contributors to the Virtual Machine Manager for Go project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ + +package qemu + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "log" + "os" + "reflect" + "sync" + "testing" + "time" + + "context" +) + +const ( + microStr = "50" + minorStr = "9" + majorStr = "2" + micro = 50 + minor = 9 + major = 2 + cap1 = "one" + cap2 = "two" + qmpHello = `{ "QMP": { "version": { "qemu": { "micro": ` + microStr + `, "minor": ` + minorStr + `, "major": ` + majorStr + ` }, "package": ""}, "capabilities": ["` + cap1 + `","` + cap2 + `"]}}` + "\n" +) + +type qmpTestLogger struct{} + +func (l qmpTestLogger) V(level int32) bool { + return true +} + +func (l qmpTestLogger) Infof(format string, v ...interface{}) { + log.Printf(format, v...) +} + +func (l qmpTestLogger) Warningf(format string, v ...interface{}) { + l.Infof(format, v...) +} + +func (l qmpTestLogger) Errorf(format string, v ...interface{}) { + l.Infof(format, v...) +} + +type qmpTestCommand struct { + name string + args map[string]interface{} +} + +type qmpTestEvent struct { + name string + data map[string]interface{} + timestamp map[string]interface{} + after time.Duration +} + +type qmpTestResult struct { + result string + data interface{} +} + +type qmpTestCommandBuffer struct { + newDataCh chan []byte + t *testing.T + buf *bytes.Buffer + cmds []qmpTestCommand + events []qmpTestEvent + results []qmpTestResult + currentCmd int + forceFail chan struct{} +} + +func newQMPTestCommandBuffer(t *testing.T) *qmpTestCommandBuffer { + b := &qmpTestCommandBuffer{ + newDataCh: make(chan []byte, 1), + t: t, + buf: bytes.NewBuffer([]byte{}), + forceFail: make(chan struct{}), + } + b.cmds = make([]qmpTestCommand, 0, 8) + b.events = make([]qmpTestEvent, 0, 8) + b.results = make([]qmpTestResult, 0, 8) + b.newDataCh <- []byte(qmpHello) + return b +} + +func newQMPTestCommandBufferNoGreeting(t *testing.T) *qmpTestCommandBuffer { + b := &qmpTestCommandBuffer{ + newDataCh: make(chan []byte, 1), + t: t, + buf: bytes.NewBuffer([]byte{}), + forceFail: make(chan struct{}), + } + b.cmds = make([]qmpTestCommand, 0, 8) + b.events = make([]qmpTestEvent, 0, 8) + b.results = make([]qmpTestResult, 0, 8) + return b +} + +func (b *qmpTestCommandBuffer) startEventLoop(wg *sync.WaitGroup) { + wg.Add(1) + go func() { + for _, ev := range b.events { + time.Sleep(ev.after) + eventMap := map[string]interface{}{ + "event": ev.name, + } + + if ev.data != nil { + eventMap["data"] = ev.data + } + + if ev.timestamp != nil { + eventMap["timestamp"] = ev.timestamp + } + + encodedEvent, err := json.Marshal(&eventMap) + if err != nil { + b.t.Errorf("Unable to encode event: %v", err) + } + encodedEvent = append(encodedEvent, '\n') + b.newDataCh <- encodedEvent + } + wg.Done() + }() +} + +func (b *qmpTestCommandBuffer) AddCommand(name string, args map[string]interface{}, + result string, data interface{}) { + b.cmds = append(b.cmds, qmpTestCommand{name, args}) + b.results = append(b.results, qmpTestResult{result, data}) +} + +func (b *qmpTestCommandBuffer) AddEvent(name string, after time.Duration, + data map[string]interface{}, timestamp map[string]interface{}) { + b.events = append(b.events, qmpTestEvent{ + name: name, + data: data, + timestamp: timestamp, + after: after, + }) +} + +func (b *qmpTestCommandBuffer) Close() error { + close(b.newDataCh) + return nil +} + +func (b *qmpTestCommandBuffer) Read(p []byte) (n int, err error) { + if b.buf.Len() == 0 { + ok := false + var data []byte + select { + case <-b.forceFail: + return 0, errors.New("Connection shutdown") + case data, ok = <-b.newDataCh: + select { + case <-b.forceFail: + return 0, errors.New("Connection shutdown") + default: + } + } + if !ok { + return 0, nil + } + _, err := b.buf.Write(data) + if err != nil { + if err != nil { + b.t.Errorf("Unable to buffer result: %v", err) + } + } + } + return b.buf.Read(p) +} + +func (b *qmpTestCommandBuffer) Write(p []byte) (int, error) { + var cmdJSON map[string]interface{} + currentCmd := b.currentCmd + b.currentCmd++ + if currentCmd >= len(b.cmds) { + b.t.Fatalf("Unexpected command") + } + err := json.Unmarshal(p, &cmdJSON) + if err != nil { + b.t.Fatalf("Unexpected command") + } + cmdName := cmdJSON["execute"] + gotCmdName := cmdName.(string) + result := b.results[currentCmd].result + if gotCmdName != b.cmds[currentCmd].name { + b.t.Errorf("Unexpected command. Expected %s found %s", + b.cmds[currentCmd].name, gotCmdName) + result = "error" + } + resultMap := make(map[string]interface{}) + resultMap[result] = b.results[currentCmd].data + encodedRes, err := json.Marshal(&resultMap) + if err != nil { + b.t.Errorf("Unable to encode result: %v", err) + } + encodedRes = append(encodedRes, '\n') + b.newDataCh <- encodedRes + return len(p), nil +} + +func checkVersion(t *testing.T, connectedCh <-chan *QMPVersion) *QMPVersion { + var version *QMPVersion + select { + case <-time.After(time.Second): + t.Fatal("Timed out waiting for qmp to connect") + case version = <-connectedCh: + } + + if version == nil { + t.Fatal("Invalid version information received") + } + if version.Micro != micro || version.Minor != minor || + version.Major != major { + t.Fatal("Invalid version number") + } + + if len(version.Capabilities) != 2 { + if version.Capabilities[0] != cap1 || version.Capabilities[1] != cap2 { + t.Fatal("Invalid capabilities") + } + } + + return version +} + +// Checks that a QMP Loop can be started and shutdown. +// +// We start a QMPLoop and shut it down. +// +// Loop should start up and shutdown correctly. The version information +// returned from startQMPLoop should be correct. +func TestQMPStartStopLoop(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + q.Shutdown() + <-disconnectedCh +} + +// Checks that a call to QMPStart with an invalid path exits gracefully. +// +// We call QMPStart with an invalid path. +// +// An error should be returned and the disconnected channel should be closed. +func TestQMPStartBadPath(t *testing.T) { + cfg := QMPConfig{Logger: qmpTestLogger{}} + disconnectedCh := make(chan struct{}) + q, _, err := QMPStart(context.Background(), "", cfg, disconnectedCh) + if err == nil { + t.Errorf("Expected error") + q.Shutdown() + } + <-disconnectedCh +} + +// Checks that the qmp_capabilities command is correctly sent. +// +// We start a QMPLoop, send the qmp_capabilities command and stop the +// loop. +// +// The qmp_capabilities should be correctly sent and the QMP loop +// should exit gracefully. +func TestQMPCapabilities(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("qmp_capabilities", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecuteQMPCapabilities(context.Background()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that an error returned by a QMP command is correctly handled. +// +// We start a QMPLoop, send the qmp_capabilities command and stop the +// loop. +// +// The qmp_capabilities command fails and yet we should exit gracefully. +func TestQMPBadCapabilities(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("qmp_capabilities", nil, "error", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecuteQMPCapabilities(context.Background()) + if err == nil { + t.Fatalf("Expected error") + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the stop command is correctly sent. +// +// We start a QMPLoop, send the stop command and stop the +// loop. +// +// The stop command should be correctly sent and the QMP loop +// should exit gracefully. +func TestQMPStop(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("stop", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecuteStop(context.Background()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the cont command is correctly sent. +// +// We start a QMPLoop, send the cont command and stop the +// loop. +// +// The cont command should be correctly sent and the QMP loop +// should exit gracefully. +func TestQMPCont(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("cont", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecuteCont(context.Background()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the quit command is correctly sent. +// +// We start a QMPLoop, send the quit command and wait for the loop to exit. +// +// The quit command should be correctly sent and the QMP loop should exit +// gracefully without the test calling q.Shutdown(). +func TestQMPQuit(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("quit", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecuteQuit(context.Background()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + close(buf.forceFail) + <-disconnectedCh +} + +// Checks that the blockdev-add command is correctly sent. +// +// We start a QMPLoop, send the blockdev-add command and stop the loop. +// +// The blockdev-add command should be correctly sent and the QMP loop should +// exit gracefully. +func TestQMPBlockdevAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("blockdev-add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + q.version = checkVersion(t, connectedCh) + err := q.ExecuteBlockdevAdd(context.Background(), "/dev/rbd0", + fmt.Sprintf("drive_%s", volumeUUID), false) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the blockdev-add with cache options command is correctly sent. +// +// We start a QMPLoop, send the blockdev-add with cache options +// command and stop the loop. +// +// The blockdev-add with cache options command should be correctly sent and +// the QMP loop should exit gracefully. +func TestQMPBlockdevAddWithCache(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("blockdev-add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + q.version = checkVersion(t, connectedCh) + err := q.ExecuteBlockdevAddWithCache(context.Background(), "/dev/rbd0", + fmt.Sprintf("drive_%s", volumeUUID), true, true, false) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the netdev_add command is correctly sent. +// +// We start a QMPLoop, send the netdev_add command and stop the loop. +// +// The netdev_add command should be correctly sent and the QMP loop should +// exit gracefully. +func TestQMPNetdevAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("netdev_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + q.version = checkVersion(t, connectedCh) + err := q.ExecuteNetdevAdd(context.Background(), "tap", "br0", "tap0", "no", "no", 8) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the netdev_add command is correctly sent. +// +// We start a QMPLoop, send the netdev_add command and stop the loop. +// +// The netdev_add command should be correctly sent and the QMP loop should +// exit gracefully. +func TestQMPNetdevChardevAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("netdev_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + q.version = checkVersion(t, connectedCh) + err := q.ExecuteNetdevChardevAdd(context.Background(), "tap", "br0", "chr0", 8) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the netdev_add command with fds is correctly sent. +// +// We start a QMPLoop, send the netdev_add command with fds and stop the loop. +// +// The netdev_add command with fds should be correctly sent and the QMP loop should +// exit gracefully. +func TestQMPNetdevAddByFds(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("netdev_add", nil, "return", nil) + buf.AddCommand("netdev_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + q.version = checkVersion(t, connectedCh) + err := q.ExecuteNetdevAddByFds(context.Background(), "tap", "br0", nil, []string{}) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + err = q.ExecuteNetdevAddByFds(context.Background(), "tap", "br1", nil, []string{"3"}) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the netdev_del command is correctly sent. +// +// We start a QMPLoop, send the netdev_del command and stop the loop. +// +// The netdev_del command should be correctly sent and the QMP loop should +// exit gracefully. +func TestQMPNetdevDel(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("netdev_del", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + q.version = checkVersion(t, connectedCh) + err := q.ExecuteNetdevDel(context.Background(), "br0") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +func TestQMPNetPCIDeviceAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecuteNetPCIDeviceAdd(context.Background(), "br0", "virtio-0", "02:42:ac:11:00:02", "0x7", "", "", 8, false) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +func TestQMPNetCCWDeviceAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecuteNetCCWDeviceAdd(context.Background(), "br0", "virtio-0", "02:42:ac:11:00:02", DevNo, 8) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the device_add command is correctly sent. +// +// We start a QMPLoop, send the device_add command and stop the loop. +// +// The device_add command should be correctly sent and the QMP loop should +// exit gracefully. +func TestQMPDeviceAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + q.version = checkVersion(t, connectedCh) + blockdevID := fmt.Sprintf("drive_%s", volumeUUID) + devID := fmt.Sprintf("device_%s", volumeUUID) + err := q.ExecuteDeviceAdd(context.Background(), blockdevID, devID, + "virtio-blk-pci", "", "", true, false) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the device_add command for scsi is correctly sent. +// +// We start a QMPLoop, send the device_add command and stop the loop. +// +// The device_add command should be correctly sent and the QMP loop should +// exit gracefully. +func TestQMPSCSIDeviceAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + q.version = checkVersion(t, connectedCh) + blockdevID := fmt.Sprintf("drive_%s", volumeUUID) + devID := fmt.Sprintf("device_%s", volumeUUID) + err := q.ExecuteSCSIDeviceAdd(context.Background(), blockdevID, devID, + "scsi-hd", "scsi0.0", "", 1, 2, true, false) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the blockdev-del command is correctly sent. +// +// We start a QMPLoop, send the blockdev-del command and stop the loop. +// +// The blockdev-del command should be correctly sent and the QMP loop should +// exit gracefully. +func TestQMPBlockdevDel(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("blockdev-del", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + q.version = checkVersion(t, connectedCh) + err := q.ExecuteBlockdevDel(context.Background(), + fmt.Sprintf("drive_%s", volumeUUID)) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the chardev-remove command is correctly sent. +// +// We start a QMPLoop, send the chardev-remove command and stop the loop. +// +// The chardev-remove command should be correctly sent and the QMP loop should +// exit gracefully. +func TestQMPChardevDel(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("chardev-remove", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + q.version = checkVersion(t, connectedCh) + err := q.ExecuteChardevDel(context.Background(), "chardev-0") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the device_del command is correctly sent. +// +// We start a QMPLoop, send the device_del command and wait for it to complete. +// This command generates some events so we start a separate go routine to check +// that they are received. +// +// The device_del command should be correctly sent and the QMP loop should +// exit gracefully. We should also receive two events on the eventCh. +func TestQMPDeviceDel(t *testing.T) { + const ( + seconds = int64(1352167040730) + microsecondsEv1 = 123456 + microsecondsEv2 = 123556 + device = "device_" + volumeUUID + path = "/dev/rbd0" + ) + + var wg sync.WaitGroup + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_del", nil, "return", nil) + buf.AddEvent("DEVICE_DELETED", time.Millisecond*200, + map[string]interface{}{ + "path": path, + }, + map[string]interface{}{ + "seconds": seconds, + "microseconds": microsecondsEv1, + }) + buf.AddEvent("DEVICE_DELETED", time.Millisecond*200, + map[string]interface{}{ + "device": device, + "path": path, + }, + map[string]interface{}{ + "seconds": seconds, + "microseconds": microsecondsEv2, + }) + eventCh := make(chan QMPEvent) + cfg := QMPConfig{EventCh: eventCh, Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + wg.Add(1) + go func() { + for i := 0; i < 2; i++ { + select { + case <-eventCh: + case <-time.After(time.Second): + t.Error("Timedout waiting for event") + } + } + wg.Done() + }() + checkVersion(t, connectedCh) + buf.startEventLoop(&wg) + err := q.ExecuteDeviceDel(context.Background(), + fmt.Sprintf("device_%s", volumeUUID)) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh + wg.Wait() +} + +// Checks that contexts can be used to timeout a command. +// +// We start a QMPLoop and send the device_del command with a context that times +// out after 1 second. We don't however arrangefor any DEVICE_DELETED events +// to be sent so the device_del command should not complete normally. We then +// shutdown the QMP loop. +// +// The device_del command should timeout after 1 second and the QMP loop +// should exit gracefully. +func TestQMPDeviceDelTimeout(t *testing.T) { + var wg sync.WaitGroup + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_del", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + err := q.ExecuteDeviceDel(ctx, + fmt.Sprintf("device_%s", volumeUUID)) + cancel() + if err != context.DeadlineExceeded { + t.Fatalf("Timeout expected found %v", err) + } + q.Shutdown() + <-disconnectedCh + wg.Wait() +} + +// Checks that contexts can be used to cancel a command. +// +// We start a QMPLoop and send two qmp_capabilities commands, cancelling +// the first. The second is allowed to proceed normally. +// +// The first call to ExecuteQMPCapabilities should fail with +// context.Canceled. The second should succeed. +func TestQMPCancel(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("qmp_capabilities", nil, "return", nil) + buf.AddCommand("qmp_capabilities", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := q.ExecuteQMPCapabilities(ctx) + if err != context.Canceled { + t.Fatalf("Unexpected error %v", err) + } + err = q.ExecuteQMPCapabilities(context.Background()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that the system_powerdown command is correctly sent. +// +// We start a QMPLoop, send the system_powerdown command and stop the loop. +// +// The system_powerdown command should be correctly sent and should return +// as we've provisioned a SHUTDOWN event. The QMP loop should exit gracefully. +func TestQMPSystemPowerdown(t *testing.T) { + const ( + seconds = int64(1352167040730) + microsecondsEv1 = 123456 + ) + + var wg sync.WaitGroup + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("system_powerdown", nil, "return", nil) + buf.AddEvent("POWERDOWN", time.Millisecond*100, + nil, + map[string]interface{}{ + "seconds": seconds, + "microseconds": microsecondsEv1, + }) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + buf.startEventLoop(&wg) + err := q.ExecuteSystemPowerdown(context.Background()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh + wg.Wait() +} + +// Checks that event commands can be cancelled. +// +// We start a QMPLoop, send the system_powerdown command. This command +// will time out after 1 second as the SHUTDOWN event never arrives. +// We then send a quit command to terminate the session. +// +// The system_powerdown command should be correctly sent but should block +// waiting for the SHUTDOWN event and should be successfully cancelled. +// The quit command should be successfully received and the QMP loop should +// exit gracefully. +func TestQMPEventedCommandCancel(t *testing.T) { + var wg sync.WaitGroup + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("system_powerdown", nil, "return", nil) + buf.AddCommand("quit", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + buf.startEventLoop(&wg) + ctx, cancelFN := context.WithTimeout(context.Background(), time.Second) + err := q.ExecuteSystemPowerdown(ctx) + cancelFN() + if err == nil { + t.Fatalf("Expected SystemPowerdown to fail") + } + err = q.ExecuteQuit(context.Background()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh + wg.Wait() +} + +// Checks that queued commands execute after an evented command is cancelled. +// +// This test is similar to the previous test with the exception that it +// tries to ensure that a second command is placed on the QMP structure's +// command queue before the evented command is cancelled. This allows us +// to test a slightly different use case. We start a QMPLoop, send the +// system_powerdown command. We do this by sending the command directly +// down the QMP.cmdCh rather than calling a higher level function as this +// allows us to ensure that we have another command queued before we +// timeout the first command. We then send a qmp_capabilities command and +// then we shutdown. +// +// The system_powerdown command should be correctly sent but should block +// waiting for the SHUTDOWN event and should be successfully cancelled. +// The query_capabilities command should be successfully received and the +// QMP loop should exit gracefully. +func TestQMPEventedCommandCancelConcurrent(t *testing.T) { + var wg sync.WaitGroup + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + + buf.AddCommand("system_powerdown", nil, "error", nil) + buf.AddCommand("qmp_capabilities", nil, "return", nil) + + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + buf.startEventLoop(&wg) + + resCh := make(chan qmpResult) + ctx, cancelFn := context.WithTimeout(context.Background(), time.Second) + q.cmdCh <- qmpCommand{ + ctx: ctx, + res: resCh, + name: "system_powerdown", + filter: &qmpEventFilter{ + eventName: "SHUTDOWN", + }, + } + + var cmdWg sync.WaitGroup + cmdWg.Add(1) + go func() { + err := q.ExecuteQMPCapabilities(context.Background()) + if err != nil { + t.Errorf("Unexpected error %v", err) + } + cmdWg.Done() + }() + + <-resCh + cancelFn() + cmdWg.Wait() + q.Shutdown() + <-disconnectedCh + wg.Wait() +} + +// Checks that events can be received and parsed. +// +// Two events are provisioned and the QMPLoop is started with an valid eventCh. +// We wait for both events to be received and check that their contents are +// correct. We then shutdown the QMP loop. +// +// Both events are received and their contents are correct. The QMP loop should +// shut down gracefully. +func TestQMPEvents(t *testing.T) { + const ( + seconds = int64(1352167040730) + microsecondsEv1 = 123456 + microsecondsEv2 = 123556 + device = "device_" + volumeUUID + path = "/dev/rbd0" + ) + var wg sync.WaitGroup + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddEvent("DEVICE_DELETED", time.Millisecond*100, + map[string]interface{}{ + "device": device, + "path": path, + }, + map[string]interface{}{ + "seconds": seconds, + "microseconds": microsecondsEv1, + }) + buf.AddEvent("POWERDOWN", time.Millisecond*200, nil, + map[string]interface{}{ + "seconds": seconds, + "microseconds": microsecondsEv2, + }) + eventCh := make(chan QMPEvent) + cfg := QMPConfig{EventCh: eventCh, Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + buf.startEventLoop(&wg) + + ev := <-eventCh + if ev.Name != "DEVICE_DELETED" { + t.Errorf("incorrect event name received. Expected %s, found %s", + "DEVICE_DELETED", ev.Name) + } + if ev.Timestamp != time.Unix(seconds, microsecondsEv1) { + t.Error("incorrect timestamp") + } + deviceName := ev.Data["device"].(string) + if deviceName != device { + t.Errorf("Unexpected device field. Expected %s, found %s", + "device_"+volumeUUID, device) + } + pathName := ev.Data["path"].(string) + if pathName != path { + t.Errorf("Unexpected path field. Expected %s, found %s", + "/dev/rbd0", path) + } + + ev = <-eventCh + if ev.Name != "POWERDOWN" { + t.Errorf("incorrect event name received. Expected %s, found %s", + "POWERDOWN", ev.Name) + } + if ev.Timestamp != time.Unix(seconds, microsecondsEv2) { + t.Error("incorrect timestamp") + } + if ev.Data != nil { + t.Errorf("event data expected to be nil") + } + + q.Shutdown() + + select { + case _, ok := <-eventCh: + if ok { + t.Errorf("Expected eventCh to be closed") + } + case <-time.After(time.Second): + t.Error("Timed out waiting for eventCh to close") + } + + <-disconnectedCh + wg.Wait() +} + +// Checks that commands issued after the QMP loop exits fail (and don't hang) +// +// We start the QMP loop but force it to fail immediately simulating a QEMU +// instance exit. We then send two qmp_cabilities commands. +// +// Both commands should fail with an error. The QMP loop should exit. +func TestQMPLostLoop(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + close(buf.forceFail) + buf.AddCommand("qmp_capabilities", nil, "return", nil) + err := q.ExecuteQMPCapabilities(context.Background()) + if err == nil { + t.Error("Expected executeQMPCapabilities to fail") + } + <-disconnectedCh + buf.AddCommand("qmp_capabilities", nil, "return", nil) + err = q.ExecuteQMPCapabilities(context.Background()) + if err == nil { + t.Error("Expected executeQMPCapabilities to fail") + } +} + +// Checks that PCI devices are correctly added using device_add. +// +// We start a QMPLoop, send the device_add command and stop the loop. +// +// The device_add command should be correctly sent and the QMP loop should +// exit gracefully. +func TestQMPPCIDeviceAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + q.version = checkVersion(t, connectedCh) + blockdevID := fmt.Sprintf("drive_%s", volumeUUID) + devID := fmt.Sprintf("device_%s", volumeUUID) + err := q.ExecutePCIDeviceAdd(context.Background(), blockdevID, devID, + "virtio-blk-pci", "0x1", "", "", 1, true, false) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that PCI VFIO mediated devices are correctly added using device_add. +// +// We start a QMPLoop, send the device_add command and stop the loop. +// +// The device_add command should be correctly sent and the QMP loop should +// exit gracefully. +func TestQMPPCIVFIOMediatedDeviceAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + sysfsDev := "/sys/bus/pci/devices/0000:00:02.0/a297db4a-f4c2-11e6-90f6-d3b88d6c9525" + devID := fmt.Sprintf("device_%s", volumeUUID) + err := q.ExecutePCIVFIOMediatedDeviceAdd(context.Background(), devID, sysfsDev, "0x1", "", "") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +func TestQMPPCIVFIOPCIeDeviceAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + bdf := "04:00.0" + bus := "rp0" + addr := "0x1" + romfile := "" + devID := fmt.Sprintf("device_%s", volumeUUID) + err := q.ExecutePCIVFIODeviceAdd(context.Background(), devID, bdf, addr, bus, romfile) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +func TestQMPAPVFIOMediatedDeviceAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + sysfsDev := "/sys/devices/vfio_ap/matrix/a297db4a-f4c2-11e6-90f6-d3b88d6c9525" + err := q.ExecuteAPVFIOMediatedDeviceAdd(context.Background(), sysfsDev) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that CPU are correctly added using device_add +func TestQMPCPUDeviceAdd(t *testing.T) { + drivers := []string{"host-x86_64-cpu", "host-s390x-cpu", "host-powerpc64-cpu"} + cpuID := "cpu-0" + socketID := "0" + dieID := "0" + coreID := "1" + threadID := "0" + for _, d := range drivers { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecuteCPUDeviceAdd(context.Background(), d, cpuID, socketID, dieID, coreID, threadID, "") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh + } +} + +// Checks that hotpluggable CPUs are listed correctly +func TestQMPExecuteQueryHotpluggableCPUs(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + hotCPU := HotpluggableCPU{ + Type: "host-x86", + VcpusCount: 5, + Properties: CPUProperties{ + Node: 1, + Socket: 3, + Die: 1, + Core: 2, + Thread: 4, + }, + QOMPath: "/abc/123/rgb", + } + buf.AddCommand("query-hotpluggable-cpus", nil, "return", []interface{}{hotCPU}) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + hotCPUs, err := q.ExecuteQueryHotpluggableCPUs(context.Background()) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(hotCPUs) != 1 { + t.Fatalf("Expected hot CPUs length equals to 1\n") + } + if reflect.DeepEqual(hotCPUs[0], hotCPU) == false { + t.Fatalf("Expected %v equals to %v", hotCPUs[0], hotCPU) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that memory devices are listed correctly +func TestQMPExecuteQueryMemoryDevices(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + memoryDevices := MemoryDevices{ + Type: "dimm", + Data: MemoryDevicesData{ + Slot: 1, + Node: 0, + Addr: 1234, + Memdev: "dimm1", + ID: "mem1", + Hotpluggable: true, + Hotplugged: false, + Size: 1234, + }, + } + buf.AddCommand("query-memory-devices", nil, "return", []interface{}{memoryDevices}) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + memDevices, err := q.ExecQueryMemoryDevices(context.Background()) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(memDevices) != 1 { + t.Fatalf("Expected memory devices length equals to 1\n") + } + if reflect.DeepEqual(memDevices[0], memoryDevices) == false { + t.Fatalf("Expected %v equals to %v", memDevices[0], memoryDevices) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that cpus are listed correctly +func TestQMPExecuteQueryCpus(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + cpuInfo := CPUInfo{ + CPU: 1, + Current: false, + Halted: false, + Arch: "x86_64", + QomPath: "/tmp/testQom", + Pc: 123456, + ThreadID: 123457, + Props: CPUProperties{ + Node: 0, + Socket: 1, + Core: 1, + Thread: 1966, + }, + } + buf.AddCommand("query-cpus", nil, "return", []interface{}{cpuInfo}) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + cpus, err := q.ExecQueryCpus(context.Background()) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(cpus) != 1 { + t.Fatalf("Expected memory devices length equals to 1\n") + } + if reflect.DeepEqual(cpus[0], cpuInfo) == false { + t.Fatalf("Expected %v equals to %v", cpus[0], cpuInfo) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that cpus are listed correctly +func TestQMPExecuteQueryCpusFast(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + cpuInfoFast := CPUInfoFast{ + CPUIndex: 1, + Arch: "x86", + Target: "x86_64", + QomPath: "/tmp/testQom", + ThreadID: 123457, + Props: CPUProperties{ + Node: 0, + Socket: 1, + Core: 1, + Thread: 1966, + }, + } + buf.AddCommand("query-cpus-fast", nil, "return", []interface{}{cpuInfoFast}) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + cpus, err := q.ExecQueryCpusFast(context.Background()) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(cpus) != 1 { + t.Fatalf("Expected memory devices length equals to 1\n") + } + if reflect.DeepEqual(cpus[0], cpuInfoFast) == false { + t.Fatalf("Expected %v equals to %v", cpus[0], cpuInfoFast) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that migrate capabilities can be set +func TestExecSetMigrationCaps(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("migrate-set-capabilities", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + caps := []map[string]interface{}{ + { + "capability": "bypass-shared-memory", + "state": true, + }, + } + err := q.ExecSetMigrationCaps(context.Background(), caps) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks that migrate arguments can be set +func TestExecSetMigrateArguments(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("migrate", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecSetMigrateArguments(context.Background(), "exec:foobar") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks add memory device +func TestExecMemdevAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("object-add", nil, "return", nil) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecMemdevAdd(context.Background(), "memory-backend-ram", "mem0", "", 128, true, "virtio-mem-pci", "virtiomem0", "0x1", "pci-bridge-0") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks hotplug memory +func TestExecHotplugMemory(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("object-add", nil, "return", nil) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecHotplugMemory(context.Background(), "memory-backend-ram", "mem0", "", 128, true) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks vsock-pci hotplug +func TestExecutePCIVSockAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecutePCIVSockAdd(context.Background(), "vsock-pci0", "3", "1", "1", "1", "", true) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks vhost-user-pci hotplug +func TestExecutePCIVhostUserDevAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + driver := "vhost-user-blk-pci" + devID := "vhost-user-blk0" + chardevID := "vhost-user-blk-char0" + err := q.ExecutePCIVhostUserDevAdd(context.Background(), driver, devID, chardevID, "1", "1") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks getfd +func TestExecuteGetFdD(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("getfd", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecuteGetFD(context.Background(), "foo", os.NewFile(0, "foo")) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks chardev-add unix socket +func TestExecuteCharDevUnixSocketAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("chardev-add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecuteCharDevUnixSocketAdd(context.Background(), "foo", "foo.sock", false, true) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks virtio serial port hotplug +func TestExecuteVirtSerialPortAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecuteVirtSerialPortAdd(context.Background(), "foo", "foo.channel", "foo") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Check migration incoming +func TestExecuteMigrationIncoming(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("migrate-incoming", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecuteMigrationIncoming(context.Background(), "uri") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks migration status +func TestExecuteQueryMigration(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + status := MigrationStatus{ + Status: "completed", + RAM: MigrationRAM{ + Total: 100, + Remaining: 101, + Transferred: 101, + TotalTime: 101, + SetupTime: 101, + ExpectedDowntime: 101, + Duplicate: 101, + Normal: 101, + NormalBytes: 101, + DirtySyncCount: 101, + }, + Disk: MigrationDisk{ + Total: 200, + Remaining: 200, + Transferred: 200, + }, + XbzrleCache: MigrationXbzrleCache{ + CacheSize: 300, + Bytes: 300, + Pages: 300, + CacheMiss: 300, + CacheMissRate: 300, + Overflow: 300, + }, + } + caps := map[string]interface{}{"foo": true} + status.Capabilities = append(status.Capabilities, caps) + buf.AddCommand("query-migrate", nil, "return", interface{}(status)) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + s, err := q.ExecuteQueryMigration(context.Background()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if !reflect.DeepEqual(s, status) { + t.Fatalf("expected %v got %v", status, s) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks balloon +func TestExecuteBalloon(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("balloon", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecuteBalloon(context.Background(), 1073741824) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +func TestErrorDesc(t *testing.T) { + errDesc := "Somthing err messages" + errData := map[string]string{ + "class": "GenericError", + "desc": errDesc, + } + + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + + desc, err := q.errorDesc(errData) + if err != nil { + t.Fatalf("Unexpected error '%v'", err) + } + if desc != errDesc { + t.Fatalf("expected '%v'\n got '%v'", errDesc, desc) + } + + q.Shutdown() + <-disconnectedCh +} + +func TestExecCommandFailed(t *testing.T) { + errDesc := "unable to map backing store for guest RAM: Cannot allocate memory" + errData := map[string]string{ + "class": "GenericError", + "desc": errDesc, + } + + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("object-add", nil, "error", errData) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + + _, err := q.executeCommandWithResponse(context.Background(), "object-add", nil, nil, nil) + if err == nil { + t.Fatalf("expected error but got nil") + } + + expectedString := "QMP command failed: " + errDesc + if err.Error() != expectedString { + t.Fatalf("expected '%v' but got '%v'", expectedString, err) + } + + q.Shutdown() + <-disconnectedCh +} + +func TestExecCommandFailedWithInnerError(t *testing.T) { + errData := map[string]string{ + "class": "GenericError", + "descFieldInvalid": "Invalid", + } + + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("object-add", nil, "error", errData) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + + _, err := q.executeCommandWithResponse(context.Background(), "object-add", nil, nil, nil) + if err == nil { + t.Fatalf("expected error but got nil") + } + + expectedString := "QMP command failed: " + if err.Error() != expectedString { + t.Fatalf("expected '%v' but got '%v'", expectedString, err) + } + + q.Shutdown() + <-disconnectedCh +} + +// Checks NVDIMM device add +func TestExecuteNVDIMMDeviceAdd(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("object-add", nil, "return", nil) + buf.AddCommand("device_add", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + pmem := true + err := q.ExecuteNVDIMMDeviceAdd(context.Background(), "nvdimm0", "/dev/rbd0", 1024, &pmem) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +func TestMainLoopEventBeforeGreeting(t *testing.T) { + const ( + seconds = int64(1352167040730) + microseconds = 123456 + ) + + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBufferNoGreeting(t) + + // Add events + var wg sync.WaitGroup + buf.AddEvent("VSERPORT_CHANGE", time.Millisecond*100, + map[string]interface{}{ + "open": false, + "id": "channel0", + }, + map[string]interface{}{ + "seconds": seconds, + "microseconds": microseconds, + }) + buf.AddEvent("POWERDOWN", time.Millisecond*200, nil, + map[string]interface{}{ + "seconds": seconds, + "microseconds": microseconds, + }) + + // register a channel to receive events + eventCh := make(chan QMPEvent) + cfg := QMPConfig{EventCh: eventCh, Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + + // Start events, this will lead to a deadlock if mainLoop is not implemented + // correctly + buf.startEventLoop(&wg) + wg.Wait() + + // Send greeting and check version + buf.newDataCh <- []byte(qmpHello) + checkVersion(t, connectedCh) + + q.Shutdown() + <-disconnectedCh +} + +func TestQMPExecQueryQmpSchema(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + schemaInfo := []SchemaInfo{ + { + MetaType: "command", + Name: "object-add", + }, + { + MetaType: "event", + Name: "VSOCK_RUNNING", + }, + } + buf.AddCommand("query-qmp-schema", nil, "return", schemaInfo) + cfg := QMPConfig{ + Logger: qmpTestLogger{}, + MaxCapacity: 1024, + } + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + info, err := q.ExecQueryQmpSchema(context.Background()) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(schemaInfo) != 2 { + t.Fatalf("Expected schema infos length equals to 2\n") + } + if reflect.DeepEqual(info, schemaInfo) == false { + t.Fatalf("Expected %v equals to %v", info, schemaInfo) + } + q.Shutdown() + <-disconnectedCh +} + +func TestQMPExecQueryQmpStatus(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + statusInfo := StatusInfo{ + Running: true, + SingleStep: false, + Status: "running", + } + buf.AddCommand("query-status", nil, "return", statusInfo) + cfg := QMPConfig{ + Logger: qmpTestLogger{}, + MaxCapacity: 1024, + } + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + info, err := q.ExecuteQueryStatus(context.Background()) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if reflect.DeepEqual(info, statusInfo) == false { + t.Fatalf("Expected %v equals to %v", info, statusInfo) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks qom-set +func TestExecQomSet(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("qom-set", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + err := q.ExecQomSet(context.Background(), "virtiomem0", "requested-size", 1024) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + q.Shutdown() + <-disconnectedCh +} + +// Checks qom-get +func TestExecQomGet(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("qom-get", nil, "return", "container") + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + val, err := q.ExecQomGet(context.Background(), "/", "type") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + vals, ok := val.(string) + if !ok { + t.Fatalf("Unexpected type in qom-get") + } + if vals != "container" { + t.Fatalf("Unpexected value in qom-get") + } + q.Shutdown() + <-disconnectedCh +} + +// Checks dump-guest-memory +func TestExecuteDumpGuestMemory(t *testing.T) { + connectedCh := make(chan *QMPVersion) + disconnectedCh := make(chan struct{}) + buf := newQMPTestCommandBuffer(t) + buf.AddCommand("dump-guest-memory", nil, "return", nil) + cfg := QMPConfig{Logger: qmpTestLogger{}} + q := startQMPLoop(buf, cfg, connectedCh, disconnectedCh) + checkVersion(t, connectedCh) + + err := q.ExecuteDumpGuestMemory(context.Background(), "file:/tmp/dump.xxx.yyy", false, "elf") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + q.Shutdown() + <-disconnectedCh +}