├── .gitignore ├── utils.go ├── examples ├── machine │ └── main.go └── image │ └── main.go ├── LICENSE.md ├── net.go ├── README.md ├── machine.go └── image.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swo 2 | *.swp 3 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package qemu 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func oneLine(in []byte) string { 8 | str := strings.TrimSpace(string(in)) 9 | return strings.Replace(str, "\n", ". ", -1) 10 | } 11 | -------------------------------------------------------------------------------- /examples/machine/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quadrifoglio/go-qemu" 5 | 6 | "fmt" 7 | "log" 8 | ) 9 | 10 | func main() { 11 | img, err := qemu.OpenImage("alpine.qcow2") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | 16 | m := qemu.NewMachine(1, 512) 17 | m.AddDrive(img) 18 | 19 | pid, err := m.Start("x86_64", true) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | fmt.Println("QEMU started on PID", pid) 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | 15 | -------------------------------------------------------------------------------- /examples/image/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/quadrifoglio/go-qemu" 8 | ) 9 | 10 | const ( 11 | GiB = 1073741824 // 1 GiB = 2^30 bytes 12 | ) 13 | 14 | func snapshots() { 15 | img, err := qemu.OpenImage("debian.qcow2") 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | fmt.Println("base image", img.Path, "format", img.Format, "size", img.Size) 21 | 22 | err = img.CreateSnapshot("backup") 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | snaps, err := img.Snapshots() 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | for _, snapshot := range snaps { 33 | fmt.Println(snapshot.Name, snapshot.Date) 34 | } 35 | } 36 | 37 | func create() { 38 | img := qemu.NewImage("vm.qcow2", qemu.ImageFormatQCOW2, 5*GiB) 39 | img.SetBackingFile("debian.qcow2") 40 | 41 | err := img.Create() 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | } 46 | 47 | func main() { 48 | create() 49 | snapshots() 50 | } 51 | -------------------------------------------------------------------------------- /net.go: -------------------------------------------------------------------------------- 1 | package qemu 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type NetDev struct { 8 | Type string // Netdev type (user, tap...) 9 | ID string // Netdev ID 10 | 11 | IfName string // TAP: interface name 12 | MAC string // TAP: Interface hardware address 13 | } 14 | 15 | // NewNetworkDevice creates a QEMU network 16 | // device 17 | func NewNetworkDevice(t, id string) (NetDev, error) { 18 | var netdev NetDev 19 | 20 | if t != "user" && t != "tap" { 21 | return netdev, fmt.Errorf("Unsupported netdev type") 22 | } 23 | if len(id) == 0 { 24 | return netdev, fmt.Errorf("You must specify a netdev ID") 25 | } 26 | 27 | netdev.Type = t 28 | netdev.ID = id 29 | 30 | return netdev, nil 31 | } 32 | 33 | // SetHostInterfaceName sets the host interface name 34 | // for the netdev (if supported by netdev type) 35 | func (n *NetDev) SetHostInterfaceName(name string) { 36 | n.IfName = name 37 | } 38 | 39 | // SetMacAddress sets the mac address of the 40 | // netdev (if supported by netdev type) 41 | func (n *NetDev) SetMacAddress(mac string) { 42 | n.MAC = mac 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-qemu 2 | 3 | Golang interface to the QEMU hypervisor 4 | 5 | ## Installation 6 | 7 | ``` 8 | go get github.com/quadrifoglio/go-qemu 9 | ``` 10 | 11 | You obviously need QEMU to use this tool. 12 | 13 | ## Usage 14 | 15 | ### Create an image 16 | 17 | ```go 18 | img := qemu.NewImage("vm.qcow2", qemu.ImageFormatQCOW2, 5*GiB) 19 | img.SetBackingFile("debian.qcow2") 20 | 21 | err := img.Create() 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | ``` 26 | 27 | ### Open an existing image 28 | 29 | ```go 30 | img, err := qemu.OpenImage("debian.qcow2") 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | fmt.Println("image", img.Path, "format", img.Format, "size", img.Size) 36 | ``` 37 | 38 | ### Snapshots 39 | 40 | ```go 41 | err = img.CreateSnapshot("backup") 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | snaps, err := img.Snapshots() 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | for _, snapshot := range snaps { 52 | fmt.Println(snapshot.Name, snapshot.Date) 53 | } 54 | ``` 55 | 56 | ### Start a virtual machine 57 | 58 | ```go 59 | img, err := qemu.OpenImage("debian.qcow2") 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | 64 | m := qemu.NewMachine(1, 512) // 1 CPU, 512MiB RAM 65 | m.AddDrive(img) 66 | 67 | // x86_64 arch (using qemu-system-x86_64), with kvm 68 | pid, err := m.Start("x86_64", true, func(stderr string) { 69 | log.Println("QEMU stderr:", stderr) 70 | }) 71 | 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | fmt.Println("QEMU started on PID", pid) 77 | ``` 78 | 79 | ## License 80 | 81 | WTFPL (Public Domain) 82 | -------------------------------------------------------------------------------- /machine.go: -------------------------------------------------------------------------------- 1 | package qemu 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "strconv" 9 | "syscall" 10 | "time" 11 | ) 12 | 13 | // Machine represents a QEMU virtual machine 14 | type Machine struct { 15 | Cores int // Number of CPU cores 16 | Memory uint64 // RAM quantity in megabytes 17 | 18 | cd string 19 | display string 20 | vnc string 21 | monitor string 22 | drives []Drive 23 | ifaces []NetDev 24 | 25 | custom [][]string 26 | } 27 | 28 | // Drive represents a machine hard drive 29 | type Drive struct { 30 | Path string // Image file path 31 | Format string // Image format 32 | } 33 | 34 | // NewMachine creates a new virtual machine 35 | // with the specified number of cpu cores and memory 36 | func NewMachine(cores int, memory uint64) Machine { 37 | var machine Machine 38 | machine.Cores = cores 39 | machine.Memory = memory 40 | machine.drives = make([]Drive, 0) 41 | 42 | return machine 43 | } 44 | 45 | // AddCDRom attaches a disk image 46 | // as a CD-ROM on the machine 47 | func (m *Machine) AddCDRom(dev string) { 48 | m.cd = dev 49 | } 50 | 51 | // AddDrive attaches a new hard drive to 52 | // the virtual machine 53 | func (m *Machine) AddDrive(d Drive) { 54 | m.drives = append(m.drives, d) 55 | } 56 | 57 | // AddDriveImage attaches the specified Image to 58 | // the virtual machine 59 | func (m *Machine) AddDriveImage(img Image) { 60 | m.drives = append(m.drives, Drive{img.Path, img.Format}) 61 | } 62 | 63 | // AddNetworkDevice attaches the specified netdev tp 64 | // the virtual machine 65 | func (m *Machine) AddNetworkDevice(netdev NetDev) { 66 | m.ifaces = append(m.ifaces, netdev) 67 | } 68 | 69 | // SetDisplay sets the display mode 70 | // for the virtual machine 71 | func (m *Machine) SetDisplay(mode string) { 72 | m.display = mode 73 | } 74 | 75 | // AddVNC attaches a VNC server to 76 | // the virtual machine, bound to the specified address and port 77 | // If wsPort is not 0, VNC will work over WebSocket on that port 78 | func (m *Machine) AddVNC(addr string, port, wsPort int, passwd bool) { 79 | m.vnc = fmt.Sprintf("%s:%d", addr, port) 80 | 81 | if wsPort > 0 { 82 | m.vnc = fmt.Sprintf("%s,websocket=%d", m.vnc, wsPort) 83 | 84 | if passwd { 85 | m.vnc = fmt.Sprintf("%s,password", m.vnc) 86 | } 87 | } 88 | } 89 | 90 | // AddMonitor redirects the QEMU monitor 91 | // to the specified unix socket file 92 | func (m *Machine) AddMonitorUnix(dev string) { 93 | m.monitor = dev 94 | } 95 | 96 | // AddOption adds a custom command line option 97 | // to the QEMU start command 98 | func (m *Machine) AddOption(opt, val string) { 99 | m.custom = append(m.custom, []string{opt, val}) 100 | } 101 | 102 | // Start stars the machine 103 | // The 'kvm' bool specifies if KVM should be used 104 | // It returns the QEMU process and an error (if any) 105 | func (m *Machine) Start(arch string, kvm bool, stderrCb func(s string)) (*os.Process, error) { 106 | qemu := fmt.Sprintf("qemu-system-%s", arch) 107 | args := []string{"-smp", strconv.Itoa(m.Cores), "-m", strconv.FormatUint(m.Memory, 10)} 108 | 109 | if kvm { 110 | args = append(args, "-enable-kvm") 111 | } 112 | 113 | if len(m.cd) > 0 { 114 | args = append(args, "-cdrom") 115 | args = append(args, m.cd) 116 | } 117 | 118 | for _, drive := range m.drives { 119 | args = append(args, "-drive") 120 | args = append(args, fmt.Sprintf("file=%s,format=%s", drive.Path, drive.Format)) 121 | } 122 | 123 | if len(m.ifaces) == 0 { 124 | args = append(args, "-net") 125 | args = append(args, "none") 126 | } 127 | 128 | for _, iface := range m.ifaces { 129 | s := fmt.Sprintf("%s,id=%s", iface.Type, iface.ID) 130 | if len(iface.IfName) > 0 { 131 | s = fmt.Sprintf("%s,ifname=%s", s, iface.IfName) 132 | } 133 | 134 | args = append(args, "-netdev") 135 | args = append(args, s) 136 | 137 | s = fmt.Sprintf("virtio-net,netdev=%s", iface.ID) 138 | if len(iface.MAC) > 0 { 139 | s = fmt.Sprintf("%s,mac=%s", s, iface.MAC) 140 | } 141 | 142 | args = append(args, "-device") 143 | args = append(args, s) 144 | } 145 | 146 | if len(m.vnc) > 0 { 147 | args = append(args, "-vnc") 148 | args = append(args, m.vnc) 149 | } else if len(m.display) == 0 { 150 | args = append(args, "-display") 151 | args = append(args, "none") 152 | } 153 | 154 | if len(m.display) > 0 { 155 | args = append(args, "-display") 156 | args = append(args, m.display) 157 | } 158 | 159 | if len(m.monitor) > 0 { 160 | args = append(args, "-qmp") 161 | args = append(args, fmt.Sprintf("unix:%s,server,nowait", m.monitor)) 162 | } 163 | 164 | for _, c := range m.custom { 165 | args = append(args, c[0]) 166 | args = append(args, c[1]) 167 | } 168 | 169 | cmd := exec.Command(qemu, args...) 170 | cmd.SysProcAttr = new(syscall.SysProcAttr) 171 | cmd.SysProcAttr.Setsid = true 172 | 173 | stderr, err := cmd.StderrPipe() 174 | if err == nil { 175 | go func() { 176 | s, err := ioutil.ReadAll(stderr) 177 | if err != nil { 178 | return 179 | } 180 | 181 | stderrCb(string(s)) 182 | }() 183 | } 184 | 185 | err = cmd.Start() 186 | if err != nil { 187 | return nil, err 188 | } 189 | 190 | proc := cmd.Process 191 | errc := make(chan error) 192 | 193 | go func() { 194 | err := cmd.Wait() 195 | if err != nil { 196 | errc <- fmt.Errorf("'qemu-system-%s': %s", arch, err) 197 | return 198 | } 199 | }() 200 | 201 | time.Sleep(50 * time.Millisecond) 202 | 203 | var vmerr error 204 | select { 205 | case vmerr = <-errc: 206 | if vmerr != nil { 207 | return nil, vmerr 208 | } 209 | default: 210 | break 211 | } 212 | 213 | return proc, nil 214 | } 215 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package qemu 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | const ( 13 | ImageFormatRAW = "raw" 14 | ImageFormatCLOOP = "cloop" 15 | ImageFormatCOW = "cow" 16 | ImageFormatQCOW = "qcow" 17 | ImageFormatQCOW2 = "qcow2" 18 | ImageFormatVDMK = "vdmk" 19 | ImageFormatVDI = "vdi" 20 | ImageFormatVHDX = "vhdx" 21 | ImageFormatVPC = "vpc" 22 | ) 23 | 24 | // Image represents a QEMU disk image 25 | type Image struct { 26 | Path string // Image location (file) 27 | Format string // Image format 28 | Size uint64 // Image size in bytes 29 | 30 | backingFile string 31 | snapshots []Snapshot 32 | } 33 | 34 | // Snapshot represents a QEMU image snapshot 35 | // Snapshots are snapshots of the complete virtual machine including CPU state 36 | // RAM, device state and the content of all the writable disks 37 | type Snapshot struct { 38 | ID int 39 | Name string 40 | Date time.Time 41 | VMClock time.Time 42 | } 43 | 44 | // NewImage constructs a new Image data structure based 45 | // on the specified parameters 46 | func NewImage(path, format string, size uint64) Image { 47 | var img Image 48 | img.Path = path 49 | img.Format = format 50 | img.Size = size 51 | 52 | return img 53 | } 54 | 55 | // OpenImage retreives the information of the specified image 56 | // file into an Image data structure 57 | func OpenImage(path string) (Image, error) { 58 | var img Image 59 | 60 | if _, err := os.Stat(path); os.IsNotExist(err) { 61 | return img, err 62 | } 63 | 64 | img.Path = path 65 | 66 | err := img.retreiveInfos() 67 | if err != nil { 68 | return img, err 69 | } 70 | 71 | return img, nil 72 | } 73 | 74 | func (i *Image) retreiveInfos() error { 75 | type snapshotInfo struct { 76 | ID string `json:"id"` 77 | Name string `json:"name"` 78 | DateSec int64 `json:"date-sec"` 79 | DateNsec int64 `json:"date-nsec"` 80 | ClockSec int64 `json:"vm-clock-sec"` 81 | ClockNsec int64 `json:"vm-clock-nsec"` 82 | } 83 | 84 | type imgInfo struct { 85 | Snapshots []snapshotInfo `json:"snapshots"` 86 | 87 | Format string `json:"format"` 88 | Size uint64 `json:"virtual-size"` 89 | } 90 | 91 | var info imgInfo 92 | 93 | cmd := exec.Command("qemu-img", "info", "--output=json", i.Path) 94 | 95 | out, err := cmd.CombinedOutput() 96 | if err != nil { 97 | return fmt.Errorf("'qemu-img info' output: %s", oneLine(out)) 98 | } 99 | 100 | err = json.Unmarshal(out, &info) 101 | if err != nil { 102 | return fmt.Errorf("'qemu-img info' invalid json output") 103 | } 104 | 105 | i.Format = info.Format 106 | i.Size = info.Size 107 | 108 | i.snapshots = make([]Snapshot, 0) 109 | for _, snap := range info.Snapshots { 110 | var s Snapshot 111 | 112 | id, err := strconv.Atoi(snap.ID) 113 | if err != nil { 114 | continue 115 | } 116 | 117 | s.ID = id 118 | s.Name = snap.Name 119 | s.Date = time.Unix(snap.DateSec, snap.DateNsec) 120 | s.VMClock = time.Unix(snap.ClockSec, snap.ClockNsec) 121 | 122 | i.snapshots = append(i.snapshots, s) 123 | } 124 | 125 | return nil 126 | } 127 | 128 | // Snapshots returns the snapshots contained 129 | // within the image 130 | func (i Image) Snapshots() ([]Snapshot, error) { 131 | err := i.retreiveInfos() 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | if len(i.snapshots) == 0 { 137 | return make([]Snapshot, 0), nil 138 | } 139 | 140 | return i.snapshots, nil 141 | } 142 | 143 | // CreateSnapshot creates a snapshot of the image 144 | // with the specified name 145 | func (i *Image) CreateSnapshot(name string) error { 146 | cmd := exec.Command("qemu-img", "snapshot", "-c", name, i.Path) 147 | 148 | out, err := cmd.CombinedOutput() 149 | if err != nil { 150 | return fmt.Errorf("'qemu-img snapshot' output: %s", oneLine(out)) 151 | } 152 | 153 | return nil 154 | } 155 | 156 | // RestoreSnapshot restores the the image to the 157 | // specified snapshot name 158 | func (i Image) RestoreSnapshot(name string) error { 159 | cmd := exec.Command("qemu-img", "snapshot", "-a", name, i.Path) 160 | 161 | out, err := cmd.CombinedOutput() 162 | if err != nil { 163 | return fmt.Errorf("'qemu-img snapshot' output: %s", oneLine(out)) 164 | } 165 | 166 | return nil 167 | } 168 | 169 | // DeleteSnapshot deletes the the corresponding 170 | // snapshot from the image 171 | func (i Image) DeleteSnapshot(name string) error { 172 | cmd := exec.Command("qemu-img", "snapshot", "-d", name, i.Path) 173 | 174 | out, err := cmd.CombinedOutput() 175 | if err != nil { 176 | return fmt.Errorf("'qemu-img snapshot' output: %s", oneLine(out)) 177 | } 178 | 179 | return nil 180 | } 181 | 182 | // SetBackingFile sets a backing file for the image 183 | // If it is specified, the image will only record the 184 | // differences from the backing file 185 | func (i *Image) SetBackingFile(backingFile string) error { 186 | if _, err := os.Stat(backingFile); os.IsNotExist(err) { 187 | return err 188 | } 189 | 190 | i.backingFile = backingFile 191 | return nil 192 | } 193 | 194 | // Create actually creates the image based on the Image structure 195 | // using the 'qemu-img create' command 196 | func (i Image) Create() error { 197 | args := []string{"create", "-f", i.Format} 198 | 199 | if len(i.backingFile) > 0 { 200 | args = append(args, "-o") 201 | args = append(args, fmt.Sprintf("backing_file=%s", i.backingFile)) 202 | } 203 | 204 | args = append(args, i.Path) 205 | args = append(args, strconv.FormatUint(i.Size, 10)) 206 | 207 | cmd := exec.Command("qemu-img", args...) 208 | 209 | out, err := cmd.CombinedOutput() 210 | if err != nil { 211 | return fmt.Errorf("'qemu-img create' output: %s", oneLine(out)) 212 | } 213 | 214 | return nil 215 | } 216 | 217 | // Rebase changes the backing file of the image 218 | // to the specified file path 219 | func (i *Image) Rebase(backingFile string) error { 220 | i.backingFile = backingFile 221 | 222 | cmd := exec.Command("qemu-img", "rebase", "-b", backingFile, i.Path) 223 | 224 | out, err := cmd.CombinedOutput() 225 | if err != nil { 226 | return fmt.Errorf("'qemu-img rebase' output: %s", oneLine(out)) 227 | } 228 | 229 | return nil 230 | } 231 | --------------------------------------------------------------------------------