├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ └── goqemu.yml ├── AUTHORS ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── examples ├── README.md ├── domain_details │ └── main.go ├── domain_system_powerdown │ └── main.go └── hypervisor_domain_list │ └── main.go ├── go.mod ├── go.sum ├── hypervisor ├── README.md ├── example_test.go ├── hypervisor.go ├── hypervisor_test.go ├── rpc.go ├── rpc_test.go └── socket.go ├── internal └── qmp-gen │ ├── generate.go │ ├── main.go │ ├── parse_test.go │ ├── templates │ ├── alternate │ ├── command │ ├── enum │ ├── event │ ├── field │ ├── flatunion │ ├── main │ ├── simpleunion │ └── structtype │ └── types.go ├── qapi-schema ├── README.md ├── applicator.go ├── cmd │ ├── qapilex │ │ └── main.go │ └── qapiparse │ │ └── main.go ├── grammar.go ├── grammar_test.go ├── internal │ ├── lex │ │ ├── lex.go │ │ └── lex_test.go │ ├── parse │ │ └── parse.go │ └── token │ │ └── token.go └── tree.go ├── qemu ├── block.go ├── block_test.go ├── cpu.go ├── domain.go ├── domain_test.go ├── example_test.go ├── pci.go ├── string.gen.go ├── tempfile.go └── version_test.go ├── qmp ├── README.md ├── qmp.go ├── qmptest │ ├── qmptest.go │ └── qmptest_test.go ├── raw │ ├── autogen.go │ ├── doc.go │ ├── gen-api.go │ ├── raw.go │ └── raw_test.go ├── rpc.go ├── rpc_test.go ├── socket.go ├── socket_test.go ├── socket_unix.go └── socket_windows.go └── scripts ├── codegeneration.sh ├── golint.sh ├── license.txt ├── licensecheck.sh └── prependlicense.sh /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @digitalocean/go-qemu 2 | 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/goqemu.yml: -------------------------------------------------------------------------------- 1 | name: go-qemu 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | # track upstream Go's currently supported stable versions. 10 | gorelease: ['stable', 'oldstable'] 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - uses: actions/cache@v4 15 | with: 16 | path: | 17 | ~/.cache/go-build 18 | ~/go/pkg/mod 19 | key: go-${{ matrix.gorelease }}-${{ hashFiles('**/go.sum') }} 20 | restore-keys: | 21 | go-${{ matrix.gorelease }} 22 | 23 | - uses: actions/setup-go@v5 24 | with: 25 | go-version: ${{ matrix.gorelease }} 26 | 27 | - name: build/go-${{ matrix.gorelease }} 28 | run: go build -v ./... 29 | 30 | - name: test/go-${{ matrix.gorelease }} 31 | run: go test -v ./... 32 | 33 | # disabled for now, seeing as it is complaining about things that seem like 34 | # non-issues and that are helpful. 35 | # 36 | # I.e., it dislikes assert helpers: 37 | # 38 | # qmp/socket_test.go:238:4: call to (*T).Fatalf from a non-test goroutine 39 | # - name: vet 40 | # run: go vet ./... 41 | 42 | # disabled for now, is incompatible with anything that doesn't have 2016 as 43 | # the year. Might need fixing if we want to keep this. 44 | # 45 | # - name: check license 46 | # run: ./scripts/licensecheck.sh 47 | 48 | - name: check code generation 49 | run: | 50 | go install golang.org/x/tools/cmd/stringer@latest 51 | ./scripts/codegeneration.sh 52 | 53 | - name: golint 54 | run: | 55 | go install golang.org/x/lint/golint@latest 56 | ./scripts/golint.sh 57 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Maintainer 2 | ---------- 3 | DigitalOcean, Inc 4 | 5 | Original Authors 6 | ---------------- 7 | Ben LeMasurier 8 | Matt Layher 9 | 10 | Contributors 11 | ------------ 12 | David Anderson 13 | Justin Kim 14 | Luis Sagastume 15 | Nedim Dedic 16 | Roberto J Rojas 17 | Marko Mudrinic 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | The `go-qemu` project makes use of the [GitHub Flow](https://guides.github.com/introduction/flow/) 5 | for contributions. 6 | 7 | If you'd like to contribute to the project, please 8 | [open an issue](https://github.com/digitalocean/go-qemu/issues/new) or find an 9 | [existing issue](https://github.com/digitalocean/go-qemu/issues) that you'd like 10 | to take on. This ensures that efforts are not duplicated, and that a new feature 11 | aligns with the focus of the rest of the repository. 12 | 13 | Once your suggestion has been submitted and discussed, please be sure that your 14 | code meets the following criteria: 15 | - code is completely `gofmt`'d 16 | - new features or codepaths have appropriate test coverage 17 | - `go test ./...` passes 18 | - `go vet ./...` passes 19 | - `golint ./...` returns no warnings, including documentation comment warnings 20 | 21 | In addition, if this is your first time contributing to the `go-qemu` project, 22 | add your name and email address to the 23 | [AUTHORS](https://github.com/digitalocean/go-qemu/blob/master/AUTHORS) file 24 | under the "Contributors" section using the format: 25 | `First Last `. 26 | 27 | Finally, submit a pull request for review! 28 | 29 | Questions? Feel free to join us in [`#go-qemu` on libera chat](https://web.libera.chat/) 30 | if you'd like to discuss the project. 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright [yyyy] [name of copyright owner] 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. 195 | 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-qemu [![GoDoc](http://godoc.org/github.com/digitalocean/go-qemu?status.svg)](http://godoc.org/github.com/digitalocean/go-qemu) ![Build Status](https://github.com/digitalocean/go-qemu/actions/workflows/goqemu.yml/badge.svg?branch=master) [![Report Card](https://goreportcard.com/badge/github.com/digitalocean/go-qemu)](https://goreportcard.com/report/github.com/digitalocean/go-qemu) 2 | ======= 3 | 4 | `go-qemu` is a collection of Go packages for interacting with running QEMU 5 | instances. Apache 2.0 Licensed. 6 | 7 | Feel free to join us in [`#go-qemu` on libera chat](https://web.libera.chat/) 8 | if you'd like to discuss the project. 9 | 10 | Installation 11 | ------------ 12 | 13 | Use `go get` to retrieve all of the packages in `go-qemu`: 14 | 15 | ```shell 16 | $ go get github.com/digitalocean/go-qemu/... 17 | ``` 18 | 19 | Overview 20 | -------- 21 | 22 | Here is a quick overview of each top-level package, and what they can be used for: 23 | 24 | - `hypervisor`: Package hypervisor provides management facilities for one or 25 | more QEMU virtual machines on a hypervisor. 26 | - Provides easier access for managing groups of VMs than the `qemu` package. 27 | - Provides access to individual `qemu.Domain` types. 28 | - `qemu`: Package qemu provides an interface for interacting with running QEMU instances. 29 | - Typically used for managing a single VM. 30 | - Good for quick experiments. 31 | - `qmp`: Package qmp enables interaction with QEMU instances via the QEMU Machine 32 | Protocol (QMP). 33 | - Typically not used by consumers outside of this repository. 34 | - Wraps code-generated types with friendlier APIs. 35 | 36 | Details 37 | ------- 38 | 39 | Package `qemu` is used in production at DigitalOcean, alongside package 40 | [`libvirt`](https://github.com/digitalocean/go-libvirt). This being said, it is 41 | possible that there may still be subtle bugs which could cause the packages to act 42 | in unexpected ways. 43 | 44 | If you encounter any problems, please [look at the open issues](https://github.com/digitalocean/go-qemu/issues) 45 | and if your problem does not match any of the ones listed, file a new issue with as 46 | much detail as you are willing to provide. 47 | 48 | The API is not considered stable at this time. We do not anticipate making major 49 | changes to the API, but it is possible that the API may change over time, if deemed 50 | necessary by the project maintainers. If you would like to include package 51 | `qemu` in a project, we highly recommend vendoring it into your project. 52 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # go-qemu examples 2 | 3 | This folder contains example applications that demonstrate the use of the 4 | go-qemu and go-qemu/hypervisor packages. 5 | 6 | If the program is executed using the `unix` named network (i.e locally 7 | from the hypervisor host), the user under which the program executes on 8 | needs belong to `libvirtd` group 9 | so the account has access to `/var/run/libvirt/libvirt-sock`. 10 | 11 | In case of executing the program remotely and connecting through `tcp`, 12 | you could configure libvirtd on the hypervisor host to allow tcp connections. 13 | Of course, this is not secure. 14 | 15 | To see the list of posible arguments for each program, use `--help`. 16 | 17 | #### hypervisor_domain_list 18 | 19 | [hypervisor_domain_list](./hypervisor_domain_list) demonstrates how to use 20 | the [hypervisor](https://godoc.org/github.com/digitalocean/go-qemu/hypervisor) 21 | package to obtain a list of the domains from the connected hypervisor. 22 | The list of domains returned is of type: 23 | [go-qemu/Domain](https://godoc.org/github.com/digitalocean/go-qemu#Domain). 24 | 25 | To run: 26 | ```{r, engine='bash', count_lines} 27 | $ go get github.com/digitalocean/go-qemu/... 28 | $ go run examples/hypervisor_domain_list/main.go -network=tcp \ 29 | -address="hypervisorhost:16509" 30 | ``` 31 | 32 | 33 | You should have an output similar to this: 34 | ```{r, engine='bash', count_lines} 35 | Connecting to unix:///var/run/libvirt/libvirt-sock 36 | 37 | **********Domains********** 38 | centos7 39 | ubuntu14.04 40 | debian8 41 | 42 | *************************** 43 | ``` 44 | 45 | 46 | #### domain_details 47 | 48 | [domain_details](./domain_details) domanstrates how to use the 49 | [go-qemu](https://godoc.org/github.com/digitalocean/go-qemu) 50 | to connect to a hypervisor host using 51 | [qmp.NewLibvirtRPCMonitor](https://godoc.org/github.com/digitalocean/go-qemu/qmp#LibvirtRPCMonitor) 52 | and get the details for a specified domain. 53 | 54 | To run: 55 | ```{r, engine='bash', count_lines} 56 | $ go get github.com/digitalocean/go-qemu/... 57 | $ go run examples/domain_details/main.go 58 | or 59 | $ go run examples/domain_details/main.go -network=tcp \ 60 | -address="hypervisorhost:16509" -domainName="ubuntu14.04" 61 | ``` 62 | 63 | 64 | You should have an output similar to this: 65 | ```{r, engine='bash', count_lines} 66 | 67 | Connecting to Connecting to unix:///var/run/libvirt/libvirt-sock 68 | 69 | Version: 1.5.3 70 | 71 | Status: running 72 | 73 | [ PCIDevices ] 74 | ====================================== 75 | [ID] [Description] 76 | ====================================== 77 | [ ] [ Host bridge] 78 | [ ] [ ISA bridge] 79 | [ ] [ IDE controller] 80 | [ ] [ Bridge] 81 | [ ] [ VGA controller] 82 | [ net0] [ Ethernet controller] 83 | [ sound0] [ Audio controller] 84 | [virtio-serial0] [ ] 85 | [ ] [ USB controller] 86 | [ ] [ USB controller] 87 | [ ] [ USB controller] 88 | [ usb] [ USB controller] 89 | [virtio-disk0] [ SCSI controller] 90 | [ balloon0] [ ] 91 | 92 | [ BlockDevices ] 93 | ======================================================================== 94 | Device Driver File 95 | ======================================================================== 96 | drive-virtio-disk0 qcow2 /var/lib/libvirt/images/ubuntu14.04.qcow2 97 | drive-ide0-0-0 98 | 99 | 100 | ``` 101 | 102 | #### domain_system_powerdown 103 | 104 | [domain_system_powerdown](./domain_system_powerdown) demonstrates how to use 105 | the [hypervisor](https://godoc.org/github.com/digitalocean/go-qemu/hypervisor) 106 | package to shut off the specified domain. 107 | 108 | To run: 109 | ```{r, engine='bash', count_lines} 110 | $ go get github.com/digitalocean/go-qemu/... 111 | $ go run examples/domain_system_powerdown/main.go -domainName="ubuntu14.04" 112 | or 113 | $ go run examples/domain_system_powerdown/main.go -network=tcp \ 114 | -address="hypervisorhost:16509" -domainName="ubuntu14.04" 115 | ``` 116 | 117 | 118 | You should have an output similar to this: 119 | ```{r, engine='bash', count_lines} 120 | Connecting to unix:///var/run/libvirt/libvirt-sock 121 | Domain should be shut off now 122 | ``` 123 | -------------------------------------------------------------------------------- /examples/domain_details/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "log" 21 | "net" 22 | "time" 23 | 24 | "github.com/digitalocean/go-qemu/qemu" 25 | "github.com/digitalocean/go-qemu/qmp" 26 | ) 27 | 28 | var ( 29 | network = flag.String("network", "unix", `Named network used to connect on. For Unix sockets -network=unix, for TCP connections: -network=tcp`) 30 | address = flag.String("address", "/var/run/libvirt/libvirt-sock", `Address of the hypervisor. This could be in the form of Unix or TCP sockets. For TCP connections: -address="host:16509"`) 31 | timeout = flag.Duration("timeout", 2*time.Second, "Connection timeout. Another valid value could be -timeout=500ms") 32 | domainName = flag.String("domainName", "mydomain", "This is the domain to get details for.") 33 | ) 34 | 35 | func main() { 36 | flag.Parse() 37 | 38 | fmt.Printf("\nConnecting to %s://%s\n", *network, *address) 39 | 40 | c, err := net.DialTimeout(*network, *address, *timeout) 41 | if err != nil { 42 | log.Fatalf("failed to connect to hypervisor: %v", err) 43 | } 44 | 45 | mon := qmp.NewLibvirtRPCMonitor(*domainName, c) 46 | 47 | if err := mon.Connect(); err != nil { 48 | log.Fatalf("failed to connect: %v", err) 49 | } 50 | 51 | domain, err := qemu.NewDomain(mon, *domainName) 52 | if err != nil { 53 | log.Fatalf("failed to create domain object: %v", err) 54 | } 55 | defer domain.Close() 56 | 57 | version, err := domain.Version() 58 | if err != nil { 59 | log.Fatalf("Error getting Domain Version: %v\n", err) 60 | } 61 | fmt.Printf("\nVersion: %s\n", version) 62 | 63 | status, err := domain.Status() 64 | if err != nil { 65 | log.Fatalf("Error getting Domain Status: %v\n", err) 66 | } 67 | fmt.Printf("\nStatus: %s\n", status) 68 | 69 | displayPCIDevices(domain) 70 | 71 | displayBlockDevices(domain) 72 | } 73 | 74 | func displayPCIDevices(domain *qemu.Domain) { 75 | pciDevices, err := domain.PCIDevices() 76 | if err != nil { 77 | log.Fatalf("Error getting PCIDevices: %v\n", pciDevices) 78 | } 79 | fmt.Printf("\n[ PCIDevices ]\n") 80 | fmt.Printf("======================================\n") 81 | fmt.Printf("%10s %20s\n", "[ID]", "[Description]") 82 | fmt.Printf("======================================\n") 83 | for _, pciDevice := range pciDevices { 84 | fmt.Printf("[%10s] [%20s]\n", pciDevice.QdevID, pciDevice.ClassInfo.Desc) 85 | } 86 | } 87 | 88 | func displayBlockDevices(domain *qemu.Domain) { 89 | blockDevices, err := domain.BlockDevices() 90 | if err != nil { 91 | log.Fatalf("Error getting blockDevices: %v\n", blockDevices) 92 | } 93 | fmt.Printf("\n[ BlockDevices ]\n") 94 | fmt.Printf("========================================================================\n") 95 | fmt.Printf("%20s %8s %30s\n", "Device", "Driver", "File") 96 | fmt.Printf("========================================================================\n") 97 | for _, blockDevice := range blockDevices { 98 | fmt.Printf("%20s %8s %30s\n", 99 | blockDevice.Device, blockDevice.Inserted.Driver, blockDevice.Inserted.File) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /examples/domain_system_powerdown/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "log" 21 | "net" 22 | "time" 23 | 24 | hypervisor "github.com/digitalocean/go-qemu/hypervisor" 25 | ) 26 | 27 | var ( 28 | network = flag.String("network", "unix", `Named network used to connect on. For Unix sockets -network=unix, for TCP connections: -network=tcp`) 29 | address = flag.String("address", "/var/run/libvirt/libvirt-sock", `Address of the hypervisor. This could be in the form of Unix or TCP sockets. For TCP connections: -address="host:16509"`) 30 | timeout = flag.Duration("timeout", 2*time.Second, "Connection timeout. Another valid value could be -timeout=500ms") 31 | domainName = flag.String("domainName", "mydomain", "This is the domain to get details for.") 32 | ) 33 | 34 | func main() { 35 | flag.Parse() 36 | 37 | fmt.Printf("\nConnecting to %s://%s\n", *network, *address) 38 | newConn := func() (net.Conn, error) { 39 | return net.DialTimeout(*network, *address, *timeout) 40 | } 41 | 42 | driver := hypervisor.NewRPCDriver(newConn) 43 | hv := hypervisor.New(driver) 44 | 45 | domain, err := hv.Domain(*domainName) 46 | if err != nil { 47 | log.Fatalf("Unable to get domain: %v\n", err) 48 | } 49 | 50 | err = domain.SystemPowerdown() 51 | if err != nil { 52 | log.Fatalf("Unable to power down domain: %v\n", err) 53 | } 54 | 55 | fmt.Println("Domain should be shut off now") 56 | } 57 | -------------------------------------------------------------------------------- /examples/hypervisor_domain_list/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "log" 21 | "net" 22 | "time" 23 | 24 | hypervisor "github.com/digitalocean/go-qemu/hypervisor" 25 | ) 26 | 27 | var ( 28 | network = flag.String("network", "unix", `Named network used to connect on. For Unix sockets -network=unix, for TCP connections: -network=tcp`) 29 | address = flag.String("address", "/var/run/libvirt/libvirt-sock", `Address of the hypervisor. This could be in the form of Unix or TCP sockets. For TCP connections: -address="host:16509"`) 30 | timeout = flag.Duration("timeout", 2*time.Second, "Connection timeout. Another valid value could be -timeout=500ms") 31 | ) 32 | 33 | func main() { 34 | flag.Parse() 35 | 36 | fmt.Printf("\nConnecting to %s://%s\n", *network, *address) 37 | newConn := func() (net.Conn, error) { 38 | return net.DialTimeout(*network, *address, *timeout) 39 | } 40 | 41 | driver := hypervisor.NewRPCDriver(newConn) 42 | hv := hypervisor.New(driver) 43 | 44 | fmt.Printf("\n**********Domains**********\n") 45 | domains, err := hv.Domains() 46 | if err != nil { 47 | log.Fatalf("Unable to get domains from hypervisor: %v", err) 48 | } 49 | for _, dom := range domains { 50 | fmt.Printf("%s\n", dom.Name) 51 | } 52 | fmt.Printf("\n***************************\n") 53 | } 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/digitalocean/go-qemu 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/alecthomas/repr v0.4.0 7 | github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e 8 | github.com/fatih/camelcase v1.0.0 9 | golang.org/x/sys v0.30.0 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 2 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e h1:SCnqm8SjSa0QqRxXbo5YY//S+OryeJioe17nK+iDZpg= 6 | github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e/go.mod h1:o129ljs6alsIQTc8d6eweihqpmmrbxZ2g1jhgjhPykI= 7 | github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= 8 | github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 13 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 15 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 16 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 17 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 18 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 19 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 20 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 21 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 22 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 23 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 24 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 26 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 29 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 30 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 31 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 32 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 33 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 34 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 35 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 36 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 37 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 38 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 40 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 41 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 42 | -------------------------------------------------------------------------------- /hypervisor/README.md: -------------------------------------------------------------------------------- 1 | hypervisor 2 | ========== 3 | 4 | Package `hypervisor` provides management facilities for one or more QEMU 5 | virtual machines on a hypervisor. 6 | -------------------------------------------------------------------------------- /hypervisor/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hypervisor_test 16 | 17 | import ( 18 | "fmt" 19 | "log" 20 | 21 | "github.com/digitalocean/go-qemu/hypervisor" 22 | "github.com/digitalocean/go-qemu/qmp" 23 | "github.com/digitalocean/go-qemu/qmp/qmptest" 24 | ) 25 | 26 | func ExampleNew() { 27 | // Create a hypervisor.Hypervisor using an in-memory hypervisor.Driver, 28 | // that returns a fixed list of three domains. 29 | hv := hypervisor.New(newDriver([]string{ 30 | "foo", 31 | "bar", 32 | "baz", 33 | })) 34 | 35 | // Connect to the monitor socket of each domain using the in-memory driver. 36 | domains, err := hv.Domains() 37 | if err != nil { 38 | log.Fatalf("failed to connect to domains: %v", err) 39 | } 40 | 41 | fmt.Println("hypervisor:") 42 | for _, d := range domains { 43 | // Return the domain's QEMU version. 44 | version, err := d.Version() 45 | if err != nil { 46 | log.Fatalf("failed to check domain QEMU version: %v", err) 47 | } 48 | 49 | // Print some information about the domain. 50 | fmt.Printf(" - %s - QEMU %s\n", d.Name, version) 51 | 52 | // Close the domain to clean up its resources and underlying monitor. 53 | if err := d.Close(); err != nil { 54 | log.Fatalf("failed to close domain: %v", err) 55 | } 56 | } 57 | 58 | // Output: 59 | // hypervisor: 60 | // - foo - QEMU 2.0.0 61 | // - bar - QEMU 2.0.1 62 | // - baz - QEMU 2.0.2 63 | } 64 | 65 | var _ hypervisor.Driver = &driver{} 66 | 67 | func newDriver(domains []string) *driver { 68 | return &driver{ 69 | domains: domains, 70 | } 71 | } 72 | 73 | type driver struct { 74 | domains []string 75 | } 76 | 77 | func (d *driver) NewMonitor(domain string) (qmp.Monitor, error) { 78 | m := qmptest.NewMonitor(func(cmd qmp.Command) (interface{}, error) { 79 | return run(domain, cmd) 80 | }) 81 | 82 | return m, nil 83 | } 84 | 85 | func (d *driver) DomainNames() ([]string, error) { 86 | return d.domains, nil 87 | } 88 | 89 | func run(domain string, cmd qmp.Command) (interface{}, error) { 90 | switch cmd.Execute { 91 | case "query-version": 92 | return runQueryVersion(domain), nil 93 | } 94 | 95 | return nil, fmt.Errorf("unknown command: %q", cmd.Execute) 96 | } 97 | 98 | func runQueryVersion(domain string) interface{} { 99 | var response struct { 100 | ID string `json:"id"` 101 | Return qmp.Version `json:"return"` 102 | } 103 | response.Return.QEMU.Major = 2 104 | 105 | switch domain { 106 | case "bar": 107 | response.Return.QEMU.Micro = 1 108 | case "baz": 109 | response.Return.QEMU.Micro = 2 110 | } 111 | 112 | return response 113 | } 114 | -------------------------------------------------------------------------------- /hypervisor/hypervisor.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package hypervisor provides management facilities for one or more QEMU 16 | // virtual machines on a hypervisor. 17 | package hypervisor 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/digitalocean/go-qemu/qemu" 23 | "github.com/digitalocean/go-qemu/qmp" 24 | ) 25 | 26 | // A Hypervisor enables access to all virtual machines on a QEMU hypervisor. 27 | type Hypervisor struct { 28 | // Driver used to communicate with domains 29 | driver Driver 30 | } 31 | 32 | // New creates a new Hypervisor using the input Driver. 33 | func New(driver Driver) *Hypervisor { 34 | return &Hypervisor{ 35 | driver: driver, 36 | } 37 | } 38 | 39 | // A Driver is a QEMU QMP monitor driver that a Hypervisor can use to perform 40 | // actions on groups of virtual machines. 41 | type Driver interface { 42 | NewMonitor(domain string) (qmp.Monitor, error) 43 | DomainNames() ([]string, error) 44 | } 45 | 46 | // A Versioner is a Driver that is able to report its version on the 47 | // Hypervisor. 48 | type Versioner interface { 49 | Version() (string, error) 50 | } 51 | 52 | // Domains retrieves all virtual machines which reside on a given hypervisor, 53 | // connecting to the monitor socket of each machine. 54 | // 55 | // Each Domain's Close method must be called to clean up its resources when 56 | // they are no longer needed. 57 | func (hv *Hypervisor) Domains() ([]*qemu.Domain, error) { 58 | domains, err := hv.driver.DomainNames() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | ds := make([]*qemu.Domain, 0, len(domains)) 64 | for _, d := range domains { 65 | domain, err := hv.connectDomain(d) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | ds = append(ds, domain) 71 | } 72 | 73 | return ds, nil 74 | } 75 | 76 | // Domain retrieves a single virtual machine from a hypervisor, connecting to 77 | // its monitor socket. If no virtual machine exists with the given name, an 78 | // error is returned. 79 | // 80 | // The Domain's Close method must be called to clean up its resources when 81 | // they are no longer needed. 82 | func (hv *Hypervisor) Domain(name string) (*qemu.Domain, error) { 83 | domains, err := hv.driver.DomainNames() 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | for _, d := range domains { 89 | if d == name { 90 | return hv.connectDomain(name) 91 | } 92 | } 93 | 94 | return nil, fmt.Errorf("domain %q not found", name) 95 | } 96 | 97 | // DomainNames retrieves the names of all virtual machines which reside on 98 | // a given hypervisor, so that individual connections can be opened using 99 | // the Domain method, as needed. 100 | func (hv *Hypervisor) DomainNames() ([]string, error) { 101 | return hv.driver.DomainNames() 102 | } 103 | 104 | // connectDomain opens a monitor socket connection and creates a virtual 105 | // machine for the machine with the specified name. 106 | func (hv *Hypervisor) connectDomain(name string) (*qemu.Domain, error) { 107 | mon, err := hv.driver.NewMonitor(name) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | if err := mon.Connect(); err != nil { 113 | return nil, err 114 | } 115 | 116 | return qemu.NewDomain(mon, name) 117 | } 118 | -------------------------------------------------------------------------------- /hypervisor/hypervisor_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hypervisor 16 | 17 | import ( 18 | "context" 19 | "reflect" 20 | "testing" 21 | 22 | "github.com/digitalocean/go-qemu/qmp" 23 | ) 24 | 25 | func TestHypervisorDomains(t *testing.T) { 26 | wantDomains := []string{"ubuntu", "arch", "gentoo"} 27 | 28 | var seenDomains []string 29 | hv := testHypervisor( 30 | t, 31 | wantDomains, 32 | func(domain string) (qmp.Monitor, error) { 33 | seenDomains = append(seenDomains, domain) 34 | return &noopMonitor{}, nil 35 | }, 36 | ) 37 | 38 | domains, err := hv.Domains() 39 | if err != nil { 40 | t.Fatalf("failed to retrieve domains: %v", err) 41 | } 42 | 43 | gotDomains := make([]string, 0, len(domains)) 44 | for _, d := range domains { 45 | gotDomains = append(gotDomains, d.Name) 46 | } 47 | 48 | if want, got := wantDomains, gotDomains; !reflect.DeepEqual(want, got) { 49 | t.Fatalf("unexpected domains:\n- want: %v\n- got: %v", 50 | want, got) 51 | } 52 | 53 | if want, got := wantDomains, seenDomains; !reflect.DeepEqual(want, got) { 54 | t.Fatalf("did not open monitors for all domains:\n- want: %v\n- got: %v", 55 | want, got) 56 | } 57 | } 58 | 59 | func TestHypervisorDomainOK(t *testing.T) { 60 | const wantDomain = "ubuntu" 61 | 62 | hv := testHypervisor( 63 | t, 64 | []string{wantDomain}, 65 | noopMonitorFunc(), 66 | ) 67 | 68 | gotDomain, err := hv.Domain(wantDomain) 69 | if err != nil { 70 | t.Fatalf("failed to retrieve domain: %v", err) 71 | } 72 | 73 | if want, got := wantDomain, gotDomain.Name; want != got { 74 | t.Fatalf("unexpected domain:\n- want: %v\n- got: %v", 75 | want, got) 76 | } 77 | } 78 | 79 | func TestHypervisorDomainNotFound(t *testing.T) { 80 | hv := testHypervisor( 81 | t, 82 | nil, 83 | noopMonitorFunc(), 84 | ) 85 | 86 | _, err := hv.Domain("foo") 87 | if want, got := `domain "foo" not found`, err.Error(); want != got { 88 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", 89 | want, got) 90 | } 91 | } 92 | 93 | func TestHypervisorDomainNames(t *testing.T) { 94 | wantDomains := []string{"ubuntu", "arch", "gentoo"} 95 | 96 | hv := testHypervisor( 97 | t, 98 | wantDomains, 99 | noopMonitorFunc(), 100 | ) 101 | 102 | gotDomains, err := hv.DomainNames() 103 | if err != nil { 104 | t.Fatalf("failed to retrieve domain names: %v", err) 105 | } 106 | 107 | if want, got := wantDomains, gotDomains; !reflect.DeepEqual(want, got) { 108 | t.Fatalf("unexpected domain names:\n- want: %v\n- got: %v", 109 | want, got) 110 | } 111 | } 112 | 113 | // A monitorFunc is a function which can create a qmp.Monitor. 114 | type monitorFunc func(domain string) (qmp.Monitor, error) 115 | 116 | func noopMonitorFunc() monitorFunc { 117 | return func(_ string) (qmp.Monitor, error) { 118 | return &noopMonitor{}, nil 119 | } 120 | } 121 | 122 | var _ qmp.Monitor = &noopMonitor{} 123 | 124 | type noopMonitor struct{} 125 | 126 | func (noopMonitor) Connect() error { return nil } 127 | func (noopMonitor) Disconnect() error { return nil } 128 | func (noopMonitor) Run(_ []byte) ([]byte, error) { return nil, nil } 129 | func (noopMonitor) Events(context.Context) (<-chan qmp.Event, error) { return nil, nil } 130 | 131 | type testConnectMonitor struct { 132 | connected bool 133 | noopMonitor 134 | } 135 | 136 | func (m *testConnectMonitor) Connect() error { 137 | m.connected = true 138 | return nil 139 | } 140 | 141 | func (m *testConnectMonitor) Disconnect() error { 142 | m.connected = false 143 | return nil 144 | } 145 | 146 | func testHypervisor( 147 | t *testing.T, 148 | domains []string, 149 | newMonitor monitorFunc, 150 | ) *Hypervisor { 151 | return New(&testDriver{ 152 | newMonitor: newMonitor, 153 | domains: domains, 154 | }) 155 | } 156 | 157 | type testDriver struct { 158 | newMonitor monitorFunc 159 | domains []string 160 | } 161 | 162 | func (d *testDriver) NewMonitor(domain string) (qmp.Monitor, error) { 163 | return d.newMonitor(domain) 164 | } 165 | 166 | func (d *testDriver) DomainNames() ([]string, error) { 167 | return d.domains, nil 168 | } 169 | -------------------------------------------------------------------------------- /hypervisor/rpc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hypervisor 16 | 17 | import ( 18 | "net" 19 | 20 | "github.com/digitalocean/go-libvirt" 21 | "github.com/digitalocean/go-qemu/qmp" 22 | ) 23 | 24 | var _ Driver = &RPCDriver{} 25 | var _ Versioner = &RPCDriver{} 26 | 27 | // RPCDriver is a QEMU QMP monitor driver which 28 | // communicates via libvirt's RPC interface. 29 | type RPCDriver struct { 30 | newConn func() (net.Conn, error) 31 | } 32 | 33 | // DomainNames retrieves all hypervisor domain names using libvirt RPC. 34 | func (d *RPCDriver) DomainNames() ([]string, error) { 35 | var names []string 36 | err := d.withLibvirt(func(l *libvirt.Libvirt) error { 37 | domains, err := l.Domains() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | names = make([]string, 0, len(domains)) 43 | for _, d := range domains { 44 | names = append(names, d.Name) 45 | } 46 | 47 | return nil 48 | }) 49 | 50 | return names, err 51 | } 52 | 53 | // NewMonitor creates a new qmp.Monitor using libvirt RPC. 54 | func (d *RPCDriver) NewMonitor(domain string) (qmp.Monitor, error) { 55 | conn, err := d.newConn() 56 | return qmp.NewLibvirtRPCMonitor(domain, conn), err 57 | } 58 | 59 | // Version returns the version string for the libvirt daemon. 60 | func (d *RPCDriver) Version() (string, error) { 61 | var version string 62 | err := d.withLibvirt(func(l *libvirt.Libvirt) error { 63 | v, err := l.Version() 64 | version = v 65 | return err 66 | }) 67 | 68 | return version, err 69 | } 70 | 71 | // NewRPCDriver configures a hypervisor driver using Libvirt RPC. 72 | // The provided newConn function should return an established 73 | // network connection with the target libvirt daemon. 74 | func NewRPCDriver(newConn func() (net.Conn, error)) *RPCDriver { 75 | return &RPCDriver{ 76 | newConn: newConn, 77 | } 78 | } 79 | 80 | // withLibvirt opens a new RPC connection to Libvirt and invokes the input 81 | // closure using the RPC connection. Connections are automatically cleaned 82 | // up when the close returns. 83 | func (d *RPCDriver) withLibvirt(fn func(l *libvirt.Libvirt) error) error { 84 | conn, err := d.newConn() 85 | if err != nil { 86 | return err 87 | } 88 | 89 | l := libvirt.New(conn) 90 | if err := l.Connect(); err != nil { 91 | _ = conn.Close() 92 | return err 93 | } 94 | defer func() { 95 | _ = l.Disconnect() 96 | }() 97 | 98 | return fn(l) 99 | } 100 | -------------------------------------------------------------------------------- /hypervisor/rpc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hypervisor 16 | 17 | import ( 18 | "fmt" 19 | "net" 20 | "testing" 21 | 22 | "github.com/digitalocean/go-libvirt/libvirttest" 23 | ) 24 | 25 | var hvConn = func() (net.Conn, error) { 26 | m := libvirttest.New() 27 | conn, err := m.Dial() 28 | return conn, err 29 | } 30 | 31 | func TestRPCDriverDomainNames(t *testing.T) { 32 | h := NewRPCDriver(hvConn) 33 | 34 | domains, err := h.DomainNames() 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | 39 | wantLen := 2 40 | gotLen := len(domains) 41 | if gotLen != wantLen { 42 | t.Errorf("expected %d domains to be returned, got %d", wantLen, gotLen) 43 | } 44 | 45 | for i, name := range domains { 46 | wantName := fmt.Sprintf("aaaaaaa-%d", i+1) 47 | if name != wantName { 48 | t.Errorf("expected domain name %q, got %q", wantName, name) 49 | } 50 | } 51 | } 52 | 53 | func TestRPCDriverVersion(t *testing.T) { 54 | h := NewRPCDriver(hvConn) 55 | 56 | version, err := h.Version() 57 | if err != nil { 58 | t.Error(err) 59 | } 60 | 61 | expected := "1.3.4" 62 | if version != expected { 63 | t.Errorf("expected version %q, got %q", expected, version) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /hypervisor/socket.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hypervisor 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/digitalocean/go-qemu/qmp" 22 | ) 23 | 24 | var _ Driver = &SocketDriver{} 25 | 26 | // A SocketDriver is a QEMU QMP monitor driver which communicates directly 27 | // with a QEMU monitor socket. 28 | type SocketDriver struct { 29 | addrs []SocketAddress 30 | } 31 | 32 | // A SocketAddress is a QEMU QMP monitor socket address used to configure 33 | // a SocketDriver. 34 | type SocketAddress struct { 35 | // Network should be one of "unix", "tcp", etc. 36 | Network string 37 | 38 | // Address should be a host:port address or UNIX socket filepath, 39 | // such "8.8.8.8:4444" or "/var/lib/qemu/example.socket". 40 | Address string 41 | 42 | // Timeout specifies how long the monitor socket will attempt to be 43 | // reached before timing out. 44 | Timeout time.Duration 45 | } 46 | 47 | // NewMonitor creates a new qmp.Monitor using a QEMU monitor socket at 48 | // the specified address. 49 | func (d *SocketDriver) NewMonitor(addr string) (qmp.Monitor, error) { 50 | for _, a := range d.addrs { 51 | if a.Address == addr { 52 | return qmp.NewSocketMonitor( 53 | a.Network, 54 | a.Address, 55 | a.Timeout, 56 | ) 57 | } 58 | } 59 | 60 | return nil, fmt.Errorf("address not known to SocketDriver: %q", addr) 61 | } 62 | 63 | // DomainNames retrieves all hypervisor domain names known to the SocketDriver. 64 | func (d *SocketDriver) DomainNames() ([]string, error) { 65 | names := make([]string, 0, len(d.addrs)) 66 | for _, a := range d.addrs { 67 | names = append(names, a.Address) 68 | } 69 | 70 | return names, nil 71 | } 72 | 73 | // NewSocketDriver configures a SocketDriver using one or more SocketAddress 74 | // structures for configuration. 75 | func NewSocketDriver(addrs []SocketAddress) *SocketDriver { 76 | return &SocketDriver{ 77 | addrs: addrs, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internal/qmp-gen/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gen 16 | 17 | import ( 18 | "bytes" 19 | "errors" 20 | "path/filepath" 21 | "reflect" 22 | "strings" 23 | "text/template" 24 | 25 | "github.com/fatih/camelcase" 26 | ) 27 | 28 | // neededTypes returns the subset of API that should be rendered to file. 29 | // 30 | // The subset to render is defined as the transitive closure of all types 31 | // reachable from commands and events. 32 | func neededTypes(api map[name]interface{}) map[name]interface{} { 33 | need := map[name]interface{}{} 34 | 35 | for n, i := range api { 36 | switch i.(type) { 37 | case command: 38 | need[n] = i 39 | case event: 40 | need[n] = i 41 | } 42 | } 43 | 44 | for { 45 | nextNeed := expandTypes(api, need) 46 | if len(need) == len(nextNeed) { 47 | break 48 | } 49 | need = nextNeed 50 | } 51 | 52 | return need 53 | } 54 | 55 | // expandTypes returns the union of need and all types it directly references. 56 | func expandTypes(api, need map[name]interface{}) map[name]interface{} { 57 | ret := map[name]interface{}{} 58 | 59 | for n, i := range need { 60 | ret[n] = i 61 | switch v := i.(type) { 62 | case command: 63 | addName(api, ret, v.Name) 64 | if v.BoxedInput { 65 | addName(api, ret, v.Inputs.Ref) 66 | } else { 67 | for _, f := range v.Inputs.Fields(api) { 68 | addName(api, ret, f.Type) 69 | } 70 | } 71 | if v.Output.Type != "" { 72 | addName(api, ret, v.Output.Type) 73 | } 74 | case event: 75 | addName(api, ret, v.Name) 76 | if v.BoxedData { 77 | addName(api, ret, v.Data.Ref) 78 | } else { 79 | for _, f := range v.Data.Fields(api) { 80 | addName(api, ret, f.Type) 81 | } 82 | } 83 | case structType: 84 | addName(api, ret, v.Name) 85 | for _, f := range v.Fields { 86 | addName(api, ret, f.Type) 87 | } 88 | if v.Base != "" { 89 | for _, f := range api[v.Base].(structType).Fields { 90 | addName(api, ret, f.Type) 91 | } 92 | } 93 | case simpleUnion: 94 | addName(api, ret, v.Name) 95 | for _, typ := range v.Options { 96 | addName(api, ret, typ) 97 | } 98 | case flatUnion: 99 | addName(api, ret, v.Name) 100 | for _, f := range v.Base.Fields(api) { 101 | addName(api, ret, f.Type) 102 | } 103 | for _, typ := range v.Options { 104 | for _, f := range api[typ].(structType).Fields { 105 | addName(api, ret, f.Type) 106 | } 107 | } 108 | case alternate: 109 | addName(api, ret, v.Name) 110 | for _, typ := range v.Options { 111 | addName(api, ret, typ) 112 | } 113 | case enum: 114 | addName(api, ret, v.Name) 115 | } 116 | } 117 | return ret 118 | } 119 | 120 | func addName(api, need map[name]interface{}, name name) { 121 | if name.SimpleType() { 122 | return 123 | } 124 | v, ok := api[name] 125 | if ok { 126 | need[name] = v 127 | } 128 | } 129 | 130 | // renderAPI generates the Go implementation for the types listed in need. 131 | func renderAPI(templateDir string, api, need map[name]interface{}) ([]byte, error) { 132 | tmpl := template.New("") 133 | tmpl.Funcs(template.FuncMap{ 134 | "API": func() map[name]interface{} { return api }, 135 | "typeOf": func(v interface{}) string { 136 | if v == nil { 137 | return "nil" 138 | } 139 | return strings.ToLower(reflect.TypeOf(v).Name()) 140 | }, 141 | "unexport": func(s string) string { 142 | ws := camelcase.Split(s) 143 | if upperWords[strings.ToLower(ws[0])] { 144 | ws[0] = strings.ToLower(ws[0]) 145 | } else { 146 | ws[0] = strings.ToLower(ws[0][:1]) + ws[0][1:] 147 | } 148 | ret := strings.Join(ws, "") 149 | if ret == "type" { 150 | return "typ" 151 | } 152 | return ret 153 | }, 154 | "render": func(v interface{}) (string, error) { 155 | n := strings.ToLower(reflect.TypeOf(v).Name()) 156 | var b bytes.Buffer 157 | err := tmpl.ExecuteTemplate(&b, n, v) 158 | return b.String(), err 159 | }, 160 | "abort": func(s string) (string, error) { return "", errors.New(s) }, 161 | "last": func(fs fields, i int) bool { return i == len(fs)-1 }, 162 | }) 163 | tmpl, err := tmpl.ParseGlob(filepath.Join(templateDir, "*")) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | var out bytes.Buffer 169 | if err := tmpl.ExecuteTemplate(&out, "main", need); err != nil { 170 | return nil, err 171 | } 172 | return out.Bytes(), nil 173 | } 174 | -------------------------------------------------------------------------------- /internal/qmp-gen/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gen 16 | 17 | import ( 18 | "container/list" 19 | "flag" 20 | "go/format" 21 | "io/ioutil" 22 | 23 | qapischema "github.com/digitalocean/go-qemu/qapi-schema" 24 | ) 25 | 26 | const specURL = `https://raw.githubusercontent.com/qemu/qemu/stable-2.11/qapi-schema.json` 27 | 28 | var ( 29 | inputSpec = flag.String("input", specURL, "Input spec") 30 | outputGo = flag.String("output", "", "Generated code output") 31 | templates = flag.String("templates", "", "Template directory") 32 | ) 33 | 34 | // Generate parses a QMP API spec and generates a Go implementation. 35 | func Generate() error { 36 | flag.Parse() 37 | 38 | src, err := getQAPI(*inputSpec) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | tree, err := qapischema.Parse(string(src)) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | tree, err = completeParseTree(tree, *inputSpec) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | symbols := lowerParseTree(tree) 54 | 55 | need := neededTypes(symbols) 56 | bs, err := renderAPI(*templates, symbols, need) 57 | if err != nil { 58 | return err 59 | } 60 | formatted, err := format.Source(bs) 61 | if err != nil { 62 | // Ignore the error, we're just trying to write out the 63 | // unformatted code to help debugging. 64 | ioutil.WriteFile(*outputGo, bs, 0640) 65 | return err 66 | } 67 | 68 | return ioutil.WriteFile(*outputGo, formatted, 0640) 69 | } 70 | 71 | // completeParseTree walks the parse tree and expands the Include statements. 72 | // It returns the completely resolved parse tree. 73 | func completeParseTree(tree *qapischema.Tree, base string) (*qapischema.Tree, error) { 74 | if inc, ok := tree.Node.(qapischema.Include); ok { 75 | src, err := resolvePath(base, string(inc)) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | // Any include directives in a subtree of this node will use 81 | // *this* include directive as its main path. 82 | base = src 83 | 84 | srcBytes, err := getQAPI(src) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | root, err := qapischema.Parse(string(srcBytes)) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | tree.Children = root.Children 95 | } 96 | 97 | for i, subtree := range tree.Children { 98 | st, err := completeParseTree(subtree, base) 99 | if err != nil { 100 | return tree, err 101 | } 102 | 103 | tree.Children[i] = st 104 | } 105 | 106 | return tree, nil 107 | } 108 | 109 | // lowerParseTree converts the parsed types from package qapischema into 110 | // types that are wired up for Go code generation. 111 | func lowerParseTree(tree *qapischema.Tree) map[name]interface{} { 112 | types := make(map[name]interface{}) 113 | 114 | remaining := list.New() 115 | remaining.PushFront(tree) 116 | 117 | for remaining.Len() > 0 { 118 | tree := remaining.Remove(remaining.Front()).(*qapischema.Tree) 119 | 120 | for _, t := range tree.Children { 121 | remaining.PushBack(t) 122 | } 123 | 124 | switch node := tree.Node.(type) { 125 | case qapischema.Root: 126 | // Nothing to do, subtrees were already added to the queue above. 127 | case qapischema.Include: 128 | // Nothing to do, subtrees were already added to the queue above. 129 | case qapischema.Pragma: 130 | // Ignored by go-qemu. 131 | case *qapischema.Struct: 132 | v := structType{ 133 | Name: name(node.Name), 134 | Base: name(node.Base), 135 | } 136 | 137 | for _, member := range node.Members { 138 | f := field{ 139 | Name: name(member.Name), 140 | Optional: member.Optional, 141 | } 142 | 143 | if member.Type.TypeArray != "" { 144 | f.Type = name(member.Type.TypeArray) 145 | f.List = true 146 | } else { 147 | f.Type = name(member.Type.Type) 148 | } 149 | v.Fields = append(v.Fields, f) 150 | } 151 | 152 | types[v.Name] = v 153 | 154 | case *qapischema.Command: 155 | v := command{ 156 | Name: name(node.Name), 157 | } 158 | 159 | if node.Boxed != nil { 160 | v.BoxedInput = *node.Boxed 161 | } 162 | 163 | if node.Data.Ref != nil { 164 | v.Inputs.Ref = name(*node.Data.Ref) 165 | } else if node.Data.Embed != nil { 166 | var fs fields 167 | for _, member := range node.Data.Embed.Members { 168 | f := field{ 169 | Name: name(member.Name), 170 | Optional: member.Optional, 171 | } 172 | 173 | if member.Type.TypeArray != "" { 174 | f.List = true 175 | f.Type = name(member.Type.TypeArray) 176 | } else { 177 | f.Type = name(member.Type.Type) 178 | } 179 | 180 | fs = append(fs, f) 181 | v.Inputs.AnonFields = fs 182 | } 183 | } 184 | 185 | if node.Returns != nil { 186 | // TODO: Output.Name is confusing 187 | if node.Returns.TypeArray != "" { 188 | v.Output = field{ 189 | Name: name(node.Returns.TypeArray), 190 | List: true, 191 | Type: name(node.Returns.TypeArray), 192 | // TODO: Optional: 193 | } 194 | } else { 195 | v.Output = field{ 196 | Name: name(node.Returns.Type), 197 | Type: name(node.Returns.Type), 198 | // TODO: Optional: 199 | } 200 | } 201 | } 202 | 203 | types[v.Name] = v 204 | case *qapischema.Event: 205 | v := event{ 206 | Name: name(node.Name), 207 | } 208 | 209 | if node.Data.Selector.Embed != nil { 210 | v.Data = fieldsOrRef{ 211 | Ref: name(node.Data.Selector.Ref), 212 | } 213 | if v.Data.Ref != "" { 214 | v.BoxedData = true 215 | } 216 | 217 | for _, member := range node.Data.Selector.Embed.Members { 218 | f := field{ 219 | Name: name(member.Name), 220 | Optional: member.Optional, 221 | } 222 | 223 | if member.Type.TypeArray != "" { 224 | f.List = true 225 | f.Type = name(member.Type.TypeArray) 226 | } else { 227 | f.Type = name(member.Type.Type) 228 | } 229 | 230 | v.Data.AnonFields = append(v.Data.AnonFields, f) 231 | } 232 | } 233 | 234 | types[v.Name] = v 235 | case *qapischema.Alternate: 236 | v := alternate{ 237 | Name: name(node.Name), 238 | Options: make(map[name]name), 239 | } 240 | 241 | for _, opt := range node.Data { 242 | n := opt.Type.Type 243 | if opt.Type.TypeArray != "" { 244 | n = string(opt.Type.TypeArray) 245 | } 246 | v.Options[name(opt.Name)] = name(n) 247 | } 248 | 249 | types[v.Name] = v 250 | case *qapischema.Union: 251 | if node.Discriminator == "" { 252 | v := simpleUnion{ 253 | Name: name(node.Name), 254 | Options: make(map[name]name), 255 | } 256 | 257 | for _, branch := range node.Branches { 258 | b := string(branch.Type.Type) 259 | if branch.Type.TypeArray != "" { 260 | b = string(branch.Type.TypeArray) 261 | } 262 | v.Options[name(branch.Name)] = name(b) 263 | } 264 | 265 | types[v.Name] = v 266 | } else { 267 | v := flatUnion{ 268 | Name: name(node.Name), 269 | Discriminator: name(node.Discriminator), 270 | Base: fieldsOrRef{ 271 | Ref: name(node.Base.Name), 272 | }, 273 | Options: make(map[name]name), 274 | } 275 | 276 | for _, member := range node.Base.Members { 277 | f := field{ 278 | Name: name(member.Name), 279 | Optional: member.Optional, 280 | } 281 | 282 | if member.Type.TypeArray != "" { 283 | f.List = true 284 | f.Type = name(member.Type.TypeArray) 285 | } else { 286 | f.Type = name(member.Type.Type) 287 | } 288 | 289 | v.Base.AnonFields = append(v.Base.AnonFields, f) 290 | } 291 | 292 | for _, branch := range node.Branches { 293 | b := string(branch.Type.Type) 294 | if branch.Type.TypeArray != "" { 295 | b = string(branch.Type.TypeArray) 296 | } 297 | v.Options[name(branch.Name)] = name(b) 298 | } 299 | 300 | types[v.Name] = v 301 | } 302 | 303 | case *qapischema.Enum: 304 | v := enum{ 305 | Name: name(node.Name), 306 | } 307 | 308 | for _, val := range node.Values { 309 | v.Values = append(v.Values, name(val.Value)) 310 | } 311 | 312 | types[v.Name] = v 313 | } 314 | } 315 | 316 | return types 317 | } 318 | -------------------------------------------------------------------------------- /internal/qmp-gen/parse_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gen 16 | 17 | import ( 18 | "bytes" 19 | "go/format" 20 | "net/http" 21 | "net/http/httptest" 22 | "strings" 23 | "testing" 24 | 25 | qapischema "github.com/digitalocean/go-qemu/qapi-schema" 26 | ) 27 | 28 | func TestGenerate(t *testing.T) { 29 | tests := []struct { 30 | name string 31 | in []byte 32 | out []byte 33 | }{ 34 | { 35 | name: "StructInfo", 36 | in: []byte(` 37 | ## 38 | # @StatusInfo: 39 | # 40 | # Information about VCPU run state 41 | # 42 | # @running: true if all VCPUs are runnable, false if not runnable 43 | # 44 | # @singlestep: true if VCPUs are in single-step mode 45 | # 46 | # @status: the virtual machine @RunState 47 | # 48 | # Since: 0.14.0 49 | # 50 | # Notes: @singlestep is enabled through the GDB stub 51 | ## 52 | { 'struct': 'StatusInfo', 53 | 'data': {'running': 'bool', 'singlestep': 'bool', 'status': 'RunState'} } 54 | `), 55 | out: []byte(` 56 | // StatusInfo -> StatusInfo (struct) 57 | 58 | // StatusInfo implements the "StatusInfo" QMP API type. 59 | type StatusInfo struct { 60 | Running bool 'json:"running"' 61 | Singlestep bool 'json:"singlestep"' 62 | Status RunState 'json:"status"' 63 | } 64 | `), 65 | }, 66 | } 67 | 68 | for _, tt := range tests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | // Replace single quote with backtick to match generated Go 71 | // source code 72 | tt.out = bytes.TrimSpace(bytes.Replace(tt.out, []byte("'"), []byte("`"), -1)) 73 | 74 | // Do not check needed types, third parameter is false 75 | source := testGenerate(t, tt.in, false) 76 | 77 | if want, got := tt.out, source; !bytes.Contains(got, want) { 78 | t.Fatalf("generated code does not match actual code:\n- want: %s\n- got: %s", 79 | indent(string(want)), indent(string(got))) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestGenerateNeededTypes(t *testing.T) { 86 | tests := []struct { 87 | name string 88 | in []byte 89 | out []byte 90 | }{ 91 | { 92 | name: "no output: no command that uses StructInfo", 93 | in: []byte(` 94 | ## 95 | # @StatusInfo: 96 | # 97 | # Information about VCPU run state 98 | # 99 | # @running: true if all VCPUs are runnable, false if not runnable 100 | # 101 | # @singlestep: true if VCPUs are in single-step mode 102 | # 103 | # @status: the virtual machine @RunState 104 | # 105 | # Since: 0.14.0 106 | # 107 | # Notes: @singlestep is enabled through the GDB stub 108 | ## 109 | { 'struct': 'StatusInfo', 110 | 'data': {'running': 'bool', 'singlestep': 'bool', 'status': 'RunState'} } 111 | `), 112 | }, 113 | { 114 | name: "StatusInfo with query-status command", 115 | in: []byte(` 116 | ## 117 | # @StatusInfo: 118 | # 119 | # Information about VCPU run state 120 | # 121 | # @running: true if all VCPUs are runnable, false if not runnable 122 | # 123 | # @singlestep: true if VCPUs are in single-step mode 124 | # 125 | # @status: the virtual machine @RunState 126 | # 127 | # Since: 0.14.0 128 | # 129 | # Notes: @singlestep is enabled through the GDB stub 130 | ## 131 | { 'struct': 'StatusInfo', 132 | 'data': {'running': 'bool', 'singlestep': 'bool', 'status': 'RunState'} } 133 | 134 | ## 135 | # @query-status: 136 | # 137 | # Query the run status of all VCPUs 138 | # 139 | # Returns: @StatusInfo reflecting all VCPUs 140 | # 141 | # Since: 0.14.0 142 | ## 143 | { 'command': 'query-status', 'returns': 'StatusInfo' } 144 | `), 145 | out: []byte(` 146 | // StatusInfo -> StatusInfo (struct) 147 | 148 | // StatusInfo implements the "StatusInfo" QMP API type. 149 | type StatusInfo struct { 150 | Running bool 'json:"running"' 151 | Singlestep bool 'json:"singlestep"' 152 | Status RunState 'json:"status"' 153 | } 154 | 155 | // query-status -> QueryStatus (command) 156 | 157 | // QueryStatus implements the "query-status" QMP API call. 158 | func (m *Monitor) QueryStatus() (ret StatusInfo, err error) { 159 | cmd := struct { 160 | }{} 161 | bs, err := json.Marshal(map[string]interface{}{ 162 | "execute": "query-status", 163 | "arguments": cmd, 164 | }) 165 | if err != nil { 166 | return 167 | } 168 | bs, err = m.mon.Run(bs) 169 | if err != nil { 170 | return 171 | } 172 | res := struct { 173 | Res json.RawMessage 'json:"return"' 174 | }{} 175 | if err = json.Unmarshal(bs, &res); err != nil { 176 | return 177 | } 178 | if err = json.Unmarshal([]byte(res.Res), &ret); err != nil { 179 | return 180 | } 181 | return 182 | }`), 183 | }, 184 | } 185 | 186 | for _, tt := range tests { 187 | t.Run(tt.name, func(t *testing.T) { 188 | // Replace single quote with backtick to match generated Go 189 | // source code 190 | tt.out = bytes.TrimSpace(bytes.Replace(tt.out, []byte("'"), []byte("`"), -1)) 191 | 192 | // Check needed types, third parameter is true 193 | source := testGenerate(t, tt.in, true) 194 | 195 | if want, got := tt.out, source; !bytes.Contains(got, want) { 196 | t.Fatalf("generated code does not match actual code:\n- want: %s\n- got: %s", 197 | indent(string(want)), indent(string(got))) 198 | } 199 | }) 200 | } 201 | } 202 | 203 | func indent(s string) string { 204 | return strings.Replace(s, "\n", "\n ", -1) 205 | } 206 | 207 | func testGenerate(t *testing.T, in []byte, checkNeededTypes bool) []byte { 208 | s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 209 | _, _ = w.Write(in) 210 | })) 211 | defer s.Close() 212 | 213 | data, err := getQAPI(s.URL) 214 | if err != nil { 215 | t.Fatalf("unexpected error: %v", err) 216 | } 217 | 218 | tree, err := qapischema.Parse(string(data)) 219 | if err != nil { 220 | t.Fatalf("unexpected error: %v", err) 221 | } 222 | 223 | tree, err = completeParseTree(tree, s.URL) 224 | if err != nil { 225 | t.Fatalf("unexpected error: %v", err) 226 | } 227 | 228 | symbols := lowerParseTree(tree) 229 | 230 | need := symbols 231 | if checkNeededTypes { 232 | need = neededTypes(symbols) 233 | } 234 | 235 | bs, err := renderAPI("templates/", symbols, need) 236 | if err != nil { 237 | t.Fatalf("unexpected error: %v", err) 238 | } 239 | 240 | formatted, err := format.Source(bs) 241 | if err != nil { 242 | t.Fatalf("unexpected error: %v", err) 243 | } 244 | 245 | return formatted 246 | } 247 | -------------------------------------------------------------------------------- /internal/qmp-gen/templates/alternate: -------------------------------------------------------------------------------- 1 | {{/* 2 | 3 | There are very few alternates in the API, but one of them 4 | (BlockDevRef) is quite tricky, because it references a flat union as 5 | one of its implementations. So, a lot of this logic is specific to 6 | supporting flat unions as implementations of an alternate. 7 | 8 | */}} 9 | 10 | {{ $basename := .Name.Go }} 11 | 12 | // {{ .Name }} -> {{ $basename }} (alternate) 13 | 14 | // {{ $basename }} implements the "{{ .Name }}" QMP API type. 15 | // 16 | // Can be one of: 17 | {{- range $suffix, $type := .Options }} 18 | // - {{ $basename }}{{ $suffix.Go }} 19 | {{- end }} 20 | type {{ $basename }} interface { 21 | is{{ $basename }}() 22 | } 23 | 24 | {{- range $suffix, $type := .Options }} 25 | {{ $subname := printf "%s%s" $basename $suffix.Go }} 26 | 27 | {{- if $type.NullType }} 28 | // {{ $subname }} is a JSON null type, so it must 29 | // also implement the isNullable interface. 30 | type {{ $subname }} struct {} 31 | func ({{ $subname }}) isNull() bool { return true } 32 | {{- else }} 33 | // {{ $subname }} is an implementation of {{ $basename }} 34 | type {{ $subname }} {{ $type.Go }} 35 | {{- end }} 36 | 37 | {{- if eq (typeOf (index API $type)) "flatunion" }} 38 | {{ $u := index API $type }} 39 | {{- range $suffix, $type := $u.Options }} 40 | func ({{ $u.Name.Go }}{{ $suffix.Go }}) is{{ $basename }}() {} 41 | {{- end }} 42 | {{- else }} 43 | func ({{ $subname }}) is{{ $basename }}() {} 44 | {{- end }} 45 | {{- end }} 46 | 47 | func decode{{ $basename }}(bs json.RawMessage) ({{ $basename }}, error) { 48 | {{/* 49 | Unfortunately, we have to range through the types three times in order 50 | for us to do the type discovery via unmarshalling in the correct order 51 | */}} 52 | {{- range $suffix, $type := .Options }} 53 | {{- if $type.NullType }} 54 | // Always try unmarshalling for nil first if it's an option 55 | // because other types could unmarshal successfully in the case 56 | // where a Null json type was provided. 57 | var {{ $suffix }} *int 58 | if err := json.Unmarshal([]byte(bs), &{{ $suffix }}); err == nil { 59 | if {{ $suffix }} == nil { 60 | return {{ printf "%s%s" $basename $suffix.Go }}{}, nil 61 | } 62 | } 63 | {{- end }} 64 | {{- end }} 65 | {{- range $suffix, $type := .Options }} 66 | {{- if $type.SimpleType }} 67 | var {{ $suffix }} {{ printf "%s%s" $basename $suffix.Go }} 68 | if err := json.Unmarshal([]byte(bs), &{{ $suffix }}); err == nil { 69 | return {{ $suffix }}, nil 70 | } 71 | {{- end }} 72 | {{- end }} 73 | {{- range $suffix, $type := .Options }} 74 | {{- if not $type.NullType }} 75 | {{- if not $type.SimpleType }} 76 | {{ $subtype := index API $type }} 77 | {{- if eq (typeOf $subtype) "flatunion" }} 78 | if {{ $suffix }}, err := decode{{ $type.Go }}([]byte(bs)); err == nil { 79 | switch impl := {{ $suffix }}.(type) { 80 | {{- range $suffix, $type := $subtype.Options }} 81 | case {{ $subtype.Name.Go }}{{ $suffix.Go }}: 82 | return impl, nil 83 | {{- end }} 84 | } 85 | } 86 | {{- else }} 87 | var {{ $suffix }} {{ printf "%s%s" $basename $suffix.Go }} 88 | if err := json.Unmarshal([]byte(bs), &{{ $suffix }}); err == nil { 89 | return {{ $suffix }}, nil 90 | } 91 | {{- end }} 92 | {{- end }} 93 | {{- end }} 94 | {{- end }} 95 | return nil, fmt.Errorf("unable to decode %q as a {{ $basename }}", string(bs)) 96 | } 97 | -------------------------------------------------------------------------------- /internal/qmp-gen/templates/command: -------------------------------------------------------------------------------- 1 | // {{ .Name }} -> {{ .Name.Go }} (command) 2 | 3 | {{ define "funcArg" -}} 4 | {{- if .List -}} 5 | {{ unexport .Name.FieldName }} []{{ .Type.Go }} 6 | {{- else if and .Optional (not (.Type.InterfaceType API)) -}} 7 | {{ unexport .Name.FieldName }} *{{ .Type.Go }} 8 | {{- else -}} 9 | {{ unexport .Name.FieldName }} {{ .Type.Go }} 10 | {{- end -}} 11 | {{- end }} 12 | 13 | {{ define "funcArgs" -}} 14 | {{- if .BoxedInput -}} 15 | cmd *{{ .Inputs.Ref.Go }} 16 | {{- else -}} 17 | {{- $inputs := .Inputs.Fields API -}} 18 | {{- range $i, $f := $inputs -}} 19 | {{- template "funcArg" $f -}} 20 | {{- if not (last $inputs $i) -}},{{- end -}} 21 | {{- end -}} 22 | {{- end -}} 23 | {{- end }} 24 | 25 | {{ define "retVal" -}} 26 | {{- if eq .Type "" -}} 27 | (err error) 28 | {{- else if .List -}} 29 | (ret []{{ .Type.Go }}, err error) 30 | {{- else if and .Optional (not (.Type.InterfaceType API)) -}} 31 | (ret *{{ .Type.Go }}, err error) 32 | {{- else -}} 33 | (ret {{ .Type.Go }}, err error) 34 | {{- end -}} 35 | {{- end }} 36 | 37 | // {{ .Name.Go }} implements the "{{ .Name }}" QMP API call. 38 | func (m *Monitor) {{ .Name.Go }}({{ template "funcArgs" . }}) {{ template "retVal" .Output }} { 39 | {{- if not .BoxedInput }} 40 | cmd := struct{ 41 | {{- range .Inputs.Fields API }} 42 | {{ render . }} 43 | {{- end }} 44 | }{ 45 | {{- range .Inputs.Fields API }} 46 | {{ unexport .Name.FieldName }}, 47 | {{- end }} 48 | } 49 | {{- end }} 50 | bs, err := json.Marshal(map[string]interface{}{ 51 | "execute": "{{ .Name }}", 52 | "arguments": cmd, 53 | }) 54 | if err != nil { 55 | return 56 | } 57 | bs, err = m.mon.Run(bs) 58 | if err != nil { 59 | return 60 | } 61 | 62 | {{- if ne .Output.Type "" }} 63 | res := struct{ 64 | Res json.RawMessage `json:"return"` 65 | }{} 66 | if err = json.Unmarshal(bs, &res); err != nil { 67 | return 68 | } 69 | {{- if .Output.Type.InterfaceType API }} 70 | {{- if .Output.List }} 71 | var reslist []json.RawMessage 72 | if err = json.Unmarshal([]byte(res.Res), &reslist); err != nil { 73 | return 74 | } 75 | for _, r := range reslist { 76 | v, err := decode{{ .Output.Type.Go }}(r) 77 | if err != nil { 78 | return nil, err 79 | } 80 | ret = append(ret, v) 81 | } 82 | {{- else }} 83 | return decode{{ .Output.Type.Go }}(res.Res) 84 | {{- end }} 85 | {{- else }} 86 | if err = json.Unmarshal([]byte(res.Res), &ret); err != nil { 87 | return 88 | } 89 | {{- end }} 90 | {{- end }} 91 | return 92 | } 93 | -------------------------------------------------------------------------------- /internal/qmp-gen/templates/enum: -------------------------------------------------------------------------------- 1 | {{- $enum := . -}} 2 | 3 | // {{ .Name }} -> {{ .Name.Go }} (enum) 4 | 5 | // {{ .Name.Go }} implements the "{{ .Name }}" QMP API type. 6 | type {{ $enum.Name.Go }} int 7 | 8 | // Known values of {{ .Name.Go }}. 9 | const ( 10 | {{- range $i, $v := $enum.Values }} 11 | {{- if eq $i 0 }} 12 | {{ $enum.Name.Go }}{{ $v.Go }} {{ $enum.Name.Go }} = iota 13 | {{- else }} 14 | {{ $enum.Name.Go }}{{ $v.Go }} 15 | {{- end }} 16 | {{- end }} 17 | ) 18 | 19 | // String implements fmt.Stringer. 20 | func (e {{ $enum.Name.Go }}) String() string { 21 | switch e { 22 | {{- range $enum.Values }} 23 | case {{ $enum.Name.Go }}{{ .Go }}: 24 | return "{{ . }}" 25 | {{- end }} 26 | default: 27 | return fmt.Sprintf("{{ $enum.Name.Go }}(%d)", e) 28 | } 29 | } 30 | 31 | // MarshalJSON implements json.Marshaler. 32 | func (e {{ $enum.Name.Go }}) MarshalJSON() ([]byte, error) { 33 | switch e { 34 | {{- range $enum.Values }} 35 | case {{ $enum.Name.Go }}{{ .Go }}: 36 | return json.Marshal("{{ . }}") 37 | {{- end }} 38 | default: 39 | return nil, fmt.Errorf("unknown enum value %q for {{ $enum.Name.Go }}", e) 40 | } 41 | } 42 | 43 | // UnmarshalJSON implements json.Unmarshaler. 44 | func (e *{{ $enum.Name.Go }}) UnmarshalJSON(bs []byte) error { 45 | var s string 46 | if err := json.Unmarshal(bs, &s); err != nil { 47 | return err 48 | } 49 | switch s { 50 | {{- range $enum.Values }} 51 | case "{{ . }}": 52 | *e = {{ $enum.Name.Go }}{{ .Go }} 53 | {{- end }} 54 | default: 55 | return fmt.Errorf("unknown enum value %q for {{ $enum.Name.Go }}", s) 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/qmp-gen/templates/event: -------------------------------------------------------------------------------- 1 | // EVENT {{ .Name }} 2 | -------------------------------------------------------------------------------- /internal/qmp-gen/templates/field: -------------------------------------------------------------------------------- 1 | {{- if .List -}} 2 | {{ .Name.FieldName }} []{{ .Type.Go }} 3 | {{- else if and .Optional (not (.Type.InterfaceType API)) -}} 4 | {{ .Name.FieldName }} *{{ .Type.Go }} 5 | {{- else -}} 6 | {{ .Name.FieldName }} {{ .Type.Go }} 7 | {{- end -}} 8 | 9 | {{- if .Optional }} `json:"{{ .Name }},omitempty"` 10 | {{- else }} `json:"{{ .Name }}"` 11 | {{- end -}} 12 | -------------------------------------------------------------------------------- /internal/qmp-gen/templates/flatunion: -------------------------------------------------------------------------------- 1 | {{ $u := . }} 2 | {{ $basename := $u.Name.Go }} 3 | {{ $discriminatorField := $u.DiscriminatorField API }} 4 | 5 | // {{ $u.Name }} -> {{ $basename }} (flat union) 6 | 7 | // {{ $basename }} implements the "{{ .Name }}" QMP API type. 8 | // 9 | // Can be one of: 10 | {{- range $suffix, $type := $u.Options }} 11 | // - {{ $basename }}{{ $suffix.Go }} 12 | {{- end }} 13 | type {{ $basename }} interface { 14 | is{{ $basename }}() 15 | } 16 | 17 | {{- range $suffix, $type := $u.Options }} 18 | {{ $subname := printf "%s%s" $basename $suffix.Go }} 19 | {{ $subtype := (index API $type) }} 20 | 21 | // {{ $subname }} is an implementation of {{ $basename }}. 22 | type {{ $subname }} struct { 23 | {{- range $u.Base.Fields API }} 24 | {{- if ne .Name $u.Discriminator }} 25 | {{ render . }} 26 | {{- end }} 27 | {{- end }} 28 | {{- range $subtype.Fields }} 29 | {{ render . }} 30 | {{- end }} 31 | } 32 | 33 | func ({{ $subname }}) is{{ $basename }}() {} 34 | 35 | // MarshalJSON implements json.Marshaler. 36 | func (s {{ $subname }}) MarshalJSON() ([]byte, error) { 37 | v := struct{ 38 | {{- range $u.Base.Fields API }} 39 | {{- if eq .Name $u.Discriminator }} 40 | {{ render . }} 41 | {{- end }} 42 | {{- end }} 43 | {{ $subname }} 44 | }{ 45 | {{ $discriminatorField.Type.Go }}{{ $suffix.Go }}, 46 | s, 47 | } 48 | return json.Marshal(v) 49 | } 50 | {{- end }} 51 | 52 | func decode{{ $basename }}(bs json.RawMessage) ({{ $basename }}, error) { 53 | v := struct{ 54 | {{ render $discriminatorField }} 55 | }{} 56 | if err := json.Unmarshal([]byte(bs), &v); err != nil { 57 | return nil, err 58 | } 59 | switch v.{{ $discriminatorField.Name.Go }} { 60 | {{- range $suffix, $type := $u.Options }} 61 | case {{ $discriminatorField.Type.Go }}{{ $suffix.Go }}: 62 | var ret {{ $basename }}{{ $suffix.Go }} 63 | err := json.Unmarshal([]byte(bs), &ret) 64 | return ret, err 65 | {{- end }} 66 | default: 67 | return nil, fmt.Errorf("unknown flat union subtype %q for flat union {{ $basename }}", v.{{ $discriminatorField.Name.Go }}) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/qmp-gen/templates/main: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package raw 16 | 17 | // Generated using `go generate`, do not edit by hand! 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | ) 23 | 24 | // IsNullable is implemented by any 25 | // JSON null type 26 | type IsNullable interface { 27 | isNull() bool 28 | } 29 | 30 | {{ range . }} 31 | {{ render . }} 32 | {{ end }} 33 | -------------------------------------------------------------------------------- /internal/qmp-gen/templates/simpleunion: -------------------------------------------------------------------------------- 1 | {{/* 2 | 3 | It's important to note that union implementations are always a basic 4 | type or a struct, never another union. This means that we can safely 5 | decode union implementations with json.Unmarshal, so we don't have to 6 | handle recursively calling decodeFoo functions to unmarshal unions. 7 | 8 | Also, there is one simple union type whose name clashes with the 9 | underlying struct type: ImageInfoSpecificVmdk. The struct type is not 10 | used anywhere outside the simple union, so we just skip the 11 | redefinition, but still tack on all the marshaling logic. 12 | 13 | */}} 14 | 15 | {{ $basename := .Name.Go }} 16 | 17 | // {{ .Name }} -> {{ $basename }} (simple union) 18 | 19 | // {{ $basename }} implements the "{{ .Name }}" QMP API type. 20 | // 21 | // Can be one of: 22 | {{- range $suffix, $type := .Options }} 23 | // - {{ $basename }}{{ $suffix.Go }} 24 | {{- end }} 25 | type {{ $basename }} interface { 26 | is{{ $basename }}() 27 | } 28 | 29 | {{- range $suffix, $type := .Options }} 30 | {{ $subname := printf "%s%s" $basename $suffix.Go }} 31 | {{- if ne $subname $type.Go }} 32 | // {{ $subname }} is an implementation of {{ $basename }}. 33 | type {{ $subname }} {{ $type.Go }} 34 | {{- end }} 35 | 36 | func ({{ $subname }}) is{{ $basename }}() {} 37 | 38 | // MarshalJSON implements json.Marshaler. 39 | func (s {{ $subname }}) MarshalJSON() ([]byte, error) { 40 | v := map[string]interface{}{ 41 | "type": "{{ $suffix }}", 42 | "data": {{ $type.Go }} (s), 43 | } 44 | return json.Marshal(v) 45 | } 46 | {{- end }} 47 | 48 | func decode{{ $basename }}(bs json.RawMessage) ({{ $basename }}, error) { 49 | v := struct{ 50 | T string `json:"type"` 51 | V json.RawMessage `json:"data"` 52 | }{} 53 | if err := json.Unmarshal([]byte(bs), &v); err != nil { 54 | return nil, err 55 | } 56 | switch v.T { 57 | {{- range $suffix, $type := .Options }} 58 | case "{{ $suffix }}": 59 | var ret {{ $basename }}{{ $suffix.Go }} 60 | if err := json.Unmarshal([]byte(v.V), &ret); err != nil { 61 | return nil, err 62 | } 63 | return ret, nil 64 | {{- end }} 65 | default: 66 | return nil, fmt.Errorf("unknown subtype %q for union {{ $basename }}", v.T) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/qmp-gen/templates/structtype: -------------------------------------------------------------------------------- 1 | // {{ .Name }} -> {{ .Name.Go }} (struct) 2 | 3 | // {{ .Name.Go }} implements the "{{ .Name }}" QMP API type. 4 | type {{ .Name.Go }} struct { 5 | {{- range .AllFields API }} 6 | {{ render . }} 7 | {{- end }} 8 | } 9 | 10 | {{- if .HasInterfaceField API }} 11 | // UnmarshalJSON implements json.Unmarshaler. 12 | func (s *{{ .Name.Go }}) UnmarshalJSON(bs []byte) error { 13 | v := struct{ 14 | {{- range .AllFields API }} 15 | {{- if .Type.InterfaceType API }} 16 | {{- if .List }} 17 | {{ abort "rendering of list of union/alternate not supported" }} 18 | {{- else }} 19 | {{ .Name.Go }} json.RawMessage `json:"{{ .Name }}"` 20 | {{- end }} 21 | {{- else }} 22 | {{ render . }} 23 | {{- end }} 24 | {{- end }} 25 | }{} 26 | err := json.Unmarshal(bs, &v) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | {{- range .AllFields API }} 32 | {{- if .Type.InterfaceType API }} 33 | {{- if .Optional }} 34 | if len(v.{{ .Name.Go }}) > 0 { 35 | {{- end }} 36 | s.{{ .Name.Go }}, err = decode{{ .Type.Go }}(v.{{ .Name.Go }}) 37 | if err != nil { 38 | return err 39 | } 40 | {{- if .Optional }} 41 | } else { 42 | s.{{ .Name.Go }} = nil 43 | } 44 | {{- end }} 45 | {{- else }} 46 | s.{{ .Name.Go }} = v.{{ .Name.Go }} 47 | {{- end }} 48 | {{- end }} 49 | 50 | return nil 51 | } 52 | {{- end }} 53 | -------------------------------------------------------------------------------- /internal/qmp-gen/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gen 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "fmt" 21 | "io/ioutil" 22 | "net/http" 23 | "net/url" 24 | "path/filepath" 25 | "strings" 26 | 27 | "github.com/fatih/camelcase" 28 | ) 29 | 30 | // a definition is the definition of one QMP API type and its docstring. 31 | 32 | type event struct { 33 | Name name `json:"event"` 34 | Data fieldsOrRef `json:"data"` 35 | BoxedData bool `json:"boxed"` 36 | } 37 | 38 | type command struct { 39 | Name name 40 | Inputs fieldsOrRef 41 | Output field 42 | BoxedInput bool 43 | } 44 | 45 | type alternate struct { 46 | Name name `json:"alternate"` 47 | Options map[name]name `json:"data"` 48 | } 49 | 50 | type flatUnion struct { 51 | Name name 52 | Base fieldsOrRef 53 | Discriminator name 54 | Options map[name]name 55 | } 56 | 57 | func (fu flatUnion) DiscriminatorField(api map[name]interface{}) field { 58 | for _, f := range fu.Base.Fields(api) { 59 | if f.Name == fu.Discriminator { 60 | return f 61 | } 62 | } 63 | panic("no discriminator field found") 64 | } 65 | 66 | type simpleUnion struct { 67 | Name name 68 | Options map[name]name 69 | } 70 | 71 | type structType struct { 72 | Name name `json:"struct"` 73 | Fields fields `json:"data"` 74 | Base name `json:"base"` 75 | } 76 | 77 | func (s structType) AllFields(api map[name]interface{}) fields { 78 | var ret fields 79 | if s.Base != "" { 80 | ret = append(ret, api[s.Base].(structType).Fields...) 81 | } 82 | ret = append(ret, s.Fields...) 83 | return ret 84 | } 85 | 86 | func (s structType) HasInterfaceField(api map[name]interface{}) bool { 87 | if s.Fields.HasInterfaceField(api) { 88 | return true 89 | } 90 | if s.Base != "" && api[s.Base].(structType).Fields.HasInterfaceField(api) { 91 | return true 92 | } 93 | return false 94 | } 95 | 96 | type enum struct { 97 | Name name `json:"enum"` 98 | Values []name `json:"data"` 99 | } 100 | 101 | type field struct { 102 | Name name 103 | List bool 104 | Optional bool 105 | Type name 106 | } 107 | 108 | type fields []field 109 | 110 | func (fs fields) HasInterfaceField(api map[name]interface{}) bool { 111 | for _, f := range fs { 112 | if f.Type.InterfaceType(api) { 113 | return true 114 | } 115 | } 116 | return false 117 | } 118 | 119 | type fieldsOrRef struct { 120 | AnonFields fields 121 | Ref name 122 | } 123 | 124 | func (f fieldsOrRef) Fields(api map[name]interface{}) fields { 125 | if f.AnonFields != nil { 126 | return f.AnonFields 127 | } 128 | if f.Ref != "" { 129 | return api[f.Ref].(structType).Fields 130 | } 131 | return nil 132 | } 133 | 134 | func (f *fieldsOrRef) UnmarshalJSON(bs []byte) error { 135 | var s name 136 | if err := json.Unmarshal(bs, &s); err == nil { 137 | f.AnonFields = nil 138 | f.Ref = s 139 | return nil 140 | } 141 | return json.Unmarshal(bs, &f.AnonFields) 142 | } 143 | 144 | func (fs *fields) UnmarshalJSON(bs []byte) error { 145 | d := json.NewDecoder(bytes.NewBuffer(bs)) 146 | 147 | // Dictionary starts with { 148 | t, err := d.Token() 149 | if err != nil { 150 | return err 151 | } 152 | if delim, ok := t.(json.Delim); !ok || delim != '{' { 153 | return fmt.Errorf("unexpected token %q (%T)", t, t) 154 | } 155 | 156 | // Then a series of key/value pairs until }. 157 | for { 158 | t, err := d.Token() 159 | if err != nil { 160 | return err 161 | } 162 | 163 | // End of the dict? 164 | if delim, ok := t.(json.Delim); ok && delim == '}' { 165 | break 166 | } 167 | 168 | var field field 169 | 170 | // Dictionary key 171 | s, ok := t.(string) 172 | if !ok { 173 | return fmt.Errorf("expected dictionary key (string), got %q (%T)", t, t) 174 | } 175 | if s[0] == '*' { 176 | field.Name = name(s[1:]) 177 | field.Optional = true 178 | } else { 179 | field.Name = name(s) 180 | } 181 | 182 | // The value, either a string or a list of one string. 183 | t, err = d.Token() 184 | if err != nil { 185 | return err 186 | } 187 | 188 | switch v := t.(type) { 189 | case string: 190 | field.Type = name(v) 191 | case json.Delim: 192 | if v != '[' { 193 | return fmt.Errorf("unexpected delimiter %q, wanted a dict value", v) 194 | } 195 | 196 | // The actual type name 197 | t, err = d.Token() 198 | if err != nil { 199 | return err 200 | } 201 | s, ok := t.(string) 202 | if !ok { 203 | return fmt.Errorf("unexpected token %q (%T), wanted string", t, t) 204 | } 205 | // Closing square bracket 206 | t, err := d.Token() 207 | if err != nil { 208 | return err 209 | } 210 | if delim, ok := t.(json.Delim); !ok || delim != ']' { 211 | return fmt.Errorf("unexpected token %q (%T), wanted ']'", t, t) 212 | } 213 | 214 | field.Type = name(s) 215 | field.List = true 216 | default: 217 | return fmt.Errorf("unexpected token %q (%T), wanted string or '['", t, t) 218 | } 219 | 220 | *fs = append(*fs, field) 221 | } 222 | return nil 223 | } 224 | 225 | type name string 226 | 227 | func (n name) Go() string { 228 | if v, ok := builtinTypes[string(n)]; ok { 229 | return v 230 | } 231 | 232 | return n.FieldName() 233 | } 234 | 235 | func (n name) FieldName() string { 236 | out := []string{} 237 | for _, p := range camelcase.Split(string(n)) { 238 | if p == "-" || p == "_" { 239 | continue 240 | } 241 | if upperWords[strings.ToLower(p)] { 242 | p = strings.ToUpper(p) 243 | } else { 244 | p = strings.Title(strings.ToLower(p)) 245 | } 246 | out = append(out, p) 247 | 248 | } 249 | 250 | return strings.Join(out, "") 251 | } 252 | 253 | func (n name) SimpleType() bool { 254 | _, ok := builtinTypes[string(n)] 255 | return ok 256 | } 257 | 258 | func (n name) NullType() bool { 259 | if n.SimpleType() { 260 | return false 261 | } 262 | return strings.EqualFold(string(n), "Null") 263 | } 264 | 265 | func (n name) InterfaceType(api map[name]interface{}) bool { 266 | if n.SimpleType() { 267 | return false 268 | } 269 | switch api[n].(type) { 270 | case simpleUnion, flatUnion, alternate: 271 | return true 272 | } 273 | return false 274 | } 275 | 276 | var builtinTypes = map[string]string{ 277 | "str": "string", 278 | "number": "float64", 279 | "int": "int64", 280 | "int8": "int8", 281 | "int16": "int16", 282 | "int32": "int32", 283 | "int64": "int64", 284 | "uint8": "uint8", 285 | "uint16": "uint16", 286 | "uint32": "uint32", 287 | "uint64": "uint64", 288 | "size": "uint64", 289 | "bool": "bool", 290 | "any": "interface{}", 291 | } 292 | 293 | var upperWords = map[string]bool{ 294 | "acpi": true, 295 | "acpiost": true, 296 | "aio": true, 297 | "cpu": true, 298 | "fd": true, 299 | "ftp": true, 300 | "ftps": true, 301 | "guid": true, 302 | "http": true, 303 | "https": true, 304 | "id": true, 305 | "io": true, 306 | "ip": true, 307 | "json": true, 308 | "kvm": true, 309 | "luks": true, 310 | "mips": true, 311 | "nbd": true, 312 | "ok": true, 313 | "pci": true, 314 | "ppc": true, 315 | "qmp": true, 316 | "ram": true, 317 | "sparc": true, 318 | "ssh": true, 319 | "tcp": true, 320 | "tls": true, 321 | "tpm": true, 322 | "ttl": true, 323 | "udp": true, 324 | "uri": true, 325 | "uuid": true, 326 | "vm": true, 327 | "vmdk": true, 328 | "vnc": true, 329 | } 330 | 331 | func resolvePath(orig, new string) (string, error) { 332 | u, err := url.Parse(orig) 333 | if err != nil { 334 | return "", err 335 | } 336 | u.Path = filepath.Join(filepath.Dir(u.Path), new) 337 | return u.String(), nil 338 | } 339 | 340 | // getQAPI reads a QMP API spec file, from local disk or over HTTP(S). 341 | func getQAPI(path string) ([]byte, error) { 342 | u, err := url.Parse(path) 343 | if err != nil { 344 | return nil, err 345 | } 346 | if u.Scheme == "" { 347 | return ioutil.ReadFile(path) 348 | } 349 | resp, err := http.Get(path) 350 | if err != nil { 351 | return nil, err 352 | } 353 | defer resp.Body.Close() 354 | return ioutil.ReadAll(resp.Body) 355 | } 356 | -------------------------------------------------------------------------------- /qapi-schema/README.md: -------------------------------------------------------------------------------- 1 | # qapi-schema 2 | 3 | Package qapi-schema is a fully compliant[^1] QAPI[^2] schema language parser. 4 | The QAPI schema language looks approximately like JSON, but it differs 5 | slightly in many ways, which can confuse a regular JSON parser. 6 | 7 | ## Usage 8 | 9 | If you want to parse QAPI schema from your Go code, all you have to do 10 | is called `qapischema.Parse`: 11 | 12 | ```go 13 | input := `{ 'struct': 'DiskThing', 14 | 'data': { 15 | 'diskname': { 16 | 'type':'str', 17 | 'if':'DISKS_HAVE_NAMES' } } }` 18 | schema, _ := qapischema.Parse(input) 19 | ``` 20 | 21 | The above code snippet would produce a `*qapischema.Tree` that looks like this: 22 | 23 | ```txt 24 | &qapischema.Tree{ 25 | Node: qapischema.Root{ }, 26 | Children: []*qapischema.Tree{ 27 | { 28 | Node: &qapischema.Struct{ 29 | Name: "DiskThing", 30 | Members: []qapischema.Member{ 31 | { 32 | Name: "diskname", 33 | Type: qapischema.TypeRef{ 34 | Type: "str", 35 | }, 36 | If: &qapischema.Cond{ 37 | If: &qapischema.CondIf("DISKS_HAVE_NAMES"), 38 | }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | } 45 | ``` 46 | 47 | Once the QAPI has been parsed, you can walk the `*qapischema.Tree` and do 48 | whatever it is that you need to do. 49 | 50 | The `Node` field in `*qapischema.Tree` is an interface type, and so a type 51 | assertion is required to identify and access the QAPI-type-specific data 52 | in the node you're visiting within the tree. 53 | 54 | ```go 55 | func visit(tree *qapischema.Tree) { 56 | switch data := tree.Node.(type) { 57 | // Root node, no data, traverse the subtrees in the .Children field. 58 | case qapischema.Root: 59 | case qapischema.Include: 60 | case qapischema.Pragma: 61 | case *qapischema.Struct: 62 | case *qapischema.Union: 63 | case *qapischema.Event: 64 | case *qapischema.Command: 65 | case *qapischema.Alternate: 66 | } 67 | 68 | // Process the rest of the document 69 | for _, t := range tree.Children { 70 | visit(t) 71 | } 72 | } 73 | 74 | func main() { 75 | tree, _ := qapischema.Parse(input) 76 | 77 | visit(tree) 78 | } 79 | ``` 80 | 81 | ## Reporting defects 82 | 83 | There is a lot of room for improvement with how this parser emits diagnostic 84 | information. That is, it doesn't emit any at all. The parser will simply stop 85 | parsing when it's not able to parse something. It won't complain, it will just 86 | stop. 87 | 88 | So, when it comes to identifying which part of the document the parser did not 89 | understand, just compare the input schema to the output until you find the 90 | first element in the input schema that is missing from the parse tree. 91 | 92 | There are two utilities included in this module: `qapilex` and `qapiparse`. 93 | 94 | `qapiparse` parses QAPI from its stdin and prints a pretty string representation 95 | of the parse tree to stdout. This can be very helpful for figuring out where the 96 | parser stopped. 97 | 98 | In your bug report, please include the QAPI input that surfaced the failure to 99 | parse. If possible, try to reduce the QAPI input down to a minimal viable 100 | reproducer. 101 | 102 | ## Acknowledgements 103 | 104 | Many thanks to: 105 | 106 | * Thorsten Ball, the author of _Writing an Interpreter in Go_[^3]. The lessons 107 | in that book's chapter on lexing were directly applied to create package 108 | `internal/lex`. 109 | * Jeremy Brown, whose GopherCon 2022 talk[^4] demonstrated simple and elegant 110 | ways to write parser combinators in Go, which directly inspired much of 111 | package `internal/parse`. 112 | 113 | [^1]: At least, it's intended to be fully compliant. If it is not, please 114 | file a bug. 115 | 116 | [^2]: https://qemu.readthedocs.io/en/latest/devel/qapi-code-gen.html#introduction 117 | 118 | [^3]: https://interpreterbook.com/ 119 | 120 | [^4]: [GopherCon 2022: Jeremy Brown - Parsing w/o Code Generators: Parser Combinators in Go with Generics]( 121 | https://www.youtube.com/watch?v=x5p_SJNRB4U) 122 | -------------------------------------------------------------------------------- /qapi-schema/applicator.go: -------------------------------------------------------------------------------- 1 | package qapischema 2 | 3 | type alternateField func(a *Alternate) 4 | type alternativeField func(a *Alternative) 5 | type featureField func(f *Feature) 6 | type branchField func(b *Branch) 7 | type commandField func(c *Command) 8 | type enumField func(e *Enum) 9 | type enumValueField func(e *EnumValue) 10 | type eventField func(e *Event) 11 | type memberField func(m *Member) 12 | type pragmaField func(p *Pragma) 13 | type structField func(s *Struct) 14 | type unionField func(u *Union) 15 | 16 | type eventDataBoxedTrue struct{} 17 | -------------------------------------------------------------------------------- /qapi-schema/cmd/qapilex/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/alecthomas/repr" 9 | 10 | "github.com/digitalocean/go-qemu/qapi-schema/internal/lex" 11 | ) 12 | 13 | func main() { 14 | if err := run(); err != nil { 15 | fmt.Fprintf(os.Stderr, "%v\n", err) 16 | } 17 | } 18 | 19 | func run() error { 20 | input, err := io.ReadAll(os.Stdin) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | tokens := lex.Lex(string(input)) 26 | repr.Println(tokens) 27 | 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /qapi-schema/cmd/qapiparse/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/alecthomas/repr" 9 | 10 | qapischema "github.com/digitalocean/go-qemu/qapi-schema" 11 | ) 12 | 13 | func main() { 14 | if err := run(); err != nil { 15 | fmt.Fprintf(os.Stderr, "parse error: %v\n", err) 16 | } 17 | } 18 | 19 | func run() error { 20 | input, err := io.ReadAll(os.Stdin) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | schema, err := qapischema.Parse(string(input)) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | repr.Println(schema) 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /qapi-schema/grammar_test.go: -------------------------------------------------------------------------------- 1 | package qapischema 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/alecthomas/repr" 8 | 9 | "github.com/digitalocean/go-qemu/qapi-schema/internal/parse" 10 | "github.com/digitalocean/go-qemu/qapi-schema/internal/token" 11 | ) 12 | 13 | func TestTrueParser(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | input []token.Token 17 | want bool 18 | wantErr error 19 | }{ 20 | { 21 | name: "true", 22 | input: []token.Token{ 23 | {Type: token.AlphaNumeric, Literal: "true"}, 24 | }, 25 | want: true, 26 | }, 27 | { 28 | name: "no match", 29 | input: []token.Token{ 30 | {Type: token.AlphaNumeric, Literal: "troo"}, 31 | }, 32 | want: false, 33 | wantErr: parse.ErrNoMatch, 34 | }, 35 | } 36 | 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | got, err := parse.Parse(trueParser, tt.input) 40 | if tt.wantErr != err { 41 | t.Errorf("want err %v, got err %v", tt.wantErr, err) 42 | } 43 | 44 | if tt.want != got { 45 | t.Errorf("want %v, got %v", tt.want, got) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestFalseParser(t *testing.T) { 52 | tests := []struct { 53 | name string 54 | input []token.Token 55 | want bool 56 | wantErr error 57 | }{ 58 | { 59 | name: "false", 60 | input: []token.Token{ 61 | {Type: token.AlphaNumeric, Literal: "false"}, 62 | }, 63 | want: false, 64 | }, 65 | { 66 | name: "no match", 67 | input: []token.Token{ 68 | {Type: token.AlphaNumeric, Literal: "true"}, 69 | }, 70 | want: false, 71 | wantErr: parse.ErrNoMatch, 72 | }, 73 | } 74 | 75 | for _, tt := range tests { 76 | t.Run(tt.name, func(t *testing.T) { 77 | got, err := parse.Parse(falseParser, tt.input) 78 | if tt.wantErr != err { 79 | t.Errorf("want err %v, got err %v", tt.wantErr, err) 80 | } 81 | 82 | if tt.want != got { 83 | t.Errorf("want %v, got %v", tt.want, got) 84 | } 85 | }) 86 | } 87 | } 88 | 89 | func TestBoolParser(t *testing.T) { 90 | tests := []struct { 91 | name string 92 | input []token.Token 93 | want bool 94 | wantErr error 95 | }{ 96 | { 97 | name: "true", 98 | input: []token.Token{{Type: token.AlphaNumeric, Literal: "true"}}, 99 | want: true, 100 | }, 101 | { 102 | name: "false", 103 | input: []token.Token{{Type: token.AlphaNumeric, Literal: "false"}}, 104 | want: false, 105 | }, 106 | { 107 | name: "no match", 108 | input: []token.Token{{Type: token.AlphaNumeric, Literal: "tralse"}}, 109 | want: false, 110 | wantErr: parse.ErrNoMatch, 111 | }, 112 | } 113 | 114 | for _, tt := range tests { 115 | t.Run(tt.name, func(t *testing.T) { 116 | got, err := parse.Parse(boolParser, tt.input) 117 | 118 | if tt.wantErr != err { 119 | t.Errorf("want err %v, got err %v", tt.wantErr, err) 120 | } 121 | 122 | if tt.want != got { 123 | t.Errorf("want %v, got %v", tt.want, got) 124 | } 125 | }) 126 | } 127 | } 128 | 129 | func TestStrParser(t *testing.T) { 130 | tests := []struct { 131 | name string 132 | input []token.Token 133 | want string 134 | wantErr error 135 | }{ 136 | { 137 | name: "basic string", 138 | input: []token.Token{ 139 | {Type: token.SingleQuote, Literal: "'"}, 140 | {Type: token.AlphaNumeric, Literal: "hello"}, 141 | {Type: token.SingleQuote, Literal: "'"}, 142 | }, 143 | want: "hello", 144 | }, 145 | { 146 | name: "incomplete", 147 | input: []token.Token{ 148 | {Type: token.SingleQuote, Literal: "'"}, 149 | {Type: token.AlphaNumeric, Literal: "hello"}, 150 | }, 151 | wantErr: parse.ErrNoMatch, 152 | }, 153 | } 154 | 155 | for _, tt := range tests { 156 | t.Run(tt.name, func(t *testing.T) { 157 | got, err := parse.Parse(strParser, tt.input) 158 | 159 | if tt.wantErr != err { 160 | t.Errorf("want err %v, got err %v", tt.wantErr, err) 161 | } 162 | 163 | if tt.want != got { 164 | t.Errorf("want %q, got %q", tt.want, got) 165 | } 166 | }) 167 | } 168 | } 169 | 170 | func TestAtLeastOneStrParser(t *testing.T) { 171 | tests := []struct { 172 | name string 173 | input []token.Token 174 | want []string 175 | wantErr error 176 | }{ 177 | { 178 | name: "one", 179 | input: []token.Token{ 180 | {Type: token.SingleQuote, Literal: "'"}, 181 | {Type: token.AlphaNumeric, Literal: "hello"}, 182 | {Type: token.SingleQuote, Literal: "'"}, 183 | }, 184 | want: []string{"hello"}, 185 | }, 186 | { 187 | name: "two", 188 | input: []token.Token{ 189 | {Type: token.SingleQuote, Literal: "'"}, 190 | {Type: token.AlphaNumeric, Literal: "hello"}, 191 | {Type: token.SingleQuote, Literal: "'"}, 192 | {Type: token.Comma, Literal: ","}, 193 | {Type: token.SingleQuote, Literal: "'"}, 194 | {Type: token.AlphaNumeric, Literal: "world"}, 195 | {Type: token.SingleQuote, Literal: "'"}, 196 | }, 197 | want: []string{"hello", "world"}, 198 | }, 199 | { 200 | name: "many", 201 | input: []token.Token{ 202 | {Type: token.SingleQuote, Literal: "'"}, 203 | {Type: token.AlphaNumeric, Literal: "hello"}, 204 | {Type: token.SingleQuote, Literal: "'"}, 205 | {Type: token.Comma, Literal: ","}, 206 | {Type: token.SingleQuote, Literal: "'"}, 207 | {Type: token.AlphaNumeric, Literal: "world"}, 208 | {Type: token.SingleQuote, Literal: "'"}, 209 | {Type: token.Comma, Literal: ","}, 210 | {Type: token.SingleQuote, Literal: "'"}, 211 | {Type: token.AlphaNumeric, Literal: "hi"}, 212 | {Type: token.SingleQuote, Literal: "'"}, 213 | }, 214 | want: []string{"hello", "world", "hi"}, 215 | }, 216 | } 217 | 218 | for _, tt := range tests { 219 | t.Run(tt.name, func(t *testing.T) { 220 | got, err := parse.Parse(atLeastOneStrParser, tt.input) 221 | if tt.wantErr != err { 222 | t.Errorf("want err %v, got err %v", tt.wantErr, err) 223 | } 224 | 225 | if !reflect.DeepEqual(tt.want, got) { 226 | t.Errorf("want %v, got %v", tt.want, got) 227 | } 228 | }) 229 | } 230 | } 231 | 232 | func TestStrArrayParser(t *testing.T) { 233 | tests := []struct { 234 | name string 235 | input []token.Token 236 | want []string 237 | wantErr error 238 | }{ 239 | { 240 | name: "one", 241 | input: []token.Token{ 242 | {Type: token.LeftSquare, Literal: "["}, 243 | {Type: token.SingleQuote, Literal: "'"}, 244 | {Type: token.AlphaNumeric, Literal: "hello"}, 245 | {Type: token.SingleQuote, Literal: "'"}, 246 | {Type: token.RightSquare, Literal: "]"}, 247 | }, 248 | want: []string{"hello"}, 249 | }, 250 | { 251 | name: "two", 252 | input: []token.Token{ 253 | {Type: token.LeftSquare, Literal: "["}, 254 | {Type: token.SingleQuote, Literal: "'"}, 255 | {Type: token.AlphaNumeric, Literal: "hello"}, 256 | {Type: token.SingleQuote, Literal: "'"}, 257 | {Type: token.Comma, Literal: ","}, 258 | {Type: token.SingleQuote, Literal: "'"}, 259 | {Type: token.AlphaNumeric, Literal: "goodbye"}, 260 | {Type: token.SingleQuote, Literal: "'"}, 261 | {Type: token.RightSquare, Literal: "]"}, 262 | }, 263 | want: []string{"hello", "goodbye"}, 264 | }, 265 | { 266 | name: "three", 267 | input: []token.Token{ 268 | {Type: token.LeftSquare, Literal: "["}, 269 | {Type: token.SingleQuote, Literal: "'"}, 270 | {Type: token.AlphaNumeric, Literal: "hello"}, 271 | {Type: token.SingleQuote, Literal: "'"}, 272 | {Type: token.Comma, Literal: ","}, 273 | {Type: token.SingleQuote, Literal: "'"}, 274 | {Type: token.AlphaNumeric, Literal: "goodbye"}, 275 | {Type: token.SingleQuote, Literal: "'"}, 276 | {Type: token.Comma, Literal: ","}, 277 | {Type: token.SingleQuote, Literal: "'"}, 278 | {Type: token.AlphaNumeric, Literal: "ok"}, 279 | {Type: token.SingleQuote, Literal: "'"}, 280 | {Type: token.RightSquare, Literal: "]"}, 281 | }, 282 | want: []string{"hello", "goodbye", "ok"}, 283 | }, 284 | } 285 | 286 | for _, tt := range tests { 287 | t.Run(tt.name, func(t *testing.T) { 288 | got, err := parse.Parse(strArrayParser, tt.input) 289 | if tt.wantErr != err { 290 | t.Errorf("want err %v, got err %v", tt.wantErr, err) 291 | } 292 | 293 | if !reflect.DeepEqual(tt.want, got) { 294 | t.Errorf("want %v, got %v", tt.want, got) 295 | } 296 | }) 297 | } 298 | } 299 | 300 | func TestIncludeParser(t *testing.T) { 301 | tests := []struct { 302 | name string 303 | input []token.Token 304 | want string 305 | wantErr error 306 | }{ 307 | { 308 | name: "include foo-bar.json", 309 | input: []token.Token{ 310 | {Type: token.SingleQuote, Literal: "'"}, 311 | {Type: token.AlphaNumeric, Literal: "include"}, 312 | {Type: token.SingleQuote, Literal: "'"}, 313 | {Type: token.Colon, Literal: ":"}, 314 | {Type: token.SingleQuote, Literal: "'"}, 315 | {Type: token.AlphaNumeric, Literal: "foo-bar.json"}, 316 | {Type: token.SingleQuote, Literal: "'"}, 317 | }, 318 | want: "foo-bar.json", 319 | }, 320 | { 321 | name: "no match", 322 | input: []token.Token{ 323 | {Type: token.SingleQuote, Literal: "'"}, 324 | {Type: token.AlphaNumeric, Literal: "import"}, 325 | {Type: token.SingleQuote, Literal: "'"}, 326 | {Type: token.Colon, Literal: ":"}, 327 | {Type: token.SingleQuote, Literal: "'"}, 328 | {Type: token.AlphaNumeric, Literal: "foo-bar.json"}, 329 | {Type: token.SingleQuote, Literal: "'"}, 330 | }, 331 | wantErr: parse.ErrNoMatch, 332 | }, 333 | } 334 | 335 | for _, tt := range tests { 336 | t.Run(tt.name, func(t *testing.T) { 337 | got, err := parse.Parse(includeParser, tt.input) 338 | if tt.wantErr != err { 339 | t.Errorf("want err %v, got err %v", tt.wantErr, err) 340 | } 341 | 342 | if tt.want != got { 343 | t.Errorf("want %q, got %v", tt.want, got) 344 | } 345 | }) 346 | } 347 | } 348 | 349 | func TestIncludeObjParser(t *testing.T) { 350 | tests := []struct { 351 | name string 352 | input []token.Token 353 | want *Tree 354 | wantErr error 355 | }{ 356 | { 357 | name: "include foo-bar.json", 358 | input: []token.Token{ 359 | {Type: token.LeftCurly, Literal: "{"}, 360 | {Type: token.SingleQuote, Literal: "'"}, 361 | {Type: token.AlphaNumeric, Literal: "include"}, 362 | {Type: token.SingleQuote, Literal: "'"}, 363 | {Type: token.Colon, Literal: ":"}, 364 | {Type: token.SingleQuote, Literal: "'"}, 365 | {Type: token.AlphaNumeric, Literal: "foo-bar.json"}, 366 | {Type: token.SingleQuote, Literal: "'"}, 367 | {Type: token.RightCurly, Literal: "}"}, 368 | }, 369 | want: &Tree{Node: Include("foo-bar.json")}, 370 | }, 371 | } 372 | 373 | for _, tt := range tests { 374 | t.Run(tt.name, func(t *testing.T) { 375 | got, err := parse.Parse(includeObjParser, tt.input) 376 | if tt.wantErr != err { 377 | t.Errorf("want err %v, got err %v", tt.wantErr, err) 378 | } 379 | 380 | if !reflect.DeepEqual(tt.want, got) { 381 | t.Errorf("want %s, got %s", repr.String(tt.want), repr.String(got)) 382 | } 383 | }) 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /qapi-schema/internal/lex/lex.go: -------------------------------------------------------------------------------- 1 | package lex 2 | 3 | import ( 4 | "unicode/utf8" 5 | 6 | "github.com/digitalocean/go-qemu/qapi-schema/internal/token" 7 | ) 8 | 9 | // Lex lexes the input document into tokens. 10 | func Lex(input string) []token.Token { 11 | lexer := newLexer(input) 12 | 13 | var tokens []token.Token 14 | for { 15 | t := lexer.NextToken() 16 | tokens = append(tokens, t) 17 | if t.Type == token.EOF { 18 | break 19 | } 20 | } 21 | return tokens 22 | } 23 | 24 | type lexer struct { 25 | input string 26 | position int 27 | readPosition int 28 | ch rune 29 | } 30 | 31 | func newLexer(input string) *lexer { 32 | l := lexer{ 33 | input: input, 34 | } 35 | 36 | l.readRune() 37 | return &l 38 | } 39 | 40 | func (l *lexer) NextToken() token.Token { 41 | var tok token.Token 42 | 43 | l.ignoreCommentsAndWhitespace() 44 | 45 | switch l.ch { 46 | case '{': 47 | tok = token.Token{Type: token.LeftCurly, Literal: "{"} 48 | case '}': 49 | tok = token.Token{Type: token.RightCurly, Literal: "}"} 50 | case '[': 51 | tok = token.Token{Type: token.LeftSquare, Literal: "["} 52 | case ']': 53 | tok = token.Token{Type: token.RightSquare, Literal: "]"} 54 | case ',': 55 | tok = token.Token{Type: token.Comma, Literal: ","} 56 | case '\'': 57 | tok = token.Token{Type: token.SingleQuote, Literal: "'"} 58 | case ':': 59 | tok = token.Token{Type: token.Colon, Literal: ":"} 60 | case 0: 61 | tok = token.Token{Type: token.EOF, Literal: ""} 62 | default: 63 | if isAlphaNumeric(l.ch) { 64 | tok.Literal = l.readAlphaNumeric() 65 | tok.Type = token.AlphaNumeric 66 | return tok 67 | } 68 | } 69 | 70 | l.readRune() 71 | return tok 72 | } 73 | 74 | func (l *lexer) readRune() { 75 | if l.readPosition >= len(l.input) { 76 | l.ch = 0 77 | return 78 | } 79 | 80 | ch, size := utf8.DecodeRuneInString(l.input[l.readPosition:]) 81 | l.ch = ch 82 | l.position = l.readPosition 83 | l.readPosition += size 84 | 85 | } 86 | 87 | func (l *lexer) readAlphaNumeric() string { 88 | position := l.position 89 | for isAlphaNumeric(l.ch) { 90 | l.readRune() 91 | } 92 | return l.input[position:l.position] 93 | } 94 | 95 | func (l *lexer) ignoreCommentsAndWhitespace() { 96 | for { 97 | switch { 98 | case l.ch == '#': 99 | l.ignoreComment() 100 | case isWhitespace(l.ch): 101 | l.ignoreWhitespace() 102 | default: 103 | return 104 | } 105 | } 106 | } 107 | 108 | var endOfLine = []rune{'\n', '\r'} 109 | 110 | func (l *lexer) ignoreComment() { 111 | for !containsRune(endOfLine, l.ch) { 112 | l.readRune() 113 | } 114 | } 115 | 116 | func (l *lexer) ignoreWhitespace() { 117 | for isWhitespace(l.ch) { 118 | l.readRune() 119 | } 120 | } 121 | 122 | var whitespace = []rune{' ', '\n', '\t', '\r'} 123 | 124 | func isWhitespace(r rune) bool { 125 | return containsRune(whitespace, r) 126 | } 127 | 128 | var additionAlphaNumeric = []rune{'_', '-', '.', '/', '*'} 129 | 130 | func isAlphaNumeric(r rune) bool { 131 | return r >= '0' && r <= '9' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || containsRune(additionAlphaNumeric, r) 132 | } 133 | 134 | func containsRune(haystack []rune, needle rune) bool { 135 | for _, r := range haystack { 136 | if r == needle { 137 | return true 138 | } 139 | } 140 | return false 141 | } 142 | -------------------------------------------------------------------------------- /qapi-schema/internal/lex/lex_test.go: -------------------------------------------------------------------------------- 1 | package lex 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/digitalocean/go-qemu/qapi-schema/internal/token" 7 | ) 8 | 9 | func TestLex(t *testing.T) { 10 | input := `## 11 | # this is a comment 12 | ## 1. hello, world 13 | 14 | { 'include': 'block-job.json' } 15 | 16 | { 'enum': 'arch', 17 | 'data': ['x86', 'x86_64', 'arm64'] }` 18 | 19 | wantTokens := []token.Token{ 20 | {Type: token.LeftCurly, Literal: "{"}, 21 | {Type: token.SingleQuote, Literal: "'"}, 22 | {Type: token.AlphaNumeric, Literal: "include"}, 23 | {Type: token.SingleQuote, Literal: "'"}, 24 | {Type: token.Colon, Literal: ":"}, 25 | {Type: token.SingleQuote, Literal: "'"}, 26 | {Type: token.AlphaNumeric, Literal: "block-job.json"}, 27 | {Type: token.SingleQuote, Literal: "'"}, 28 | {Type: token.RightCurly, Literal: "}"}, 29 | {Type: token.LeftCurly, Literal: "{"}, 30 | {Type: token.SingleQuote, Literal: "'"}, 31 | {Type: token.AlphaNumeric, Literal: "enum"}, 32 | {Type: token.SingleQuote, Literal: "'"}, 33 | {Type: token.Colon, Literal: ":"}, 34 | {Type: token.SingleQuote, Literal: "'"}, 35 | {Type: token.AlphaNumeric, Literal: "arch"}, 36 | {Type: token.SingleQuote, Literal: "'"}, 37 | {Type: token.Comma, Literal: ","}, 38 | {Type: token.SingleQuote, Literal: "'"}, 39 | {Type: token.AlphaNumeric, Literal: "data"}, 40 | {Type: token.SingleQuote, Literal: "'"}, 41 | {Type: token.Colon, Literal: ":"}, 42 | {Type: token.LeftSquare, Literal: "["}, 43 | {Type: token.SingleQuote, Literal: "'"}, 44 | {Type: token.AlphaNumeric, Literal: "x86"}, 45 | {Type: token.SingleQuote, Literal: "'"}, 46 | {Type: token.Comma, Literal: ","}, 47 | {Type: token.SingleQuote, Literal: "'"}, 48 | {Type: token.AlphaNumeric, Literal: "x86_64"}, 49 | {Type: token.SingleQuote, Literal: "'"}, 50 | {Type: token.Comma, Literal: ","}, 51 | {Type: token.SingleQuote, Literal: "'"}, 52 | {Type: token.AlphaNumeric, Literal: "arm64"}, 53 | {Type: token.SingleQuote, Literal: "'"}, 54 | {Type: token.RightSquare, Literal: "]"}, 55 | {Type: token.RightCurly, Literal: "}"}, 56 | {Type: token.EOF, Literal: ""}, 57 | } 58 | 59 | lex := newLexer(input) 60 | 61 | for num, tt := range wantTokens { 62 | got := lex.NextToken() 63 | 64 | if tt.Type != got.Type { 65 | t.Errorf("test [%d] wrong token type - want %q, got %q", num, tt.Type, got.Type) 66 | } 67 | 68 | if tt.Literal != got.Literal { 69 | t.Errorf("test [%d] wrong token literal - want %q, got %q", num, tt.Literal, got.Literal) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /qapi-schema/internal/parse/parse.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/digitalocean/go-qemu/qapi-schema/internal/token" 7 | ) 8 | 9 | var ( 10 | // ErrNoMatch indicates parsing failed against the token stream. 11 | ErrNoMatch = errors.New("no match") 12 | 13 | // ErrNonExhaustiveParse indicates parsing completed but there were 14 | // still tokens remaining in the input stream. 15 | ErrNonExhaustiveParse = errors.New("did not consume all tokens") 16 | ) 17 | 18 | // Parse applies the given parser against the input stream. 19 | func Parse[T any](parser Parser[T], input []token.Token) (T, error) { 20 | initial := state{input: input} 21 | val, next, err := parser(initial) 22 | if err != nil { 23 | return val, err 24 | } 25 | 26 | if len(next.remaining()) > 0 { 27 | return val, ErrNonExhaustiveParse 28 | } 29 | 30 | return val, nil 31 | } 32 | 33 | // Parser is a Parser function that can read an input token stream 34 | // and produce the specified type or an error. 35 | type Parser[T any] func(state) (T, state, error) 36 | 37 | // Empty is an empty type to return when a parser successfully parsed 38 | // the input stream and nothing should be returned. 39 | type Empty struct{} 40 | 41 | // Exactly consumes the exact token from the input stream or fails. 42 | func Exactly(tok token.Token) Parser[Empty] { 43 | return func(initial state) (Empty, state, error) { 44 | if len(initial.remaining()) >= 1 { 45 | if initial.remaining()[0] == tok { 46 | return Empty{}, initial.consume(1), nil 47 | } 48 | } 49 | return Empty{}, initial, ErrNoMatch 50 | } 51 | } 52 | 53 | // MatchesToken consumes the token from the input stream if matcher 54 | // returns true. 55 | func MatchesToken(matcher func(token.Token) bool) Parser[token.Token] { 56 | return func(initial state) (token.Token, state, error) { 57 | if len(initial.remaining()) > 0 { 58 | if matcher(initial.remaining()[0]) { 59 | return initial.remaining()[0], initial.consume(1), nil 60 | } 61 | } 62 | return token.Token{}, initial, ErrNoMatch 63 | } 64 | } 65 | 66 | // Map converts one parser to another parser. 67 | func Map[T any, U any](parser Parser[T], mapper func(T) U) Parser[U] { 68 | return func(initial state) (U, state, error) { 69 | val, next, err := parser(initial) 70 | if err != nil { 71 | var zero U 72 | return zero, initial, err 73 | } 74 | return mapper(val), next, nil 75 | } 76 | } 77 | 78 | // OneOf tries a list of parsers and if one of them succeeds, returns 79 | // that parser. 80 | func OneOf[T any](parsers ...Parser[T]) Parser[T] { 81 | return func(initial state) (T, state, error) { 82 | for _, parser := range parsers { 83 | val, next, err := parser(initial) 84 | if err == nil { 85 | return val, next, nil 86 | } 87 | } 88 | 89 | var zero T 90 | return zero, initial, ErrNoMatch 91 | } 92 | } 93 | 94 | // ManyOf attempts to successfully parse 0 or more times. 95 | func ManyOf[T any](parser Parser[T]) Parser[[]T] { 96 | return func(initial state) ([]T, state, error) { 97 | var successes []T 98 | current := initial 99 | for { 100 | val, next, err := parser(current) 101 | if err != nil { 102 | break 103 | } 104 | 105 | successes = append(successes, val) 106 | current = next 107 | } 108 | return successes, current, nil 109 | } 110 | } 111 | 112 | // ConsumeAtLeastOne attempts to parse at least once with the given 113 | // parser and then continues until it fails to parse. 114 | func ConsumeAtLeastOne[T any](parser Parser[T]) Parser[[]T] { 115 | return func(initial state) ([]T, state, error) { 116 | val, next, err := parser(initial) 117 | if err != nil { 118 | return nil, initial, err 119 | } 120 | 121 | current := next 122 | successes := []T{val} 123 | for { 124 | val, next, err = parser(current) 125 | if err != nil { 126 | break 127 | } 128 | 129 | current = next 130 | successes = append(successes, val) 131 | } 132 | 133 | return successes, current, nil 134 | } 135 | } 136 | 137 | // Seq2 is a tuple of two parsed objects. 138 | type Seq2[A any, B any] struct { 139 | First A 140 | Second B 141 | } 142 | 143 | // All2 requires both parsers to succeed and it returns a tuple 144 | // of the parsed objects. 145 | func All2[A any, B any](first Parser[A], second Parser[B]) Parser[Seq2[A, B]] { 146 | return func(initial state) (Seq2[A, B], state, error) { 147 | var val Seq2[A, B] 148 | 149 | v1, next, err := first(initial) 150 | if err != nil { 151 | return val, initial, err 152 | } 153 | 154 | v2, next, err := second(next) 155 | if err != nil { 156 | return val, initial, err 157 | } 158 | 159 | val.First = v1 160 | val.Second = v2 161 | 162 | return val, next, nil 163 | } 164 | } 165 | 166 | // Seq3 is a tuple of three parsed objects. 167 | type Seq3[A any, B any, C any] struct { 168 | First A 169 | Second B 170 | Third C 171 | } 172 | 173 | // All3 requires all three parsers to succeed and it returns a 174 | // tuple of the parsed objects. 175 | func All3[A any, B any, C any](first Parser[A], second Parser[B], third Parser[C]) Parser[Seq3[A, B, C]] { 176 | return func(initial state) (Seq3[A, B, C], state, error) { 177 | var val Seq3[A, B, C] 178 | 179 | v1, next, err := first(initial) 180 | if err != nil { 181 | return val, initial, err 182 | } 183 | 184 | v2, next, err := second(next) 185 | if err != nil { 186 | return val, initial, err 187 | } 188 | 189 | v3, next, err := third(next) 190 | if err != nil { 191 | return val, initial, err 192 | } 193 | 194 | val.First = v1 195 | val.Second = v2 196 | val.Third = v3 197 | 198 | return val, next, nil 199 | } 200 | } 201 | 202 | // Nothing is a parser that always succeeds and doesn't consume input. 203 | func Nothing() Parser[Empty] { 204 | return func(initial state) (Empty, state, error) { 205 | return Empty{}, initial, nil 206 | } 207 | } 208 | 209 | type state struct { 210 | input []token.Token 211 | offset int 212 | } 213 | 214 | func (s state) remaining() []token.Token { 215 | return s.input[s.offset:] 216 | } 217 | 218 | func (s state) consume(n int) state { 219 | s.offset += n 220 | return s 221 | } 222 | -------------------------------------------------------------------------------- /qapi-schema/internal/token/token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | // Type describes the type of token. 4 | type Type string 5 | 6 | // Types of tokens. 7 | const ( 8 | Illegal = "illegal" 9 | EOF = "EOF" 10 | 11 | LeftCurly = "{" 12 | RightCurly = "}" 13 | LeftSquare = "[" 14 | RightSquare = "]" 15 | Comma = "," 16 | SingleQuote = "'" 17 | Colon = ":" 18 | AlphaNumeric = "alphanum" 19 | ) 20 | 21 | // Token is a lexed token. 22 | type Token struct { 23 | Type Type 24 | Literal string 25 | } 26 | -------------------------------------------------------------------------------- /qapi-schema/tree.go: -------------------------------------------------------------------------------- 1 | package qapischema 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/digitalocean/go-qemu/qapi-schema/internal/lex" 7 | "github.com/digitalocean/go-qemu/qapi-schema/internal/parse" 8 | ) 9 | 10 | // Parse parses a QAPI input document and returns its parse tree. 11 | func Parse(input string) (*Tree, error) { 12 | tokens := lex.Lex(input) 13 | 14 | schema, err := parse.Parse(schemaParser, tokens) 15 | if errors.Is(err, parse.ErrNonExhaustiveParse) { 16 | err = nil 17 | } 18 | return schema, err 19 | } 20 | 21 | // Node is a marker interface that indentifies the implementing 22 | // concrete type as a QAPI type. 23 | type Node interface { 24 | QAPINode() 25 | } 26 | 27 | // Tree is a QAPI parse tree. 28 | type Tree struct { 29 | Node Node 30 | Children []*Tree 31 | } 32 | 33 | // Root is the root of the QAPI document. On its own, it is 34 | // unremarkable. Client code will want to visit its subtrees. 35 | type Root struct{} 36 | 37 | // QAPINode marks Root as a QAPI node. 38 | func (r Root) QAPINode() {} 39 | 40 | // Include is a QAPI node that refers to another QAPI document 41 | // that contains dependent types. 42 | type Include string 43 | 44 | // QAPINode marks Include as a QAPI node. 45 | func (i Include) QAPINode() {} 46 | 47 | // Pragma is a QAPI node that parameterizes the upstream QEMU 48 | // QAPI code generator. 49 | type Pragma struct { 50 | DocRequired bool 51 | CommandNameExceptions []string 52 | CommandReturnExceptions []string 53 | MemberNameExceptions []string 54 | } 55 | 56 | // QAPINode marks Pragma as a QAPI node. 57 | func (p Pragma) QAPINode() {} 58 | 59 | // Cond expresses the conditions under which the upstream QEMU 60 | // code generator will include a type or field. 61 | type Cond struct { 62 | If *CondIf 63 | All *CondAll 64 | Any *CondAny 65 | Not *CondNot 66 | } 67 | 68 | // CondIf will result in code generation if the specified config 69 | // option is set. 70 | type CondIf string 71 | 72 | // CondNot will result in code generation if the specified config 73 | // option is not set. 74 | type CondNot string 75 | 76 | // CondAll will result in code generation if all of the specified 77 | // config options are set. 78 | type CondAll []string 79 | 80 | // CondAny will result in code generation if any of the specified 81 | // config options are set. 82 | type CondAny []string 83 | 84 | // Feature specifies a QAPI document feature. 85 | type Feature struct { 86 | Name string 87 | Cond Cond 88 | } 89 | 90 | // TypeRef refers to a QAPI type or an array of a QAPI type. 91 | // If it is an array type, then TypeArray will be set and Type 92 | // will be the empty string and vice versa. 93 | type TypeRef struct { 94 | Type string 95 | TypeArray TypeRefArray 96 | } 97 | 98 | // TypeRefArray is a stronger Go type for expressing a string 99 | // that is meant to represent an array of a QAPI type. 100 | type TypeRefArray string 101 | 102 | // Enum is a QAPI type whose value is only one of many defined 103 | // variants. 104 | type Enum struct { 105 | Name string 106 | Values []EnumValue 107 | Prefix string 108 | If *Cond 109 | Features []Feature 110 | } 111 | 112 | // QAPINode marks Enum as a QAPI node. 113 | func (e *Enum) QAPINode() {} 114 | 115 | // EnumValue is one possible variant for a QAPI enum. 116 | type EnumValue struct { 117 | Value string 118 | Cond *Cond 119 | Features []Feature 120 | } 121 | 122 | // Struct is a QAPI type that is a combined record of many 123 | // other types. 124 | type Struct struct { 125 | Name string 126 | Members []Member 127 | Base string 128 | If *Cond 129 | Features []Feature 130 | } 131 | 132 | // QAPINode marks Struct as a QAPI node. 133 | func (s *Struct) QAPINode() {} 134 | 135 | // Member is a field type for a QAPI object. 136 | type Member struct { 137 | Name string 138 | Type TypeRef 139 | If *Cond 140 | Features []Feature 141 | Optional bool 142 | } 143 | 144 | // Union is a QAPI type that is like a sum-type enum. 145 | type Union struct { 146 | Name string 147 | Base UnionBase 148 | Discriminator string 149 | Branches []Branch 150 | If *Cond 151 | Features []Feature 152 | } 153 | 154 | // QAPINode marks Union as a QAPI node. 155 | func (u *Union) QAPINode() {} 156 | 157 | // UnionBase is the base object of the union. 158 | type UnionBase struct { 159 | Name string 160 | Members []Member 161 | } 162 | 163 | // Branch describes one of the active variants for the Union type. 164 | type Branch struct { 165 | Name string 166 | Type TypeRef 167 | If *Cond 168 | } 169 | 170 | // Event is a QAPI type that describes something that has happened. 171 | type Event struct { 172 | Name string 173 | Data EventData 174 | If *Cond 175 | Features []Feature 176 | } 177 | 178 | // QAPINode marks Event as a QAPI node. 179 | func (e *Event) QAPINode() {} 180 | 181 | // EventData contains an Event's data. 182 | type EventData struct { 183 | Boxed bool 184 | Selector EventDataSelector 185 | } 186 | 187 | // EventDataSelector contains the name of the event's boxed type 188 | // or its own fields that comprise the event. 189 | type EventDataSelector struct { 190 | Ref string 191 | Embed *EventDataUnboxed 192 | } 193 | 194 | // EventDataUnboxed contains the event's own fields, rather than 195 | // embedding another type's fields as its fields. 196 | type EventDataUnboxed struct { 197 | Type string 198 | Members []Member 199 | } 200 | 201 | // Command is a QAPI type that describes a QMP command that can be 202 | // sent to the QEMU process. 203 | type Command struct { 204 | Name string 205 | Data CommandData 206 | Returns *TypeRef 207 | SuccessResponse *bool 208 | Gen *bool 209 | AllowOOB *bool 210 | AllowPreconfig *bool 211 | Coroutine *bool 212 | Boxed *bool 213 | If *Cond 214 | Features []Feature 215 | } 216 | 217 | // QAPINode marks Command as a QAPI node. 218 | func (c *Command) QAPINode() {} 219 | 220 | // CommandData contains the Command's fields. 221 | type CommandData struct { 222 | Embed *CommandDataEmbed 223 | Ref *CommandDataRef 224 | } 225 | 226 | // CommandDataRef refers to another QAPI type that should be 227 | // used as the argument for this Command. 228 | type CommandDataRef string 229 | 230 | // CommandDataEmbed refers to the fields to embed in the 231 | // Command. 232 | type CommandDataEmbed struct { 233 | Members []Member 234 | } 235 | 236 | // Alternate is a QAPI type that describes what types can 237 | // be suitable alternates. 238 | type Alternate struct { 239 | Name string 240 | Data []Alternative 241 | If *Cond 242 | Features []Feature 243 | } 244 | 245 | // QAPINode marks Alternate as a QAPI node. 246 | func (a *Alternate) QAPINode() {} 247 | 248 | // Alternative is one such alternative that could be contained 249 | // in an Alternate. 250 | type Alternative struct { 251 | Name string 252 | Type TypeRef 253 | If *Cond 254 | } 255 | -------------------------------------------------------------------------------- /qemu/block.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package qemu 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "io" 21 | "path/filepath" 22 | "time" 23 | 24 | "github.com/digitalocean/go-qemu/qmp" 25 | ) 26 | 27 | const ( 28 | blockJobCompleted = "BLOCK_JOB_COMPLETED" 29 | blockJobError = "BLOCK_JOB_ERROR" 30 | blockJobReady = "BLOCK_JOB_READY" 31 | ) 32 | 33 | // BlockDevice represents a QEMU block device. 34 | type BlockDevice struct { 35 | Device string `json:"device"` 36 | Inserted struct { 37 | BackingFile string `json:"backing_file"` 38 | BackingFileDepth int `json:"backing_file_depth"` 39 | BPS int `json:"bps"` 40 | BPSRead int `json:"bps_rd"` 41 | BPSWrite int `json:"bps_wr"` 42 | Cache struct { 43 | Direct bool `json:"direct"` 44 | NoFlush bool `json:"no-flush"` 45 | Writeback bool `json:"writeback"` 46 | } `json:"cache"` 47 | DetectZeroes string `json:"detect_zeroes"` 48 | Driver string `json:"drv"` 49 | Encrypted bool `json:"encrypted"` 50 | EncryptionKeyMissing bool `json:"encryption_key_missing"` 51 | File string `json:"file"` 52 | Image Image `json:"image"` 53 | IOPs int `json:"iops"` 54 | IOPsRead int `json:"iops_rd"` 55 | IOPsWrite int `json:"iops_wr"` 56 | NodeName string `json:"node-name"` 57 | ReadOnly bool `json:"ro"` 58 | WriteThreshold int `json:"write_threshold"` 59 | } `json:"inserted"` 60 | IOStatus string `json:"io-status"` 61 | Locked bool `json:"locked"` 62 | Removable bool `json:"removable"` 63 | Type string `json:"type"` 64 | } 65 | 66 | // BlockJob represents a QEMU blockjob. 67 | type BlockJob struct { 68 | Busy bool `json:"busy"` 69 | Device string `json:"device"` 70 | IOStatus string `json:"io-status"` 71 | Len int `json:"len"` 72 | Offset int `json:"offset"` 73 | Paused bool `json:"paused"` 74 | Ready bool `json:"ready"` 75 | Speed int `json:"speed"` 76 | Type string `json:"type"` 77 | } 78 | 79 | // Image represents a BlockDevice backing image. 80 | type Image struct { 81 | ActualSize uint64 `json:"actual-size"` 82 | BackingFilename string `json:"backing-filename"` 83 | BackingFilenameFormat string `json:"backing-filename-format"` 84 | BackingImage struct { 85 | ActualSize uint64 `json:"actual-size"` 86 | Dirty bool `json:"dirty-flag"` 87 | Filename string `json:"filename"` 88 | Format string `json:"format"` 89 | VirtualSize uint64 `json:"virtual-size"` 90 | } `json:"backing-image"` 91 | ClusterSize int `json:"cluster-size"` 92 | Dirty bool `json:"dirty-flag"` 93 | Filename string `json:"filename"` 94 | Format string `json:"format"` 95 | FormatSpecific struct { 96 | Data struct { 97 | Compat string `json:"compat"` 98 | Corrupt bool `json:"corrupt"` 99 | LazyRefcounts bool `json:"lazy-refcounts"` 100 | RefcountBits int `json:"refcount-bits"` 101 | } `json:"data"` 102 | Type string `json:"type"` 103 | } `json:"format-specific"` 104 | VirtualSize uint64 `json:"virtual-size"` 105 | } 106 | 107 | // BlockStats represents QEMU block device statistics. 108 | type BlockStats struct { 109 | // Device does not actually appear QEMU's output, but is added 110 | // by this package. 111 | Device string `json:"-"` 112 | 113 | AccountFailed bool `json:"account_failed"` 114 | AccountInvalid bool `json:"account_invalid"` 115 | FailedFlushOperations int `json:"failed_flush_operations"` 116 | FailedReadOperations int `json:"failed_rd_operations"` 117 | FailedWriteOperations int `json:"failed_wr_operations"` 118 | FlushOperations int `json:"flush_operations"` 119 | FlushTotalTimeNanoseconds int64 `json:"flush_total_time_ns"` 120 | IdleTimeNanoseconds int64 `json:"idle_time_ns"` 121 | InvalidFlushOperations int `json:"invalid_flush_operations"` 122 | InvalidReadOperations int `json:"invalid_rd_operations"` 123 | InvalidWriteOperations int `json:"invalid_wr_operations"` 124 | ReadBytes uint64 `json:"rd_bytes"` 125 | ReadMerged int `json:"rd_merged"` 126 | ReadOperations int `json:"rd_operations"` 127 | ReadTotalTimeNanoseconds int `json:"rd_total_time_ns"` 128 | WriteBytes uint64 `json:"wr_bytes"` 129 | WriteHighestOffset int64 `json:"wr_highest_offset"` 130 | WriteMerged int `json:"wr_merged"` 131 | WriteOperations int `json:"wr_operations"` 132 | WriteTotalTimeNanoseconds int64 `json:"wr_total_time_ns"` 133 | } 134 | 135 | // Mirror copies a block device to the given destination. 136 | // The destination path specified by dest must be absolute. 137 | func (bd BlockDevice) Mirror(d *Domain, dest string, timeout time.Duration) error { 138 | if !filepath.IsAbs(dest) { 139 | return errors.New("destination path must be absolute") 140 | } 141 | 142 | return waitForSignal(d, blockJobReady, timeout, func() error { 143 | cmd := qmp.Command{ 144 | Execute: "drive-mirror", 145 | Args: map[string]string{ 146 | "device": bd.Device, 147 | "target": dest, 148 | "sync": "full", 149 | "mode": "absolute-paths", 150 | "format": "raw", 151 | }, 152 | } 153 | 154 | _, err := d.Run(cmd) 155 | return err 156 | }) 157 | } 158 | 159 | // Commit synchronously merges an overlay image onto a block device's 160 | // root backing image. Once the operation is complete, CompleteJob() 161 | // must be called to pivot the domain back to the original backing image. 162 | func (bd BlockDevice) Commit(d *Domain, overlay, jobID string, timeout time.Duration) error { 163 | return waitForSignal(d, blockJobReady, timeout, func() error { 164 | cmd := qmp.Command{ 165 | Execute: "block-commit", 166 | Args: map[string]string{ 167 | "device": bd.Inserted.NodeName, 168 | "top": overlay, 169 | "job-id": jobID, 170 | }, 171 | } 172 | 173 | _, err := d.Run(cmd) 174 | return err 175 | }) 176 | } 177 | 178 | // Cancel a running block job. 179 | // For block-mirror operations, this cancels the block job. 180 | func (job BlockJob) Cancel(d *Domain, timeout time.Duration) error { 181 | return waitForSignal(d, blockJobCompleted, timeout, func() error { 182 | cmd := qmp.Command{ 183 | Execute: "block-job-cancel", 184 | Args: map[string]string{ 185 | "device": job.Device, 186 | }, 187 | } 188 | 189 | _, err := d.Run(cmd) 190 | return err 191 | }) 192 | } 193 | 194 | // Complete a running block job. 195 | // For blockcommit backups, this performs the "pivot" back to the original 196 | // backing image. 197 | func (job BlockJob) Complete(d *Domain, timeout time.Duration) error { 198 | return waitForSignal(d, blockJobCompleted, timeout, func() error { 199 | cmd := qmp.Command{ 200 | Execute: "block-job-complete", 201 | Args: map[string]string{ 202 | "device": job.Device, 203 | }, 204 | } 205 | 206 | _, err := d.Run(cmd) 207 | return err 208 | }) 209 | } 210 | 211 | // Snapshot creates a point in time snapshot. 212 | // The disk's image is given a new QCOW2 overlay, leaving the underlying image 213 | // in a state that is considered safe for copying. 214 | func (bd BlockDevice) Snapshot(d *Domain, overlay, nodeName string) error { 215 | cmd := qmp.Command{ 216 | Execute: "blockdev-snapshot-sync", 217 | Args: map[string]string{ 218 | "node-name": bd.Inserted.NodeName, 219 | "snapshot-node-name": nodeName, 220 | "snapshot-file": overlay, 221 | }, 222 | } 223 | 224 | _, err := d.Run(cmd) 225 | return err 226 | } 227 | 228 | // waitForSignal opens a domain's QMP event stream and invokes an input 229 | // closure to send commands which would create results on that event stream. 230 | func waitForSignal(d *Domain, signal string, timeout time.Duration, fn func() error) error { 231 | // "done" signal must always be sent to avoid leaking goroutines. 232 | events, done, err := d.Events() 233 | if err != nil { 234 | return err 235 | } 236 | defer close(done) 237 | 238 | jobErr := make(chan error) 239 | go func() { 240 | jobErr <- waitForJob(events, signal, timeout) 241 | }() 242 | 243 | if err := fn(); err != nil { 244 | return err 245 | } 246 | 247 | return <-jobErr 248 | } 249 | 250 | // waitForJob monitors the domain's QMP event stream, waiting for the provided 251 | // signal. An error is returned when a BLOCK_JOB_ERROR is seen, a timeout 252 | // occurs, or the underlying channel is closed. 253 | func waitForJob(events <-chan qmp.Event, signal string, timeout time.Duration) error { 254 | // Consider events stalled after timeout for X amount of time total, 255 | // rather than X amount of time without an incoming event 256 | stalled := time.After(timeout) 257 | 258 | for { 259 | select { 260 | case e, ok := <-events: 261 | if !ok { 262 | return io.EOF 263 | } 264 | switch e.Event { 265 | case signal: 266 | return nil 267 | case blockJobError: 268 | return fmt.Errorf("block job error: %v", e.Data) 269 | } 270 | case <-stalled: 271 | return errors.New("block job timeout") 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /qemu/block_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package qemu 16 | 17 | import ( 18 | "errors" 19 | "io" 20 | "testing" 21 | 22 | "github.com/digitalocean/go-qemu/qmp" 23 | ) 24 | 25 | func TestCancelJob(t *testing.T) { 26 | const device = "drive-virtio-disk0" 27 | d, done := testDomain(t, func(cmd qmp.Command) (interface{}, error) { 28 | if want, got := "block-job-cancel", cmd.Execute; want != got { 29 | t.Fatalf("unexpected QMP command:\n- want: %q\n- got: %q", 30 | want, got) 31 | } 32 | 33 | args, _ := cmd.Args.(map[string]interface{}) 34 | if want, got := device, args["device"]; want != got { 35 | t.Fatalf("unexpected device:\n- want: %q\n- got: %q", 36 | want, got) 37 | } 38 | 39 | return success{}, nil 40 | }) 41 | defer done() 42 | 43 | job := BlockJob{Device: device} 44 | err := job.Cancel(d, defaultTestTimeout) 45 | if err != nil { 46 | t.Error(err) 47 | } 48 | } 49 | 50 | func TestCommit(t *testing.T) { 51 | const ( 52 | device = "inserted[node-name] before commit" 53 | overlay = "/tmp/foo.img" 54 | jobID = "made-up-job-id-for-the-commit" 55 | ) 56 | d, done := testDomain(t, func(cmd qmp.Command) (interface{}, error) { 57 | if want, got := "block-commit", cmd.Execute; want != got { 58 | t.Fatalf("unexpected QMP command:\n- want: %q\n- got: %q", 59 | want, got) 60 | } 61 | 62 | args, _ := cmd.Args.(map[string]interface{}) 63 | if want, got := device, args["device"]; want != got { 64 | t.Fatalf("unexpected device:\n- want: %q\n- got: %q", 65 | want, got) 66 | } 67 | if want, got := overlay, args["top"]; want != got { 68 | t.Fatalf("unexpected device:\n- want: %q\n- got: %q", 69 | want, got) 70 | } 71 | if want, got := jobID, args["job-id"]; want != got { 72 | t.Fatalf("unexpected job-id:\n- want: %q\n- got: %q", 73 | want, got) 74 | } 75 | 76 | return success{}, nil 77 | }) 78 | defer done() 79 | 80 | disk := BlockDevice{} 81 | disk.Inserted.NodeName = device 82 | err := disk.Commit(d, overlay, jobID, defaultTestTimeout) 83 | if err != nil { 84 | t.Error(err) 85 | } 86 | } 87 | 88 | func TestCommitActiveBlockJob(t *testing.T) { 89 | d, done := testDomain(t, func(_ qmp.Command) (interface{}, error) { 90 | return failure{ 91 | Error: map[string]string{ 92 | "class": "GenericError", 93 | }, 94 | }, errors.New("fail") 95 | }) 96 | defer done() 97 | 98 | disk := BlockDevice{} 99 | err := disk.Commit(d, "/tmp/foo", "job-id", defaultTestTimeout) 100 | if err == nil { 101 | t.Errorf("expected blockcommit with active blockjob to fail") 102 | } 103 | } 104 | 105 | func TestCommitBlockJobError(t *testing.T) { 106 | d, done := testDomain(t, func(_ qmp.Command) (interface{}, error) { 107 | return success{}, nil 108 | }, withEventErrors) 109 | defer done() 110 | 111 | disk := BlockDevice{} 112 | err := disk.Commit(d, "/tmp/foo", "job-id", defaultTestTimeout) 113 | if err == nil { 114 | t.Error("expected block job error to cause failure") 115 | } else if errors.Is(err, io.EOF) { 116 | t.Error("didn't expect the event stream to close") 117 | } 118 | } 119 | 120 | func TestCommitTimeout(t *testing.T) { 121 | d, done := testDomain(t, func(_ qmp.Command) (interface{}, error) { 122 | return success{}, nil 123 | }, withEventTimeout) 124 | defer done() 125 | 126 | disk := BlockDevice{Device: "test"} 127 | err := disk.Commit(d, "/tmp/foo", "job-id", 0) 128 | if err == nil { 129 | t.Error("expected timeout") 130 | } 131 | } 132 | 133 | func TestJobComplete(t *testing.T) { 134 | const device = "drive-virtio-disk0" 135 | d, done := testDomain(t, func(cmd qmp.Command) (interface{}, error) { 136 | if want, got := "block-job-complete", cmd.Execute; want != got { 137 | t.Fatalf("unexpected QMP command:\n- want: %q\n- got: %q", 138 | want, got) 139 | } 140 | 141 | args, _ := cmd.Args.(map[string]interface{}) 142 | if want, got := device, args["device"]; want != got { 143 | t.Fatalf("unexpected device:\n- want: %q\n- got: %q", 144 | want, got) 145 | } 146 | 147 | return success{}, nil 148 | }) 149 | defer done() 150 | 151 | job := BlockJob{Device: device} 152 | err := job.Complete(d, defaultTestTimeout) 153 | if err != nil { 154 | t.Error(err) 155 | } 156 | } 157 | 158 | func TestJobCompleteEventError(t *testing.T) { 159 | d, done := testDomain(t, func(_ qmp.Command) (interface{}, error) { 160 | return success{}, nil 161 | }, withEventErrors) 162 | defer done() 163 | 164 | job := BlockJob{Device: "test"} 165 | err := job.Complete(d, defaultTestTimeout) 166 | if err == nil { 167 | t.Error("expected block job error to cause failure") 168 | } 169 | } 170 | 171 | func TestJobCompleteTimeout(t *testing.T) { 172 | d, done := testDomain(t, func(_ qmp.Command) (interface{}, error) { 173 | return success{}, nil 174 | }, withEventTimeout) 175 | defer done() 176 | 177 | job := BlockJob{Device: "test"} 178 | err := job.Complete(d, 0) 179 | if err == nil { 180 | t.Error("expected timeout") 181 | } 182 | } 183 | 184 | func TestMirror(t *testing.T) { 185 | const ( 186 | device = "drive-virtio-disk0" 187 | dest = "/tmp/foo.img" 188 | ) 189 | d, done := testDomain(t, func(cmd qmp.Command) (interface{}, error) { 190 | if want, got := "drive-mirror", cmd.Execute; want != got { 191 | t.Fatalf("unexpected QMP command:\n- want: %q\n- got: %q", 192 | want, got) 193 | } 194 | 195 | args, _ := cmd.Args.(map[string]interface{}) 196 | if want, got := device, args["device"]; want != got { 197 | t.Fatalf("unexpected device:\n- want: %q\n- got: %q", 198 | want, got) 199 | } 200 | 201 | if want, got := dest, args["target"]; want != got { 202 | t.Fatalf("unexpected target:\n- want: %q\n- got: %q", 203 | want, got) 204 | } 205 | 206 | return success{}, nil 207 | }) 208 | defer done() 209 | 210 | disk := BlockDevice{Device: device} 211 | err := disk.Mirror(d, dest, defaultTestTimeout) 212 | if err != nil { 213 | t.Error(err) 214 | } 215 | } 216 | 217 | func TestMirrorRelativePath(t *testing.T) { 218 | const ( 219 | device = "drive-virtio-disk0" 220 | dest = "relative-path.img" 221 | ) 222 | d, done := testDomain(t, func(_ qmp.Command) (interface{}, error) { 223 | return success{}, nil 224 | }) 225 | defer done() 226 | 227 | disk := BlockDevice{Device: device} 228 | err := disk.Mirror(d, dest, defaultTestTimeout) 229 | if err == nil { 230 | t.Errorf("expected relative path %q to fail", dest) 231 | } 232 | } 233 | 234 | func TestSnapshot(t *testing.T) { 235 | const ( 236 | device = "drive-virtio-disk0" 237 | overlay = "/tmp/foo.img" 238 | nodeName = "my-node" 239 | ) 240 | d, done := testDomain(t, func(cmd qmp.Command) (interface{}, error) { 241 | if want, got := "blockdev-snapshot-sync", cmd.Execute; want != got { 242 | t.Fatalf("unexpected QMP command:\n- want: %q\n- got: %q", 243 | want, got) 244 | } 245 | 246 | args, _ := cmd.Args.(map[string]interface{}) 247 | if want, got := device, args["node-name"]; want != got { 248 | t.Fatalf("unexpected device:\n- want: %q\n- got: %q", 249 | want, got) 250 | } 251 | 252 | if want, got := overlay, args["snapshot-file"]; want != got { 253 | t.Fatalf("unexpected target:\n- want: %q\n- got: %q", 254 | want, got) 255 | } 256 | 257 | if want, got := nodeName, args["snapshot-node-name"]; want != got { 258 | t.Fatalf("unexpected target:\n- want: %q\n- got: %q", 259 | want, got) 260 | } 261 | 262 | return success{}, nil 263 | }) 264 | defer done() 265 | 266 | disk := BlockDevice{} 267 | disk.Inserted.NodeName = device 268 | err := disk.Snapshot(d, overlay, "my-node") 269 | if err != nil { 270 | t.Error(err) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /qemu/cpu.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package qemu 16 | 17 | // CPU represents a QEMU CPU. 18 | type CPU struct { 19 | CPU int `json:"cpu"` 20 | Current bool `json:"current"` 21 | Halted bool `json:"halted"` 22 | PC int `json:"pc"` 23 | } 24 | -------------------------------------------------------------------------------- /qemu/domain.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package qemu provides an interface for interacting with running QEMU instances. 16 | package qemu 17 | 18 | //go:generate stringer -type=Status -output=string.gen.go 19 | //go:generate ../scripts/prependlicense.sh string.gen.go 20 | 21 | import ( 22 | "context" 23 | "encoding/json" 24 | "errors" 25 | "fmt" 26 | "io" 27 | "os" 28 | "path/filepath" 29 | "strings" 30 | "sync" 31 | "time" 32 | 33 | "github.com/digitalocean/go-qemu/qmp" 34 | "github.com/digitalocean/go-qemu/qmp/raw" 35 | ) 36 | 37 | var ( 38 | // ErrBlockDeviceNotFound is returned given when a block device is not found 39 | ErrBlockDeviceNotFound = errors.New("block device not found") 40 | ) 41 | 42 | // Domain represents a QEMU instance. 43 | type Domain struct { 44 | Name string 45 | m qmp.Monitor 46 | rm *raw.Monitor 47 | cancel context.CancelFunc 48 | listeners struct { 49 | sync.Mutex 50 | value []chan<- qmp.Event 51 | } 52 | 53 | eventsUnsupported bool 54 | 55 | tempFileName func(domainName string, method string) string 56 | } 57 | 58 | // Close cleans up internal resources of a Domain and disconnects the underlying 59 | // qmp.Monitor. Close must be called when done with a Domain to avoid leaking 60 | // resources. 61 | func (d *Domain) Close() error { 62 | d.cancel() 63 | return d.m.Disconnect() 64 | } 65 | 66 | // Commands returns all QMP commands supported by the domain. 67 | func (d *Domain) Commands() ([]string, error) { 68 | commands, err := d.rm.QueryCommands() 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | // flatten response 74 | cmds := make([]string, 0, len(commands)) 75 | for _, c := range commands { 76 | cmds = append(cmds, c.Name) 77 | } 78 | 79 | return cmds, nil 80 | } 81 | 82 | // queryBlockResponse is the structure returned by QEMU in response to 83 | // a query-block QMP command. 84 | type queryBlockResponse struct { 85 | ID string `json:"id"` 86 | Return []BlockDevice `json:"return"` 87 | } 88 | 89 | // BlockDevice searches a domain for the given block device. 90 | // If found, a BlockDevice is returned. If the device is not found, 91 | // the returned error will be ErrBlockDeviceNotFound. 92 | func (d *Domain) BlockDevice(name string) (BlockDevice, error) { 93 | devs, err := d.BlockDevices() 94 | if err != nil { 95 | return BlockDevice{}, err 96 | } 97 | 98 | for _, d := range devs { 99 | if d.Device == name { 100 | return d, nil 101 | } 102 | } 103 | 104 | return BlockDevice{}, ErrBlockDeviceNotFound 105 | } 106 | 107 | // BlockDevices returns a domain's block devices. 108 | func (d *Domain) BlockDevices() ([]BlockDevice, error) { 109 | raw, err := d.Run(qmp.Command{Execute: "query-block"}) 110 | if err != nil { 111 | return []BlockDevice{}, err 112 | } 113 | 114 | response := queryBlockResponse{} 115 | if err = json.Unmarshal(raw, &response); err != nil { 116 | return []BlockDevice{}, err 117 | } 118 | 119 | return response.Return, nil 120 | } 121 | 122 | // BlockJobs returns active block job operations. 123 | func (d *Domain) BlockJobs() ([]BlockJob, error) { 124 | var jobs []BlockJob 125 | raw, err := d.Run(qmp.Command{Execute: "query-block-jobs"}) 126 | if err != nil { 127 | return jobs, err 128 | } 129 | 130 | var response struct { 131 | ID string `json:"id"` 132 | Return []BlockJob `json:"return"` 133 | } 134 | 135 | if err = json.Unmarshal(raw, &response); err != nil { 136 | return jobs, err 137 | } 138 | 139 | return response.Return, nil 140 | } 141 | 142 | // BlockStats returns block device statistics for a domain. 143 | func (d *Domain) BlockStats() ([]BlockStats, error) { 144 | raw, err := d.Run(qmp.Command{Execute: "query-blockstats"}) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | var response struct { 150 | Return []struct { 151 | Device string `json:"device"` 152 | // TODO(mdlayher): figure out what Parent is. 153 | Parent struct { 154 | Stats BlockStats `json:"stats"` 155 | } `json:"parent,omitempty"` 156 | Stats BlockStats `json:"stats"` 157 | } `json:"return"` 158 | } 159 | 160 | if err = json.Unmarshal(raw, &response); err != nil { 161 | return nil, err 162 | } 163 | 164 | stats := make([]BlockStats, 0, len(response.Return)) 165 | for _, s := range response.Return { 166 | // Add device to the stat structure, even though QEMU does 167 | // not place it there 168 | s.Stats.Device = s.Device 169 | stats = append(stats, s.Stats) 170 | } 171 | 172 | return stats, nil 173 | } 174 | 175 | // PCIDevices returns a domain's PCI devices. 176 | func (d *Domain) PCIDevices() ([]PCIDevice, error) { 177 | raw, err := d.Run(qmp.Command{Execute: "query-pci"}) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | var response struct { 183 | Return []struct { 184 | Bus int `json:"bus"` 185 | Devices []PCIDevice `json:"devices"` 186 | } `json:"return"` 187 | } 188 | 189 | if err = json.Unmarshal(raw, &response); err != nil { 190 | return nil, err 191 | } 192 | 193 | // Merge devices from each bus into slice after counting them up 194 | // so only a single allocation is needed 195 | var count int 196 | for i := range response.Return { 197 | count += len(response.Return[i].Devices) 198 | } 199 | 200 | devices := make([]PCIDevice, 0, count) 201 | for _, bus := range response.Return { 202 | devices = append(devices, bus.Devices...) 203 | } 204 | 205 | return devices, nil 206 | } 207 | 208 | // CPUs returns a domain's CPUs. 209 | func (d *Domain) CPUs() ([]CPU, error) { 210 | raw, err := d.Run(qmp.Command{Execute: "query-cpus"}) 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | var response struct { 216 | Return []CPU `json:"return"` 217 | } 218 | 219 | if err = json.Unmarshal(raw, &response); err != nil { 220 | return nil, err 221 | } 222 | 223 | return response.Return, nil 224 | } 225 | 226 | // Run executes the given QMP command against the domain. 227 | // The returned []byte is the raw output from the QMP monitor. 228 | // 229 | // Run should be used with caution, as it allows the execution of 230 | // arbitrary QMP commands against the domain. 231 | func (d *Domain) Run(c qmp.Command) ([]byte, error) { 232 | cmd, err := json.Marshal(c) 233 | if err != nil { 234 | return nil, err 235 | } 236 | 237 | return d.m.Run(cmd) 238 | } 239 | 240 | // ScreenDump captures the domain's screen and creates an output PPM image 241 | // stream. ScreenDump will only work if the Domain resides on a local 242 | // hypervisor; not a remote one connected over SSH or TCP socket. 243 | // 244 | // If needed, a PPM image can be decoded using Go's package image and a 245 | // PPM decoder, such as https://godoc.org/github.com/jbuchbinder/gopnm. 246 | func (d *Domain) ScreenDump() (io.ReadCloser, error) { 247 | // Since QEMU only allows capturing output to a file, we create a 248 | // temporary file and use it for the screendump, providing a stream 249 | // to it on return which can be used with anything that accepts 250 | // io.Reader. 251 | name := d.tempFileName(d.Name, "screendump") 252 | 253 | cmd := qmp.Command{ 254 | Execute: "screendump", 255 | Args: map[string]string{ 256 | "filename": name, 257 | }, 258 | } 259 | if _, err := d.Run(cmd); err != nil { 260 | return nil, err 261 | } 262 | 263 | // Automatically remove temporary file when the Close method 264 | // is called. 265 | return newRemoveFileReadCloser(name) 266 | } 267 | 268 | // Status represents the current status of the domain. 269 | type Status int 270 | 271 | // Status constants which indicate the status of a domain. 272 | const ( 273 | StatusDebug Status = Status(raw.RunStateDebug) 274 | StatusFinishMigrate Status = Status(raw.RunStateFinishMigrate) 275 | StatusGuestPanicked Status = Status(raw.RunStateGuestPanicked) 276 | StatusIOError Status = Status(raw.RunStateIOError) 277 | StatusInMigrate Status = Status(raw.RunStateInmigrate) 278 | StatusInternalError Status = Status(raw.RunStateInternalError) 279 | StatusPaused Status = Status(raw.RunStatePaused) 280 | StatusPostMigrate Status = Status(raw.RunStatePostmigrate) 281 | StatusPreLaunch Status = Status(raw.RunStatePrelaunch) 282 | StatusRestoreVM Status = Status(raw.RunStateRestoreVM) 283 | StatusRunning Status = Status(raw.RunStateRunning) 284 | StatusSaveVM Status = Status(raw.RunStateSaveVM) 285 | StatusShutdown Status = Status(raw.RunStateShutdown) 286 | StatusSuspended Status = Status(raw.RunStateSuspended) 287 | StatusWatchdog Status = Status(raw.RunStateWatchdog) 288 | ) 289 | 290 | // Status returns the current status of the domain. 291 | func (d *Domain) Status() (Status, error) { 292 | status, err := d.rm.QueryStatus() 293 | if err != nil { 294 | // libvirt returns an error if the domain is not running 295 | if strings.Contains(err.Error(), "not running") { 296 | return StatusShutdown, nil 297 | } 298 | 299 | return 0, err 300 | } 301 | 302 | return Status(status.Status), nil 303 | } 304 | 305 | // Supported returns true if the provided command is supported by the domain. 306 | func (d *Domain) Supported(cmd string) (bool, error) { 307 | cmds, err := d.Commands() 308 | if err != nil { 309 | return false, err 310 | } 311 | 312 | for _, c := range cmds { 313 | if c == cmd { 314 | return true, nil 315 | } 316 | } 317 | 318 | return false, nil 319 | } 320 | 321 | // SystemPowerdown sends a system power down event to the domain. 322 | func (d *Domain) SystemPowerdown() error { 323 | _, err := d.Run(qmp.Command{Execute: "system_powerdown"}) 324 | return err 325 | } 326 | 327 | // SystemReset sends a system reset event to the domain. 328 | func (d *Domain) SystemReset() error { 329 | _, err := d.Run(qmp.Command{Execute: "system_reset"}) 330 | return err 331 | } 332 | 333 | // Version returns the domain's QEMU version. 334 | func (d *Domain) Version() (string, error) { 335 | raw, err := d.Run(qmp.Command{Execute: "query-version"}) 336 | if err != nil { 337 | return "", err 338 | } 339 | 340 | var response struct { 341 | ID string `json:"id"` 342 | Return qmp.Version `json:"return"` 343 | } 344 | 345 | if err = json.Unmarshal(raw, &response); err != nil { 346 | return "", err 347 | } 348 | 349 | return response.Return.String(), nil 350 | } 351 | 352 | // PackageVersion returns the domain's QEMU package version, the full build 353 | // information for QEMU. 354 | func (d *Domain) PackageVersion() (string, error) { 355 | vers, err := d.rm.QueryVersion() 356 | if err != nil { 357 | return "", err 358 | } 359 | 360 | return vers.Package, nil 361 | } 362 | 363 | // Events streams QEMU QMP events. Two channels are returned, the first contains 364 | // events emitted by the domain. The second is used to signal completion of 365 | // event processing. It is the responsibility of the caller to always close this 366 | // channel when finished. 367 | func (d *Domain) Events() (chan qmp.Event, chan struct{}, error) { 368 | if d.eventsUnsupported { 369 | return nil, nil, qmp.ErrEventsNotSupported 370 | } 371 | 372 | stream := make(chan qmp.Event) 373 | // The previous expectation was that you write to this channel, not 374 | // close it, so ensure we continue to support this. 375 | done := make(chan struct{}, 1) 376 | 377 | // handle disconnection 378 | go func() { 379 | <-done 380 | // If the caller has indicated they are done, they will 381 | // no longer be reading from the stream, and there needs 382 | // to be something which unblocks the main broadcast 383 | // goroutine for writes to this stream so we can remove 384 | // the stream from the list of listeners. 385 | go func() { 386 | for range stream { 387 | } 388 | }() 389 | d.closeAndRemoveListener(stream) 390 | }() 391 | 392 | d.addListener(stream) 393 | return stream, done, nil 394 | } 395 | 396 | // listenAndServe handles a domain's event broadcast service. 397 | func (d *Domain) listenAndServe(ctx context.Context) error { 398 | stream, err := d.m.Events(ctx) 399 | if err != nil { 400 | // let Event() inform the user events are not supported 401 | if errors.Is(err, qmp.ErrEventsNotSupported) { 402 | d.eventsUnsupported = true 403 | return nil 404 | } 405 | 406 | return err 407 | } 408 | 409 | go func() { 410 | // When we're done broadcasting, ensure all of our listeners 411 | // become aware. 412 | defer d.closeAndRemoveListeners() 413 | for event := range stream { 414 | d.broadcast(event) 415 | } 416 | }() 417 | 418 | return nil 419 | } 420 | 421 | // addListener adds the given stream to the domain's event broadcast. The main 422 | // broadcast goroutine takes ownership of the goroutine's lifetime. 423 | func (d *Domain) addListener(stream chan<- qmp.Event) { 424 | d.listeners.Lock() 425 | defer d.listeners.Unlock() 426 | d.listeners.value = append(d.listeners.value, stream) 427 | } 428 | 429 | // closeAndRemoveListeners closes all listeners and removes them from the list. 430 | func (d *Domain) closeAndRemoveListeners() { 431 | d.listeners.Lock() 432 | defer d.listeners.Unlock() 433 | for _, l := range d.listeners.value { 434 | close(l) 435 | } 436 | d.listeners.value = nil 437 | } 438 | 439 | // closeAndRemoveListener closes the listener and removes it from the domain's 440 | // event broadcast. 441 | func (d *Domain) closeAndRemoveListener(stream chan<- qmp.Event) { 442 | d.listeners.Lock() 443 | defer d.listeners.Unlock() 444 | 445 | listeners := d.listeners.value 446 | for i, client := range listeners { 447 | if client == stream { 448 | close(client) 449 | listeners = append(listeners[:i], listeners[i+1:]...) 450 | } 451 | } 452 | d.listeners.value = listeners 453 | } 454 | 455 | // broadcast sends the provided event to all event listeners. 456 | func (d *Domain) broadcast(event qmp.Event) { 457 | d.listeners.Lock() 458 | defer d.listeners.Unlock() 459 | for _, stream := range d.listeners.value { 460 | stream <- event 461 | } 462 | } 463 | 464 | // NewDomain returns a new QEMU domain identified by the given name. 465 | // QMP communication is handled by the provided monitor socket. 466 | func NewDomain(m qmp.Monitor, name string) (*Domain, error) { 467 | d := &Domain{ 468 | Name: name, 469 | m: m, 470 | rm: raw.NewMonitor(m), 471 | listeners: struct { 472 | sync.Mutex 473 | value []chan<- qmp.Event 474 | }{ 475 | value: []chan<- qmp.Event{}, 476 | }, 477 | 478 | // By default, try to generate decently random file names 479 | // for temporary files. 480 | tempFileName: func(domainName string, method string) string { 481 | return filepath.Join( 482 | os.TempDir(), 483 | fmt.Sprintf("go-qemu-%s-%s-%d", 484 | domainName, 485 | method, 486 | time.Now().UnixNano(), 487 | ), 488 | ) 489 | }, 490 | } 491 | 492 | // start event broadcast 493 | ctx, cancel := context.WithCancel(context.Background()) 494 | d.cancel = cancel 495 | err := d.listenAndServe(ctx) 496 | if err != nil { 497 | cancel() 498 | return nil, err 499 | } 500 | 501 | return d, nil 502 | } 503 | -------------------------------------------------------------------------------- /qemu/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package qemu_test 16 | 17 | import ( 18 | "fmt" 19 | "log" 20 | 21 | "github.com/digitalocean/go-qemu/qemu" 22 | "github.com/digitalocean/go-qemu/qmp" 23 | "github.com/digitalocean/go-qemu/qmp/qmptest" 24 | ) 25 | 26 | // This example demonstrates how to use qemu.NewDomain with a qmp.Monitor to 27 | // perform actions on a Domain. 28 | // 29 | // Typically, these actions would be performed using an actual monitor of type 30 | // qmp.LibvirtRPCMonitor or qmp.SocketMonitor, instead of using 31 | // qmptest.NewMonitor. 32 | func ExampleNewDomain() { 33 | // Use qmptest.NewMonitor to create an "example" qmp.Monitor, that returns 34 | // mock data from another function. 35 | // 36 | // Normally, qmp.NewLibvirtRPCMonitor or qmp.NewSocketMonitor would be used 37 | // here instead. 38 | mon := qmptest.NewMonitor(func(cmd qmp.Command) (interface{}, error) { 39 | return exampleRun(cmd) 40 | }) 41 | 42 | // Monitor must be connected before it can be used. 43 | if err := mon.Connect(); err != nil { 44 | log.Fatalf("failed to connect monitor: %v", err) 45 | } 46 | 47 | // Wrap monitor in a qemu.Domain to perform higher-level management actions. 48 | dom, err := qemu.NewDomain(mon, "example") 49 | if err != nil { 50 | log.Fatalf("failed to create domain: %v", err) 51 | } 52 | 53 | // Return the domain's QEMU version. 54 | version, err := dom.Version() 55 | if err != nil { 56 | log.Fatalf("failed to check domain QEMU version: %v", err) 57 | } 58 | 59 | // List the available QMP commands supported by the domain. 60 | commands, err := dom.Commands() 61 | if err != nil { 62 | log.Fatalf("failed to list domain commands: %v", err) 63 | } 64 | 65 | // Print some information about the domain. 66 | fmt.Printf("%s - QEMU %s\n", dom.Name, version) 67 | fmt.Println(" - commands:") 68 | for _, c := range commands { 69 | fmt.Printf(" - %s\n", c) 70 | } 71 | 72 | // Close the domain to clean up its resources and underlying monitor. 73 | if err := dom.Close(); err != nil { 74 | log.Fatalf("failed to close domain: %v", err) 75 | } 76 | 77 | // Output: 78 | // example - QEMU 2.0.0 79 | // - commands: 80 | // - query-block 81 | // - query-commands 82 | // - query-version 83 | } 84 | 85 | func exampleRun(cmd qmp.Command) (interface{}, error) { 86 | switch cmd.Execute { 87 | case "query-commands": 88 | return runQueryCommands(), nil 89 | case "query-version": 90 | return runQueryVersion(), nil 91 | } 92 | 93 | return nil, fmt.Errorf("unknown command: %q", cmd.Execute) 94 | } 95 | 96 | func runQueryCommands() interface{} { 97 | var response struct { 98 | ID string `json:"id"` 99 | Return []nameWrapper `json:"return"` 100 | } 101 | 102 | commands := []string{ 103 | "query-block", 104 | "query-commands", 105 | "query-version", 106 | } 107 | 108 | response.Return = make([]nameWrapper, 0, len(commands)) 109 | for _, c := range commands { 110 | response.Return = append(response.Return, nameWrapper{ 111 | Name: c, 112 | }) 113 | } 114 | 115 | return response 116 | } 117 | 118 | func runQueryVersion() interface{} { 119 | var response struct { 120 | ID string `json:"id"` 121 | Return qmp.Version `json:"return"` 122 | } 123 | response.Return.QEMU.Major = 2 124 | 125 | return response 126 | } 127 | 128 | type nameWrapper struct { 129 | Name string `json:"name"` 130 | } 131 | -------------------------------------------------------------------------------- /qemu/pci.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package qemu 16 | 17 | // PCIDevice represents a QEMU PCI device. 18 | type PCIDevice struct { 19 | Bus int `json:"bus"` 20 | QdevID string `json:"qdev_id"` 21 | Slot int `json:"slot"` 22 | ClassInfo struct { 23 | Class int `json:"class"` 24 | Desc string `json:"desc"` 25 | } `json:"class_info"` 26 | ID struct { 27 | Device int `json:"device"` 28 | Vendor int `json:"vendor"` 29 | } `json:"id"` 30 | Function int `json:"function"` 31 | Regions []struct { 32 | Prefetch bool `json:"prefetch"` 33 | MemType64 bool `json:"mem_type_64"` 34 | Bar int `json:"bar"` 35 | Size int `json:"size"` 36 | Address int64 `json:"address"` 37 | Type string `json:"type"` 38 | } `json:"regions"` 39 | } 40 | -------------------------------------------------------------------------------- /qemu/string.gen.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by "stringer -type=Status -output=string.gen.go"; DO NOT EDIT. 16 | 17 | package qemu 18 | 19 | import "strconv" 20 | 21 | func _() { 22 | // An "invalid array index" compiler error signifies that the constant values have changed. 23 | // Re-run the stringer command to generate them again. 24 | var x [1]struct{} 25 | _ = x[StatusDebug-0] 26 | _ = x[StatusFinishMigrate-7] 27 | _ = x[StatusGuestPanicked-14] 28 | _ = x[StatusIOError-3] 29 | _ = x[StatusInMigrate-1] 30 | _ = x[StatusInternalError-2] 31 | _ = x[StatusPaused-4] 32 | _ = x[StatusPostMigrate-5] 33 | _ = x[StatusPreLaunch-6] 34 | _ = x[StatusRestoreVM-8] 35 | _ = x[StatusRunning-9] 36 | _ = x[StatusSaveVM-10] 37 | _ = x[StatusShutdown-11] 38 | _ = x[StatusSuspended-12] 39 | _ = x[StatusWatchdog-13] 40 | } 41 | 42 | const _Status_name = "StatusDebugStatusInMigrateStatusInternalErrorStatusIOErrorStatusPausedStatusPostMigrateStatusPreLaunchStatusFinishMigrateStatusRestoreVMStatusRunningStatusSaveVMStatusShutdownStatusSuspendedStatusWatchdogStatusGuestPanicked" 43 | 44 | var _Status_index = [...]uint8{0, 11, 26, 45, 58, 70, 87, 102, 121, 136, 149, 161, 175, 190, 204, 223} 45 | 46 | func (i Status) String() string { 47 | if i < 0 || i >= Status(len(_Status_index)-1) { 48 | return "Status(" + strconv.FormatInt(int64(i), 10) + ")" 49 | } 50 | return _Status_name[_Status_index[i]:_Status_index[i+1]] 51 | } 52 | -------------------------------------------------------------------------------- /qemu/tempfile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package qemu 16 | 17 | import ( 18 | "io" 19 | "os" 20 | ) 21 | 22 | // newRemoveReadFileCloser opens a temporary file in the filesystem with the 23 | // input name and returns an io.ReadCloser which removes the temporary file 24 | // when Close is called. 25 | func newRemoveFileReadCloser(name string) (io.ReadCloser, error) { 26 | f, err := os.Open(name) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return &removeFileReadCloser{File: f}, nil 32 | } 33 | 34 | var _ io.ReadCloser = &removeFileReadCloser{} 35 | 36 | // A removeFileReadCloser is a *os.File wrapper which removes the embedded file 37 | // when its Close method is called. 38 | type removeFileReadCloser struct { 39 | *os.File 40 | } 41 | 42 | // Close closes the file handle and removes the file from the filesystem. 43 | func (c *removeFileReadCloser) Close() error { 44 | if err := c.File.Close(); err != nil { 45 | _ = os.Remove(c.File.Name()) 46 | return err 47 | } 48 | 49 | return os.Remove(c.File.Name()) 50 | } 51 | -------------------------------------------------------------------------------- /qemu/version_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package qemu 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/digitalocean/go-qemu/qmp" 21 | "github.com/digitalocean/go-qemu/qmp/raw" 22 | ) 23 | 24 | func TestVersion(t *testing.T) { 25 | result := qmp.Version{} 26 | result.QEMU.Major = 2 27 | result.QEMU.Minor = 5 28 | result.QEMU.Micro = 0 29 | 30 | d, done := testDomain(t, func(cmd qmp.Command) (interface{}, error) { 31 | if want, got := "query-version", cmd.Execute; want != got { 32 | t.Fatalf("unexpected QMP command:\n- want: %q\n- got: %q", 33 | want, got) 34 | } 35 | 36 | return success{ 37 | Return: result, 38 | }, nil 39 | }) 40 | defer done() 41 | 42 | v, err := d.Version() 43 | if err != nil { 44 | t.Error(err) 45 | } 46 | 47 | expected := "2.5.0" 48 | if v != expected { 49 | t.Errorf("expected version %q, instead got %q", expected, v) 50 | } 51 | } 52 | 53 | func TestPackageVersion(t *testing.T) { 54 | result := raw.VersionInfo{} 55 | result.Qemu.Major = 2 56 | result.Qemu.Minor = 8 57 | result.Qemu.Micro = 0 58 | result.Package = "(Debian 1:2.8+dfsg-3ubuntu2.4)" 59 | 60 | d, done := testDomain(t, func(cmd qmp.Command) (interface{}, error) { 61 | if want, got := "query-version", cmd.Execute; want != got { 62 | t.Fatalf("unexpected QMP command:\n- want: %q\n- got: %q", 63 | want, got) 64 | } 65 | 66 | return success{ 67 | Return: result, 68 | }, nil 69 | }) 70 | defer done() 71 | 72 | v, err := d.PackageVersion() 73 | if err != nil { 74 | t.Error(err) 75 | } 76 | 77 | expected := "(Debian 1:2.8+dfsg-3ubuntu2.4)" 78 | if v != expected { 79 | t.Errorf("expected package version %q, instead got %q", expected, v) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /qmp/README.md: -------------------------------------------------------------------------------- 1 | QMP 2 | === 3 | 4 | Package `qmp` enables interaction with QEMU instances via the QEMU Machine Protocol (QMP). 5 | 6 | ## Available Drivers 7 | 8 | ### Libvirt 9 | 10 | If your environment is managed by Libvirt, QMP interaction must be proxied through the Libvirt daemon. This can be be done through two available drivers: 11 | 12 | #### RPC 13 | 14 | The RPC driver provides a pure Go implementation of Libvirt's RPC protocol. 15 | 16 | ```go 17 | //conn, err := net.DialTimeout("unix", "/var/run/libvirt/libvirt-sock", 2*time.Second) 18 | conn, err := net.DialTimeout("tcp", "192.168.1.1:16509", 2*time.Second) 19 | monitor := libvirtrpc.New("stage-lb-1", conn) 20 | ``` 21 | 22 | #### virsh 23 | 24 | A connection to the monitor socket is provided by proxing requests through the `virsh` executable. 25 | 26 | ```go 27 | monitor, err := qmp.NewLibvirtMonitor("qemu:///system", "stage-lb-1") 28 | ``` 29 | 30 | ### Socket 31 | 32 | If your QEMU instances are not managed by libvirt, direct communication over its UNIX socket is available. 33 | 34 | ```go 35 | monitor, err := qmp.NewSocketMonitor("unix", "/var/lib/qemu/example.monitor", 2*time.Second) 36 | ``` 37 | 38 | ## Examples 39 | 40 | Using the above to establish a new `qmp.Monitor`, the following examples provide a brief overview of QMP usage. 41 | 42 | _error checking omitted for the sake of brevity._ 43 | 44 | ### Command Execution 45 | ```go 46 | type StatusResult struct { 47 | ID string `json:"id"` 48 | Return struct { 49 | Running bool `json:"running"` 50 | Singlestep bool `json:"singlestep"` 51 | Status string `json:"status"` 52 | } `json:"return"` 53 | } 54 | 55 | monitor.Connect() 56 | defer monitor.Disconnect() 57 | 58 | cmd := []byte(`{ "execute": "query-status" }`) 59 | raw, _ := monitor.Run(cmd) 60 | 61 | var result StatusResult 62 | json.Unmarshal(raw, &result) 63 | 64 | fmt.Println(result.Return.Status) 65 | ``` 66 | 67 | ``` 68 | running 69 | ``` 70 | 71 | ### Event Monitor 72 | 73 | ```go 74 | monitor.Connect() 75 | defer monitor.Disconnect() 76 | 77 | stream, _ := monitor.Events() 78 | for e := range stream { 79 | log.Printf("EVENT: %s", e.Event) 80 | } 81 | 82 | ``` 83 | 84 | ``` 85 | $ virsh reboot example 86 | Domain example is being rebooted 87 | ``` 88 | 89 | ``` 90 | EVENT: POWERDOWN 91 | EVENT: SHUTDOWN 92 | EVENT: STOP 93 | EVENT: RESET 94 | EVENT: RESUME 95 | EVENT: RESET 96 | ... 97 | ``` 98 | 99 | ## More information 100 | 101 | * [QEMU QMP Wiki](http://wiki.qemu.org/QMP) 102 | * [QEMU QMP Intro](http://git.qemu.org/?p=qemu.git;a=blob_plain;f=docs/qmp-intro.txt;hb=HEAD) 103 | * [QEMU QMP Events](http://git.qemu.org/?p=qemu.git;a=blob_plain;f=docs/qmp-events.txt;hb=HEAD) 104 | * [QEMU QMP Spec](http://git.qemu.org/?p=qemu.git;a=blob_plain;f=docs/qmp-spec.txt;hb=HEAD) 105 | -------------------------------------------------------------------------------- /qmp/qmp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package qmp enables interaction with QEMU instances 16 | // via the QEMU Machine Protocol (QMP). 17 | package qmp 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | ) 24 | 25 | // ErrEventsNotSupported is returned by Events() if event streams 26 | // are unsupported by either QEMU or libvirt. 27 | var ErrEventsNotSupported = errors.New("event monitor is not supported") 28 | 29 | // Monitor represents a QEMU Machine Protocol socket. 30 | // See: http://wiki.qemu.org/QMP 31 | type Monitor interface { 32 | Connect() error 33 | Disconnect() error 34 | Run(command []byte) (out []byte, err error) 35 | Events(context.Context) (events <-chan Event, err error) 36 | } 37 | 38 | // Command represents a QMP command. 39 | type Command struct { 40 | // Name of the command to run 41 | Execute string `json:"execute"` 42 | 43 | // Optional arguments for the above command. 44 | Args interface{} `json:"arguments,omitempty"` 45 | } 46 | 47 | type response struct { 48 | ID string `json:"id"` 49 | Return interface{} `json:"return,omitempty"` 50 | Error struct { 51 | Class string `json:"class"` 52 | Desc string `json:"desc"` 53 | } `json:"error,omitempty"` 54 | } 55 | 56 | func (r *response) Err() error { 57 | if r.Error.Desc == "" { 58 | return nil 59 | } 60 | 61 | return errors.New(r.Error.Desc) 62 | } 63 | 64 | // Event represents a QEMU QMP event. 65 | // See http://wiki.qemu.org/QMP 66 | type Event struct { 67 | // Event name, e.g., BLOCK_JOB_COMPLETE 68 | Event string `json:"event"` 69 | 70 | // Arbitrary event data 71 | Data map[string]interface{} `json:"data"` 72 | 73 | // Event timestamp, provided by QEMU. 74 | Timestamp struct { 75 | Seconds int64 `json:"seconds"` 76 | Microseconds int64 `json:"microseconds"` 77 | } `json:"timestamp"` 78 | } 79 | 80 | // Version is the QEMU version structure returned when a QMP connection is 81 | // initiated. 82 | type Version struct { 83 | Package string `json:"package"` 84 | QEMU struct { 85 | Major int `json:"major"` 86 | Micro int `json:"micro"` 87 | Minor int `json:"minor"` 88 | } `json:"qemu"` 89 | } 90 | 91 | func (v Version) String() string { 92 | q := v.QEMU 93 | return fmt.Sprintf("%d.%d.%d", q.Major, q.Minor, q.Micro) 94 | } 95 | -------------------------------------------------------------------------------- /qmp/qmptest/qmptest.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package qmptest provides types which assist in testing interactions with 16 | // package qmp. 17 | package qmptest 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | 23 | "github.com/digitalocean/go-qemu/qmp" 24 | ) 25 | 26 | // A RunFunc is a function invoked to process a qmp.Command. Any data 27 | // returned by results is marshaled into JSON and returned to the caller. 28 | type RunFunc func(cmd qmp.Command) (results interface{}, err error) 29 | 30 | // NewMonitor creates a qmp.Monitor that invokes runFunc whenever its Run method 31 | // is called. All other methods are a no-op, and return nil. 32 | func NewMonitor(runFunc RunFunc) qmp.Monitor { 33 | return &monitor{ 34 | fn: runFunc, 35 | } 36 | } 37 | 38 | var _ qmp.Monitor = &monitor{} 39 | 40 | type monitor struct { 41 | fn RunFunc 42 | noopMonitor 43 | } 44 | 45 | func (t *monitor) Run(raw []byte) ([]byte, error) { 46 | var cmd qmp.Command 47 | if err := json.Unmarshal(raw, &cmd); err != nil { 48 | return nil, err 49 | } 50 | 51 | result, err := t.fn(cmd) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return json.Marshal(result) 57 | } 58 | 59 | var _ qmp.Monitor = &noopMonitor{} 60 | 61 | type noopMonitor struct{} 62 | 63 | func (noopMonitor) Connect() error { return nil } 64 | func (noopMonitor) Disconnect() error { return nil } 65 | func (noopMonitor) Run(_ []byte) ([]byte, error) { return nil, nil } 66 | func (noopMonitor) Events(context.Context) (<-chan qmp.Event, error) { return nil, nil } 67 | -------------------------------------------------------------------------------- /qmp/qmptest/qmptest_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package qmptest_test 16 | 17 | import ( 18 | "encoding/json" 19 | "testing" 20 | 21 | "github.com/digitalocean/go-qemu/qmp" 22 | "github.com/digitalocean/go-qemu/qmp/qmptest" 23 | ) 24 | 25 | func TestNewMonitor(t *testing.T) { 26 | tests := []struct { 27 | cmd qmp.Command 28 | fn qmptest.RunFunc 29 | out string 30 | }{ 31 | { 32 | cmd: qmp.Command{ 33 | Execute: "query-version", 34 | }, 35 | fn: func(cmd qmp.Command) (interface{}, error) { 36 | var r struct { 37 | ID string `json:"id"` 38 | Return qmp.Version `json:"return"` 39 | } 40 | r.Return.QEMU.Major = 2 41 | 42 | return r, nil 43 | }, 44 | out: `{"id":"","return":{"package":"","qemu":{"major":2,"micro":0,"minor":0}}}`, 45 | }, 46 | { 47 | cmd: qmp.Command{ 48 | Execute: "query-commands", 49 | }, 50 | fn: func(cmd qmp.Command) (interface{}, error) { 51 | type n struct { 52 | Name string `json:"name"` 53 | } 54 | 55 | var r struct { 56 | ID string `json:"id"` 57 | Return []n `json:"return"` 58 | } 59 | 60 | cmds := []string{ 61 | "query-block", 62 | "query-commands", 63 | "query-version", 64 | } 65 | 66 | r.Return = make([]n, 0, len(cmds)) 67 | for _, c := range cmds { 68 | r.Return = append(r.Return, n{ 69 | Name: c, 70 | }) 71 | } 72 | 73 | return r, nil 74 | }, 75 | out: `{"id":"","return":[{"name":"query-block"},{"name":"query-commands"},{"name":"query-version"}]}`, 76 | }, 77 | } 78 | 79 | for _, tt := range tests { 80 | t.Run(tt.cmd.Execute, func(t *testing.T) { 81 | m := qmptest.NewMonitor(func(cmd qmp.Command) (interface{}, error) { 82 | if want, got := tt.cmd.Execute, cmd.Execute; want != got { 83 | t.Fatalf("unexpected QMP command:\n- want: %q\n- got: %q", 84 | want, got) 85 | } 86 | 87 | return tt.fn(cmd) 88 | }) 89 | 90 | cmdb, err := json.Marshal(tt.cmd) 91 | if err != nil { 92 | t.Fatalf("unexpected error: %v", err) 93 | } 94 | 95 | out, err := m.Run(cmdb) 96 | if err != nil { 97 | t.Fatalf("unexpected error: %v", err) 98 | } 99 | 100 | if want, got := tt.out, string(out); want != got { 101 | t.Fatalf("unexpected output:\n- want: %v\n- got: %v", 102 | want, got) 103 | } 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /qmp/raw/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package raw provides automatically generated QMP types based on the 16 | // QMP schema. 17 | package raw 18 | 19 | //go:generate go run gen-api.go -output autogen.go -templates ../../internal/qmp-gen/templates 20 | -------------------------------------------------------------------------------- /qmp/raw/gen-api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //+build ignore 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | 23 | gen "github.com/digitalocean/go-qemu/internal/qmp-gen" 24 | ) 25 | 26 | func main() { 27 | if err := gen.Generate(); err != nil { 28 | fmt.Println(err) 29 | os.Exit(1) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /qmp/raw/raw.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package raw 16 | 17 | import "github.com/digitalocean/go-qemu/qmp" 18 | 19 | // Monitor exposes the raw QMP command set. 20 | type Monitor struct { 21 | mon qmp.Monitor 22 | } 23 | 24 | // NewMonitor wraps a qmp.Monitor into a raw.Monitor. 25 | func NewMonitor(mon qmp.Monitor) *Monitor { 26 | return &Monitor{mon} 27 | } 28 | -------------------------------------------------------------------------------- /qmp/raw/raw_test.go: -------------------------------------------------------------------------------- 1 | package raw 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestKeyValueQcode(t *testing.T) { 9 | key := QKeyCodeShift 10 | code := KeyValueQcode(key) 11 | json, err := code.MarshalJSON() 12 | expectedJSON := []byte(`{"data":"shift","type":"qcode"}`) 13 | if err != nil { 14 | t.Fatalf("Failed to marshall %v: %s", code, err) 15 | } 16 | if !bytes.Equal(json, expectedJSON) { 17 | t.Fatalf("Expected %s, but output was %s", expectedJSON, json) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /qmp/rpc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package qmp 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "net" 21 | 22 | "github.com/digitalocean/go-libvirt" 23 | ) 24 | 25 | var _ Monitor = &LibvirtRPCMonitor{} 26 | 27 | // A LibvirtRPCMonitor implements LibVirt's remote procedure call protocol. 28 | type LibvirtRPCMonitor struct { 29 | l *libvirt.Libvirt 30 | // Domain name as seen by libvirt, e.g., stage-lb-1 31 | Domain string 32 | } 33 | 34 | // NewLibvirtRPCMonitor configures a new Libvirt RPC Monitor connection. 35 | // The provided domain should be the name of the domain as seen 36 | // by libvirt, e.g., stage-lb-1. 37 | func NewLibvirtRPCMonitor(domain string, conn net.Conn) *LibvirtRPCMonitor { 38 | l := libvirt.New(conn) 39 | 40 | return &LibvirtRPCMonitor{ 41 | l: l, 42 | Domain: domain, 43 | } 44 | } 45 | 46 | // Connect establishes communication with the libvirt server. 47 | // The underlying libvirt socket connection must be previously established. 48 | func (rpc *LibvirtRPCMonitor) Connect() error { 49 | return rpc.l.Connect() 50 | } 51 | 52 | // Disconnect shuts down communication with the libvirt server 53 | // and closes the underlying net.Conn. 54 | func (rpc *LibvirtRPCMonitor) Disconnect() error { 55 | return rpc.l.Disconnect() 56 | } 57 | 58 | // Events streams QEMU QMP Events until the provided context is cancelled. 59 | // If a problem is encountered setting up the event monitor connection 60 | // an error will be returned. Errors encountered during streaming will 61 | // cause the returned event channel to be closed. 62 | func (rpc *LibvirtRPCMonitor) Events(ctx context.Context) (<-chan Event, error) { 63 | events, err := rpc.l.SubscribeQEMUEvents(ctx, rpc.Domain) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | c := make(chan Event) 69 | go func() { 70 | defer close(c) 71 | // process events 72 | for e := range events { 73 | qe, err := qmpEvent(&e) 74 | if err != nil { 75 | break 76 | } 77 | 78 | c <- *qe 79 | } 80 | }() 81 | 82 | return c, nil 83 | } 84 | 85 | // Run executes the given QAPI command against a domain's QEMU instance. 86 | // For a list of available QAPI commands, see: 87 | // http://git.qemu.org/?p=qemu.git;a=blob;f=qapi-schema.json;hb=HEAD 88 | func (rpc *LibvirtRPCMonitor) Run(cmd []byte) ([]byte, error) { 89 | return rpc.l.Run(rpc.Domain, cmd) 90 | } 91 | 92 | // qmpEvent takes a libvirt DomainEvent and returns the QMP equivalent. 93 | func qmpEvent(e *libvirt.DomainEvent) (*Event, error) { 94 | var qe Event 95 | 96 | if e.Details != nil { 97 | if err := json.Unmarshal(e.Details, &qe.Data); err != nil { 98 | return nil, err 99 | } 100 | } 101 | 102 | qe.Event = e.Event 103 | qe.Timestamp.Seconds = int64(e.Seconds) 104 | qe.Timestamp.Microseconds = int64(e.Microseconds) 105 | 106 | return &qe, nil 107 | } 108 | -------------------------------------------------------------------------------- /qmp/rpc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package qmp 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "testing" 21 | 22 | "github.com/digitalocean/go-libvirt" 23 | "github.com/digitalocean/go-libvirt/libvirttest" 24 | ) 25 | 26 | var testEvent = []byte{ 27 | 0x00, 0x00, 0x00, 0xb0, // length 28 | 0x20, 0x00, 0x80, 0x87, // program 29 | 0x00, 0x00, 0x00, 0x01, // version 30 | 0x00, 0x00, 0x00, 0x06, // procedure 31 | 0x00, 0x00, 0x00, 0x01, // type 32 | 0x00, 0x00, 0x00, 0x00, // serial 33 | 0x00, 0x00, 0x00, 0x00, // status 34 | 0x00, 0x00, 0x00, 0x01, // callback id 35 | 36 | // domain name ("test") 37 | 0x00, 0x00, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74, 38 | 39 | // uuid (dc229f87d4de47198cfd2e21c6105b01) 40 | 0xdc, 0x22, 0x9f, 0x87, 0xd4, 0xde, 0x47, 0x19, 41 | 0x8c, 0xfd, 0x2e, 0x21, 0xc6, 0x10, 0x5b, 0x01, 42 | 43 | // domain id (14) 44 | 0x00, 0x00, 0x00, 0x0e, 45 | 46 | // event name (BLOCK_JOB_COMPLETED) 47 | 0x00, 0x00, 0x00, 0x13, 0x42, 0x4c, 0x4f, 0x43, 48 | 0x4b, 0x5f, 0x4a, 0x4f, 0x42, 0x5f, 0x43, 0x4f, 49 | 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x00, 50 | 51 | // seconds (1462211891) 52 | 0x00, 0x00, 0x00, 0x00, 0x57, 0x27, 0x95, 0x33, 53 | 54 | // microseconds (931791) 55 | 0x00, 0x0e, 0x37, 0xcf, 56 | 57 | // event json data 58 | // ({"device":"drive-ide0-0-0","len":0,"offset":0,"speed":0,"type":"commit"}) 59 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x48, 60 | 0x7b, 0x22, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 61 | 0x22, 0x3a, 0x22, 0x64, 0x72, 0x69, 0x76, 0x65, 62 | 0x2d, 0x69, 0x64, 0x65, 0x30, 0x2d, 0x30, 0x2d, 63 | 0x30, 0x22, 0x2c, 0x22, 0x6c, 0x65, 0x6e, 0x22, 64 | 0x3a, 0x30, 0x2c, 0x22, 0x6f, 0x66, 0x66, 0x73, 65 | 0x65, 0x74, 0x22, 0x3a, 0x30, 0x2c, 0x22, 0x73, 66 | 0x70, 0x65, 0x65, 0x64, 0x22, 0x3a, 0x30, 0x2c, 67 | 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x22, 68 | 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x22, 0x7d, 69 | } 70 | 71 | func TestNewLibvirtRPCMonitor(t *testing.T) { 72 | lv := libvirttest.New() 73 | conn, err := lv.Dial() 74 | if err != nil { 75 | t.Error(err) 76 | } 77 | 78 | domain := "test-1" 79 | rpc := NewLibvirtRPCMonitor(domain, conn) 80 | 81 | if rpc.Domain != domain { 82 | t.Errorf("expected domain %q, got %q", domain, rpc.Domain) 83 | } 84 | } 85 | 86 | func TestQMPEvent(t *testing.T) { 87 | e := &libvirt.DomainEvent{ 88 | CallbackID: 1, 89 | Domain: libvirt.Domain{Name: "test"}, 90 | Event: "test", 91 | Seconds: 1, 92 | Microseconds: 1, 93 | Details: []byte(`{"device":"drive-virtio-disk0","len":0,"offset":0,"speed":0,"type":"commit"}`), 94 | } 95 | 96 | qe, err := qmpEvent(e) 97 | if err != nil { 98 | t.Error(err) 99 | } 100 | 101 | if qe.Event != e.Event { 102 | t.Errorf("expected event %q, got %q", e.Event, qe.Event) 103 | } 104 | 105 | if qe.Timestamp.Seconds != int64(e.Seconds) { 106 | t.Errorf("expected seconds to be %q, got %q", e.Seconds, qe.Timestamp.Seconds) 107 | } 108 | 109 | if qe.Timestamp.Microseconds != int64(e.Microseconds) { 110 | t.Errorf("expected microseconds to be %q, got %q", e.Microseconds, qe.Timestamp.Microseconds) 111 | } 112 | 113 | expected := "drive-virtio-disk0" 114 | actual, ok := qe.Data["device"] 115 | if !ok { 116 | t.Error("expected event data 'device'") 117 | } 118 | 119 | if actual != expected { 120 | t.Errorf("expected device %q, got %q", expected, actual) 121 | } 122 | } 123 | 124 | func TestLibvirtRPCMonitorConnect(t *testing.T) { 125 | lv := libvirttest.New() 126 | conn, err := lv.Dial() 127 | if err != nil { 128 | t.Error(err) 129 | } 130 | mon := NewLibvirtRPCMonitor("test", conn) 131 | 132 | err = mon.Connect() 133 | if err != nil { 134 | t.Error(err) 135 | } 136 | } 137 | 138 | func TestLibvirtRPCMonitorDisconnect(t *testing.T) { 139 | lv := libvirttest.New() 140 | conn, err := lv.Dial() 141 | if err != nil { 142 | t.Error(err) 143 | } 144 | mon := NewLibvirtRPCMonitor("test", conn) 145 | 146 | err = mon.Disconnect() 147 | if err != nil { 148 | t.Error(err) 149 | } 150 | } 151 | 152 | func TestLibvirtRPCMonitorRun(t *testing.T) { 153 | lv := libvirttest.New() 154 | conn, err := lv.Dial() 155 | if err != nil { 156 | t.Error(err) 157 | } 158 | mon := NewLibvirtRPCMonitor("test", conn) 159 | 160 | err = mon.Connect() 161 | if err != nil { 162 | t.Error(err) 163 | } 164 | defer mon.Disconnect() 165 | 166 | res, err := mon.Run([]byte(`{"query-version"}`)) 167 | if err != nil { 168 | t.Error(err) 169 | } 170 | 171 | type version struct { 172 | Return struct { 173 | Package string `json:"package"` 174 | QEMU struct { 175 | Major int `json:"major"` 176 | Micro int `json:"micro"` 177 | Minor int `json:"minor"` 178 | } `json:"qemu"` 179 | } `json:"return"` 180 | } 181 | 182 | var v version 183 | err = json.Unmarshal(res, &v) 184 | if err != nil { 185 | t.Error(err) 186 | } 187 | 188 | expected := 2 189 | if v.Return.QEMU.Major != expected { 190 | t.Errorf("expected qemu major version %d, got %d", expected, v.Return.QEMU.Major) 191 | } 192 | } 193 | 194 | func TestLibvirtRPCMonitorEvents(t *testing.T) { 195 | ctx := context.Background() 196 | lv := libvirttest.New() 197 | conn, err := lv.Dial() 198 | if err != nil { 199 | t.Error(err) 200 | } 201 | mon := NewLibvirtRPCMonitor("test", conn) 202 | err = mon.Connect() 203 | if err != nil { 204 | t.Error(err) 205 | } 206 | defer mon.Disconnect() 207 | 208 | done := make(chan struct{}) 209 | 210 | stream, err := mon.Events(ctx) 211 | if err != nil { 212 | t.Error(err) 213 | } 214 | 215 | go func() { 216 | defer close(done) 217 | got := <-stream 218 | 219 | expected := "drive-ide0-0-0" 220 | if got.Data["device"] != expected { 221 | t.Errorf("expected device %q, got %q", expected, got.Data["device"]) 222 | } 223 | }() 224 | 225 | // send an event to the listener goroutine 226 | _, _ = lv.Test.Write(testEvent) 227 | 228 | // wait for completion 229 | <-done 230 | } 231 | -------------------------------------------------------------------------------- /qmp/socket.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package qmp 16 | 17 | import ( 18 | "bufio" 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "io" 23 | "net" 24 | "os" 25 | "sync" 26 | "sync/atomic" 27 | "time" 28 | ) 29 | 30 | // A SocketMonitor is a Monitor which speaks directly to a QEMU Machine Protocol 31 | // (QMP) socket. Communication is performed directly using a QEMU monitor socket, 32 | // typically using a UNIX socket or TCP connection. Multiple connections to the 33 | // same domain are not permitted, and will result in the monitor blocking until 34 | // the existing connection is closed. 35 | type SocketMonitor struct { 36 | // QEMU version reported by a connected monitor socket. 37 | Version *Version 38 | 39 | // QEMU QMP capabiltiies reported by a connected monitor socket. 40 | Capabilities []string 41 | 42 | // Underlying connection 43 | c net.Conn 44 | 45 | // Serialize running command against domain 46 | mu sync.Mutex 47 | 48 | // Send command responses and errors 49 | stream <-chan streamResponse 50 | 51 | // Send domain events to listeners when available 52 | listeners *int32 53 | events <-chan Event 54 | } 55 | 56 | // NewSocketMonitor configures a connection to the provided QEMU monitor socket. 57 | // An error is returned if the socket cannot be successfully dialed, or the 58 | // dial attempt times out. 59 | // 60 | // NewSocketMonitor may dial the QEMU socket using a variety of connection types: 61 | // NewSocketMonitor("unix", "/var/lib/qemu/example.monitor", 2 * time.Second) 62 | // NewSocketMonitor("tcp", "8.8.8.8:4444", 2 * time.Second) 63 | func NewSocketMonitor(network, addr string, timeout time.Duration) (*SocketMonitor, error) { 64 | c, err := net.DialTimeout(network, addr, timeout) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | mon := &SocketMonitor{ 70 | c: c, 71 | listeners: new(int32), 72 | } 73 | 74 | return mon, nil 75 | } 76 | 77 | // Listen creates a new SocketMonitor listening for a single connection to the provided socket file or address. 78 | // An error is returned if unable to listen at the specified file path or port. 79 | // 80 | // Listen will wait for a QEMU socket connection using a variety connection types: 81 | // Listen("unix", "/var/lib/qemu/example.monitor") 82 | // Listen("tcp", "0.0.0.0:4444") 83 | func Listen(network, addr string) (*SocketMonitor, error) { 84 | l, err := net.Listen(network, addr) 85 | if err != nil { 86 | return nil, err 87 | } 88 | c, err := l.Accept() 89 | defer l.Close() 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | mon := &SocketMonitor{ 95 | c: c, 96 | listeners: new(int32), 97 | } 98 | 99 | return mon, nil 100 | } 101 | 102 | // Disconnect closes the QEMU monitor socket connection. 103 | func (mon *SocketMonitor) Disconnect() error { 104 | atomic.StoreInt32(mon.listeners, 0) 105 | err := mon.c.Close() 106 | 107 | if mon.stream != nil { 108 | for range mon.stream { 109 | } 110 | } 111 | 112 | return err 113 | } 114 | 115 | // qmpCapabilities is the command which must be executed to perform the 116 | // QEMU QMP handshake. 117 | const qmpCapabilities = "qmp_capabilities" 118 | 119 | // Connect sets up a QEMU QMP connection by connecting directly to the QEMU 120 | // monitor socket. An error is returned if the capabilities handshake does 121 | // not succeed. 122 | func (mon *SocketMonitor) Connect() error { 123 | enc := json.NewEncoder(mon.c) 124 | dec := json.NewDecoder(mon.c) 125 | 126 | // Check for banner on startup 127 | var ban banner 128 | if err := dec.Decode(&ban); err != nil { 129 | return err 130 | } 131 | mon.Version = &ban.QMP.Version 132 | mon.Capabilities = ban.QMP.Capabilities 133 | 134 | // Issue capabilities handshake 135 | cmd := Command{Execute: qmpCapabilities} 136 | if err := enc.Encode(cmd); err != nil { 137 | return err 138 | } 139 | 140 | // Check for no error on return 141 | var r response 142 | if err := dec.Decode(&r); err != nil { 143 | return err 144 | } 145 | if err := r.Err(); err != nil { 146 | return err 147 | } 148 | 149 | // Initialize socket listener for command responses and asynchronous 150 | // events 151 | events := make(chan Event) 152 | stream := make(chan streamResponse) 153 | go mon.listen(mon.c, events, stream) 154 | 155 | mon.events = events 156 | mon.stream = stream 157 | 158 | return nil 159 | } 160 | 161 | // Events streams QEMU QMP Events. 162 | // Events should only be called once per Socket. If used with a qemu.Domain, 163 | // qemu.Domain.Events should be called to retrieve events instead. 164 | func (mon *SocketMonitor) Events(context.Context) (<-chan Event, error) { 165 | atomic.AddInt32(mon.listeners, 1) 166 | return mon.events, nil 167 | } 168 | 169 | // listen listens for incoming data from a QEMU monitor socket. It determines 170 | // if the data is an asynchronous event or a response to a command, and returns 171 | // the data on the appropriate channel. 172 | func (mon *SocketMonitor) listen(r io.Reader, events chan<- Event, stream chan<- streamResponse) { 173 | defer close(events) 174 | defer close(stream) 175 | 176 | scanner := bufio.NewScanner(r) 177 | for scanner.Scan() { 178 | var e Event 179 | 180 | b := scanner.Bytes() 181 | if err := json.Unmarshal(b, &e); err != nil { 182 | continue 183 | } 184 | 185 | // If data does not have an event type, it must be in response to a command. 186 | if e.Event == "" { 187 | stream <- streamResponse{buf: b} 188 | continue 189 | } 190 | 191 | // If nobody is listening for events, do not bother sending them. 192 | if atomic.LoadInt32(mon.listeners) == 0 { 193 | continue 194 | } 195 | 196 | events <- e 197 | } 198 | 199 | if err := scanner.Err(); err != nil { 200 | stream <- streamResponse{err: err} 201 | } 202 | } 203 | 204 | // Run executes the given QAPI command against a domain's QEMU instance. 205 | // For a list of available QAPI commands, see: 206 | // http://git.qemu.org/?p=qemu.git;a=blob;f=qapi-schema.json;hb=HEAD 207 | func (mon *SocketMonitor) Run(command []byte) ([]byte, error) { 208 | // Just call RunWithFile with no file 209 | return mon.RunWithFile(command, nil) 210 | } 211 | 212 | // RunWithFile behaves like Run but allows for passing a file through out-of-band data. 213 | func (mon *SocketMonitor) RunWithFile(command []byte, file *os.File) ([]byte, error) { 214 | // Only allow a single command to be run at a time to ensure that responses 215 | // to a command cannot be mixed with responses from another command 216 | mon.mu.Lock() 217 | defer mon.mu.Unlock() 218 | 219 | if file == nil { 220 | // Just send a normal command through. 221 | if _, err := mon.c.Write(command); err != nil { 222 | return nil, err 223 | } 224 | } else { 225 | unixConn, ok := mon.c.(*net.UnixConn) 226 | if !ok { 227 | return nil, fmt.Errorf("RunWithFile only works with unix monitor sockets") 228 | } 229 | 230 | oobSupported := false 231 | for _, capability := range mon.Capabilities { 232 | if capability == "oob" { 233 | oobSupported = true 234 | break 235 | } 236 | } 237 | 238 | if !oobSupported { 239 | return nil, fmt.Errorf("The QEMU server doesn't support oob (needed for RunWithFile)") 240 | } 241 | 242 | // Send the command along with the file descriptor. 243 | oob := getUnixRights(file) 244 | if _, _, err := unixConn.WriteMsgUnix(command, oob, nil); err != nil { 245 | return nil, err 246 | } 247 | } 248 | 249 | // Wait for a response or error to our command 250 | res := <-mon.stream 251 | if res.err != nil { 252 | return nil, res.err 253 | } 254 | 255 | // Check for QEMU errors 256 | var r response 257 | if err := json.Unmarshal(res.buf, &r); err != nil { 258 | return nil, err 259 | } 260 | if err := r.Err(); err != nil { 261 | return nil, err 262 | } 263 | 264 | return res.buf, nil 265 | } 266 | 267 | // banner is a wrapper type around a Version. 268 | type banner struct { 269 | QMP struct { 270 | Capabilities []string `json:"capabilities"` 271 | Version Version `json:"version"` 272 | } `json:"QMP"` 273 | } 274 | 275 | // streamResponse is a struct sent over a channel in response to a command. 276 | type streamResponse struct { 277 | buf []byte 278 | err error 279 | } 280 | -------------------------------------------------------------------------------- /qmp/socket_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package qmp 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "net" 24 | "os" 25 | "path/filepath" 26 | "reflect" 27 | "strings" 28 | "sync" 29 | "testing" 30 | ) 31 | 32 | func TestSocketMonitorConnectDisconnect(t *testing.T) { 33 | _, _, done := testSocket(t) 34 | done() 35 | } 36 | 37 | func TestSocketMonitorListen(t *testing.T) { 38 | dir := t.TempDir() 39 | 40 | sock := filepath.Join(dir, "listener.sock") 41 | 42 | // Ensure that goroutine client stops. 43 | var wg sync.WaitGroup 44 | wg.Add(1) 45 | 46 | go func() { 47 | defer wg.Done() 48 | 49 | // Keep waiting for the socket to appear. 50 | for { 51 | if _, err := os.Stat(sock); err == nil { 52 | break 53 | } 54 | } 55 | 56 | // Attempt to dial the socket before the timeout expires. 57 | if _, err := net.Dial("unix", sock); err != nil { 58 | panic(fmt.Sprintf("failed to dial to listener: %v", err)) 59 | } 60 | }() 61 | 62 | if _, err := Listen("unix", sock); err != nil { 63 | t.Fatalf("failed to listen with socket %q: %v", sock, err) 64 | } 65 | 66 | wg.Wait() 67 | } 68 | 69 | func TestSocketMonitorEvents(t *testing.T) { 70 | mon, w, done := testSocket(t) 71 | defer done() 72 | 73 | events, err := mon.Events(context.Background()) 74 | if err != nil { 75 | t.Fatalf("unexpected error: %v", err) 76 | } 77 | 78 | want := []Event{ 79 | {Event: "STOP"}, 80 | {Event: "SHUTDOWN"}, 81 | {Event: "RESET"}, 82 | } 83 | 84 | enc := json.NewEncoder(w) 85 | 86 | for i, e := range want { 87 | if err := enc.Encode(e); err != nil { 88 | t.Fatalf("unexpected error: %v", err) 89 | } 90 | 91 | event := <-events 92 | t.Logf("[%02d] event: %q", i, event.Event) 93 | 94 | if want, got := e, event; !reflect.DeepEqual(want, got) { 95 | t.Fatalf("unexpected event:\n- want: %v\n- got: %v", 96 | want, got) 97 | } 98 | } 99 | } 100 | 101 | func TestSocketMonitor_listenEmptyStream(t *testing.T) { 102 | mon := &SocketMonitor{listeners: new(int32)} 103 | 104 | r := strings.NewReader("") 105 | 106 | events := make(chan Event) 107 | stream := make(chan streamResponse) 108 | 109 | mon.listen(r, events, stream) 110 | 111 | if _, ok := <-events; ok { 112 | t.Fatal("events channel should be closed") 113 | } 114 | 115 | if _, ok := <-stream; ok { 116 | t.Fatal("stream channel should be closed") 117 | } 118 | } 119 | 120 | func TestSocketMonitor_listenScannerErr(t *testing.T) { 121 | mon := &SocketMonitor{listeners: new(int32)} 122 | 123 | errFoo := errors.New("foo") 124 | r := &errReader{err: errFoo} 125 | 126 | events := make(chan Event) 127 | stream := make(chan streamResponse) 128 | 129 | go mon.listen(r, events, stream) 130 | res := <-stream 131 | 132 | if want, got := errFoo, res.err; want != got { 133 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) 134 | } 135 | } 136 | 137 | func TestSocketMonitor_listenInvalidJSON(t *testing.T) { 138 | mon := &SocketMonitor{listeners: new(int32)} 139 | 140 | r := strings.NewReader("") 141 | 142 | events := make(chan Event) 143 | stream := make(chan streamResponse) 144 | 145 | mon.listen(r, events, stream) 146 | 147 | if _, ok := <-stream; ok { 148 | t.Fatal("stream channel should be closed") 149 | } 150 | } 151 | 152 | func TestSocketMonitor_listenStreamResponse(t *testing.T) { 153 | mon := &SocketMonitor{listeners: new(int32)} 154 | 155 | str := `{"foo": "bar"}` 156 | r := strings.NewReader(str) 157 | 158 | events := make(chan Event) 159 | stream := make(chan streamResponse) 160 | 161 | go mon.listen(r, events, stream) 162 | 163 | res := <-stream 164 | if res.err != nil { 165 | t.Fatalf("unexpected error: %v", res.err) 166 | } 167 | 168 | if want, got := str, string(res.buf); want != got { 169 | t.Fatalf("unexpected response:\n- want: %q\n- got: %q", want, got) 170 | } 171 | } 172 | 173 | func TestSocketMonitor_listenEventNoListeners(t *testing.T) { 174 | mon := &SocketMonitor{listeners: new(int32)} 175 | 176 | r := strings.NewReader(`{"event":"STOP"}`) 177 | 178 | events := make(chan Event) 179 | stream := make(chan streamResponse) 180 | 181 | go mon.listen(r, events, stream) 182 | 183 | if _, ok := <-events; ok { 184 | t.Fatal("events channel should be closed") 185 | } 186 | } 187 | 188 | func TestSocketMonitor_listenEventOneListener(t *testing.T) { 189 | l := int32(1) 190 | mon := &SocketMonitor{listeners: &l} 191 | 192 | eventStop := "STOP" 193 | r := strings.NewReader(fmt.Sprintf(`{"event":%q}`, eventStop)) 194 | 195 | events := make(chan Event) 196 | stream := make(chan streamResponse) 197 | 198 | go mon.listen(r, events, stream) 199 | 200 | e := <-events 201 | if want, got := eventStop, e.Event; want != got { 202 | t.Fatalf("unexpected event:\n- want: %q\n- got: %q", want, got) 203 | } 204 | } 205 | 206 | func testSocket(t *testing.T) (*SocketMonitor, io.Writer, func()) { 207 | sc, tc := net.Pipe() 208 | 209 | mon := &SocketMonitor{ 210 | c: sc, 211 | listeners: new(int32), 212 | } 213 | 214 | var wg sync.WaitGroup 215 | wg.Add(1) 216 | 217 | go func() { 218 | defer wg.Done() 219 | 220 | enc := json.NewEncoder(tc) 221 | dec := json.NewDecoder(tc) 222 | 223 | if err := enc.Encode(banner{}); err != nil { 224 | t.Fatalf("unexpected error: %v", err) 225 | } 226 | 227 | var cmd Command 228 | if err := dec.Decode(&cmd); err != nil { 229 | t.Fatalf("unexpected error: %v", err) 230 | } 231 | 232 | if want, got := qmpCapabilities, cmd.Execute; want != got { 233 | t.Fatalf("unexpected capabilities handshake:\n- want: %q\n- got: %q", 234 | want, got) 235 | } 236 | 237 | if err := enc.Encode(response{}); err != nil { 238 | t.Fatalf("unexpected error: %v", err) 239 | } 240 | }() 241 | 242 | if err := mon.Connect(); err != nil { 243 | t.Fatalf("unexpected error: %v", err) 244 | } 245 | 246 | wg.Wait() 247 | 248 | return mon, tc, func() { 249 | if err := mon.Disconnect(); err != nil { 250 | t.Fatalf("unexpected error: %v", err) 251 | } 252 | } 253 | } 254 | 255 | type errReader struct { 256 | err error 257 | } 258 | 259 | func (r *errReader) Read(b []byte) (int, error) { 260 | return 0, r.err 261 | } 262 | -------------------------------------------------------------------------------- /qmp/socket_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build !windows 16 | 17 | package qmp 18 | 19 | import ( 20 | "os" 21 | 22 | "golang.org/x/sys/unix" 23 | ) 24 | 25 | func getUnixRights(file *os.File) []byte { 26 | return unix.UnixRights(int(file.Fd())) 27 | } 28 | -------------------------------------------------------------------------------- /qmp/socket_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build windows 16 | 17 | package qmp 18 | 19 | import ( 20 | "os" 21 | ) 22 | 23 | func getUnixRights(file *os.File) []byte { 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /scripts/codegeneration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Perform code generation and verify that the git repository is still clean, 4 | # meaning that any newly-generated code was added in this commit. 5 | go generate ./... 6 | 7 | GITSTATUS=$(git status --porcelain) 8 | if [ -z "$GITSTATUS" ]; then 9 | exit 0 10 | fi 11 | 12 | echo -e "changes detected, run 'go generate ./...' and commit generated code in these files:\n" 13 | echo "$GITSTATUS" 14 | exit 1 15 | -------------------------------------------------------------------------------- /scripts/golint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Verify that all files are correctly golint'd, with the exception of 4 | # generated code. 5 | EXIT=0 6 | GOLINT=$(golint ./... | grep -v ".gen.go") 7 | 8 | if [[ ! -z $GOLINT ]]; then 9 | echo "$GOLINT" 10 | EXIT=1 11 | fi 12 | 13 | exit $EXIT 14 | -------------------------------------------------------------------------------- /scripts/license.txt: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The go-qemu Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | -------------------------------------------------------------------------------- /scripts/licensecheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Verify that the correct license block is present in all Go source 4 | # files. 5 | EXPECTED=$(cat ./scripts/license.txt) 6 | 7 | # Scan each Go source file for license. 8 | EXIT=0 9 | GOFILES=$(find . -name "*.go") 10 | 11 | for FILE in $GOFILES; do 12 | BLOCK=$(head -n 14 $FILE) 13 | 14 | if [ "$BLOCK" != "$EXPECTED" ]; then 15 | echo "file missing license: $FILE" 16 | EXIT=1 17 | fi 18 | done 19 | 20 | exit $EXIT 21 | -------------------------------------------------------------------------------- /scripts/prependlicense.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # License block to be prepended to file 4 | BASEDIR=$(dirname "$0") 5 | LICENSE=$(cat $BASEDIR/license.txt) 6 | 7 | if [ -z "$1" ]; then 8 | echo "missing filename argument" 9 | echo "usage: prependlicense.sh [filename]" 10 | exit 1 11 | fi 12 | 13 | # Prepend license block to file 14 | echo -e "$LICENSE\n" | cat - $1 > .prependlicense && mv .prependlicense $1 15 | --------------------------------------------------------------------------------