From c8e4b6a7b83344deea7135a2bdca43a7b4c6f347 Mon Sep 17 00:00:00 2001 From: Geoff Hickey Date: Thu, 31 Aug 2017 11:21:31 -0400 Subject: [PATCH] Add Get/SetBlockIoTune to go-libvirt API. (#45) * Add Get/SetBlockIoTune to go-libvirt API. This adds two libvirt entry points to the go-libvirt API: virDomainSetBlockIoTune and virDomainGetBlockIoTune. These can be used to control block device throttling for a VM. --- internal/constants/constants.go | 6 + libvirt.go | 225 +++++++++++++++++++++++++++++++- libvirt_test.go | 24 ++++ libvirttest/libvirt.go | 129 +++++++++++++++++- 4 files changed, 382 insertions(+), 2 deletions(-) diff --git a/internal/constants/constants.go b/internal/constants/constants.go index a7b7aba..d77b3a9 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -53,6 +53,8 @@ const ( ProcDomainUndefineFlags = 231 ProcDomainDestroyFlags = 234 ProcDomainReset = 245 + ProcDomainSetBlockIOTune = 252 + ProcDomainGetBlockIOTune = 253 ProcDomainShutdownFlags = 258 ProcConnectListAllDomains = 273 ProcConnectListAllStoragePools = 281 @@ -78,4 +80,8 @@ const ( // UUIDSize is the length of a UUID, in bytes. UUIDSize = 16 + + // TypedParamFieldLength is VIR_TYPED_PARAM_FIELD_LENGTH, and is the maximum + // length of the Field string in virTypedParameter structs. + TypedParamFieldLength = 80 ) diff --git a/libvirt.go b/libvirt.go index cbe2d69..c12355b 100644 --- a/libvirt.go +++ b/libvirt.go @@ -19,6 +19,7 @@ package libvirt import ( "bufio" "bytes" + "encoding/binary" "encoding/json" "errors" "fmt" @@ -94,6 +95,86 @@ type qemuError struct { // DomainXMLFlags specifies options for dumping a domain's XML. type DomainXMLFlags uint32 +// DomainAffectFlags specifies options for whether an operation affects the +// running VM, or the persistent VM configuration on disk. See FlagDomain... +// consts for values. +type DomainAffectFlags uint32 + +// Consts used for flags + +// virDomainModificationImpact and virTypedParameterFlags values. These are +// combined here because they are both used to set the same flags fields in the +// libvirt API. +const ( + // FlagDomainAffectCurrent means affect the current domain state + FlagDomainAffectCurrent DomainAffectFlags = 0 + // FlagDomainAffectLive means affect the running domain state + FlagDomainAffectLive = 1 << (iota - 1) + // FlagDomainAffectConfig means affect the persistent domain state. + FlagDomainAffectConfig + // FlagTypedParamStringOkay tells the server that this client understands + // TypedParameStrings. + FlagTypedParamStringOkay +) + +// Consts relating to TypedParams: +const ( + // TypedParamInt is a C int. + TypedParamInt = iota + 1 + // TypedParamUInt is a C unsigned int. + TypedParamUInt + // TypedParamLLong is a C long long int. + TypedParamLLong + // TypedParamULLong is a C unsigned long long int. + TypedParamULLong + // TypedParamDouble is a C double. + TypedParamDouble + // TypedParamBoolean is a C char. + TypedParamBoolean + // TypedParamString is a C char*. + TypedParamString + + // TypedParamLast is just an end-of-enum marker. + TypedParamLast +) + +// TypedParam represents libvirt's virTypedParameter, which is used to pass +// typed parameters to libvirt functions. The `Value` field defined as a union +// in libvirt, and given the current set of types inside it, is 8 bytes long. +type TypedParam struct { + Field string + Type int + Value [8]byte +} + +// NewTypedParamInt returns a TypedParam encoding for an int. +func NewTypedParamInt(name string, v int) *TypedParam { + // Truncate the field name if it's longer than the limit. + if len(name) > constants.TypedParamFieldLength { + name = name[:constants.TypedParamFieldLength] + } + tp := TypedParam{ + Field: name, + Type: TypedParamInt, + } + binary.BigEndian.PutUint32(tp.Value[:], uint32(v)) + return &tp +} + +// NewTypedParamULongLong returns a TypedParam encoding for an unsigned long long. +func NewTypedParamULongLong(name string, v uint64) *TypedParam { + // Truncate the field name if it's longer than the limit. + if len(name) > constants.TypedParamFieldLength { + name = name[:constants.TypedParamFieldLength] + } + tp := TypedParam{ + Field: name, + Type: TypedParamULLong, + } + binary.BigEndian.PutUint64(tp.Value[:], v) + return &tp +} + const ( // DomainXMLFlagSecure dumps XML with sensitive information included. DomainXMLFlagSecure DomainXMLFlags = 1 << iota @@ -601,7 +682,7 @@ func (l *Libvirt) Events(dom string) (<-chan DomainEvent, error) { res := <-resp if res.Status != StatusOK { - err := decodeError(res.Payload) + err = decodeError(res.Payload) if err == ErrUnsupported { return nil, ErrEventsNotSupported } @@ -1199,6 +1280,148 @@ func (l *Libvirt) Reset(dom string) error { return nil } +// BlockLimit contains a name and value pair for a Get/SetBlockIOTune limit. The +// Name field is the name of the limit (to see a list of the limits that can be +// applied, execute the 'blkdeviotune' command on a VM in virsh). Callers can +// use the QEMUBlockIO... constants below for the Name value. The Value field is +// the limit to apply. +type BlockLimit struct { + Name string + Value uint64 +} + +// BlockIOTune-able values. These tunables are different for different +// hypervisors; currently only the tunables for QEMU are defined here. These are +// not necessarily the only possible values; different libvirt versions may add +// or remove parameters from this list. +const ( + QEMUBlockIOTotalBytesSec = "total_bytes_sec" + QEMUBlockIOReadBytesSec = "read_bytes_sec" + QEMUBlockIOWriteBytesSec = "write_bytes_sec" + QEMUBlockIOTotalIOPSSec = "total_iops_sec" + QEMUBlockIOReadIOPSSec = "read_iops_sec" + QEMUBlockIOWriteIOPSSec = "write_iops_sec" + QEMUBlockIOTotalBytesSecMax = "total_bytes_sec_max" + QEMUBlockIOReadBytesSecMax = "read_bytes_sec_max" + QEMUBlockIOWriteBytesSecMax = "write_bytes_sec_max" + QEMUBlockIOTotalIOPSSecMax = "total_iops_sec_max" + QEMUBlockIOReadIOPSSecMax = "read_iops_sec_max" + QEMUBlockIOWriteIOPSSecMax = "write_iops_sec_max" + QEMUBlockIOSizeIOPSSec = "size_iops_sec" + QEMUBlockIOTotalBytesSecMaxLength = "total_bytes_sec_max_length" + QEMUBlockIOReadBytesSecMaxLength = "read_bytes_sec_max_length" + QEMUBlockIOWriteBytesSecMaxLength = "write_bytes_sec_max_length" + QEMUBlockIOTotalIOPSSecMaxLength = "total_iops_sec_max_length" + QEMUBlockIOReadIOPSSecMaxLength = "read_iops_sec_max_length" + QEMUBlockIOWriteIOPSSecMaxLength = "write_iops_sec_max_length" +) + +// SetBlockIOTune changes the per-device block I/O tunables within a guest. +// Parameters are the name of the VM, the name of the disk device to which the +// limits should be applied, and 1 or more BlockLimit structs containing the +// actual limits. +// +// The limits which can be applied here are enumerated in the QEMUBlockIO... +// constants above, and you can also see the full list by executing the +// 'blkdeviotune' command on a VM in virsh. +// +// Example usage: +// SetBlockIOTune("vm-name", "vda", BlockLimit{libvirt.QEMUBlockIOWriteBytesSec, 1000000}) +func (l *Libvirt) SetBlockIOTune(dom string, disk string, limits ...BlockLimit) error { + d, err := l.lookup(dom) + if err != nil { + return err + } + + // https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainSetBlockIoTune + payload := struct { + Domain Domain + Disk string + Params []TypedParam + Flags DomainAffectFlags + }{ + Domain: *d, + Disk: disk, + Flags: FlagDomainAffectLive, + } + + for _, limit := range limits { + tp := NewTypedParamULongLong(limit.Name, limit.Value) + payload.Params = append(payload.Params, *tp) + } + + buf, err := encode(&payload) + if err != nil { + return err + } + + resp, err := l.request(constants.ProcDomainSetBlockIOTune, constants.ProgramRemote, &buf) + if err != nil { + return err + } + + r := <-resp + if r.Status != StatusOK { + return decodeError(r.Payload) + } + + return nil +} + +// GetBlockIOTune returns a slice containing the current block I/O tunables for +// a disk. +func (l *Libvirt) GetBlockIOTune(dom string, disk string) ([]BlockLimit, error) { + d, err := l.lookup(dom) + if err != nil { + return nil, err + } + + payload := struct { + Domain Domain + Disk []string + ParamCount uint32 + Flags DomainAffectFlags + }{ + Domain: *d, + Disk: []string{disk}, + ParamCount: 32, + Flags: FlagTypedParamStringOkay, + } + + buf, err := encode(&payload) + if err != nil { + return nil, err + } + + resp, err := l.request(constants.ProcDomainGetBlockIOTune, constants.ProgramRemote, &buf) + if err != nil { + return nil, err + } + + r := <-resp + if r.Status != StatusOK { + return nil, decodeError(r.Payload) + } + + dec := xdr.NewDecoder(bytes.NewReader(r.Payload)) + result := struct { + Limits []TypedParam + ParamCount uint32 + }{} + _, err = dec.Decode(&result) + if err != nil { + return nil, err + } + + limits := make([]BlockLimit, len(result.Limits)) + for ix := range result.Limits { + limits[ix].Name = result.Limits[ix].Field + limits[ix].Value = binary.BigEndian.Uint64(result.Limits[ix].Value[:]) + } + + return limits, nil +} + // lookup returns a domain as seen by libvirt. func (l *Libvirt) lookup(name string) (*Domain, error) { payload := struct { diff --git a/libvirt_test.go b/libvirt_test.go index 9d8c315..ccf0017 100644 --- a/libvirt_test.go +++ b/libvirt_test.go @@ -437,3 +437,27 @@ func TestReset(t *testing.T) { t.Fatalf("unexpected reset error: %v", err) } } + +func TestSetBlockIOTune(t *testing.T) { + conn := libvirttest.New() + l := New(conn) + + if err := l.SetBlockIOTune("test", "vda", BlockLimit{"write_bytes_sec", 5000000}); err != nil { + t.Fatalf("unexpected SetBlockIOTune error: %v", err) + } +} + +func TestGetBlockIOTune(t *testing.T) { + conn := libvirttest.New() + l := New(conn) + + limits, err := l.GetBlockIOTune("do-test", "vda") + if err != nil { + t.Fatalf("unexpected GetBlockIOTune error: %v", err) + } + + lim := BlockLimit{"write_bytes_sec", 500000} + if limits[2] != lim { + t.Fatalf("unexpected result in limits list: %v", limits[2]) + } +} diff --git a/libvirttest/libvirt.go b/libvirttest/libvirt.go index 5de6191..8e965c3 100644 --- a/libvirttest/libvirt.go +++ b/libvirttest/libvirt.go @@ -21,8 +21,9 @@ import ( "sync/atomic" "fmt" - "github.com/digitalocean/go-libvirt/internal/constants" "os" + + "github.com/digitalocean/go-libvirt/internal/constants" ) var testDomainResponse = []byte{ @@ -399,6 +400,128 @@ var testRebootReply = []byte{ 0x00, 0x00, 0x00, 0x00, // status } +var testSetBlockIoTuneReply = []byte{ + 0x00, 0x00, 0x00, 0x1c, // length + 0x20, 0x00, 0x80, 0x86, // program + 0x00, 0x00, 0x00, 0x01, // version + 0x00, 0x00, 0x00, 0xfc, // procedure + 0x00, 0x00, 0x00, 0x00, // type + 0x00, 0x00, 0x00, 0x00, // serial + 0x00, 0x00, 0x00, 0x00, // status +} + +// This result block was obtained by calling `fmt.Printf("%#v", r.Payload)` on +// the result returned by an actual call to GetBlockIoTune, and then adding the +// standard header to the beginning. The length parameter has to be correct! +var testGetBlockIoTuneReply = []byte{ + 0x00, 0x00, 0x02, 0xe0, // length + 0x20, 0x00, 0x80, 0x86, // program + 0x00, 0x00, 0x00, 0x01, // version + 0x00, 0x00, 0x00, 0xfd, // procedure + 0x00, 0x00, 0x00, 0x00, // type + 0x00, 0x00, 0x00, 0x00, // serial + 0x00, 0x00, 0x00, 0x00, // status + + 0x0, 0x0, 0x0, 0x13, // 13 TypedParams follow + + 0x0, 0x0, 0x0, 0xf, // field name is 15 bytes, padded to a multiple of 4 + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x0, + 0x0, 0x0, 0x0, 0x4, // type + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // value + + 0x0, 0x0, 0x0, 0xe, + 0x72, 0x65, 0x61, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0xf, + 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xa1, 0x20, + + 0x0, 0x0, 0x0, 0xe, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x69, 0x6f, 0x70, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0xd, + 0x72, 0x65, 0x61, 0x64, 0x5f, 0x69, 0x6f, 0x70, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0xe, + 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x69, 0x6f, 0x70, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0x13, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0x12, + 0x72, 0x65, 0x61, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0x13, + 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc3, 0x50, + + 0x0, 0x0, 0x0, 0x12, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x69, 0x6f, 0x70, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0x11, + 0x72, 0x65, 0x61, 0x64, 0x5f, 0x69, 0x6f, 0x70, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0x12, + 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x69, 0x6f, 0x70, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0xd, + 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x69, 0x6f, 0x70, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0x1a, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0x19, + 0x72, 0x65, 0x61, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0x1a, + 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + + 0x0, 0x0, 0x0, 0x19, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x69, 0x6f, 0x70, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0x18, + 0x72, 0x65, 0x61, 0x64, 0x5f, 0x69, 0x6f, 0x70, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0x19, + 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x69, 0x6f, 0x70, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + + 0x0, 0x0, 0x0, 0x0, // End of TypedParams +} + // MockLibvirt provides a mock libvirt server for testing. type MockLibvirt struct { net.Conn @@ -486,6 +609,10 @@ func (m *MockLibvirt) handleRemote(procedure uint32, conn net.Conn) { conn.Write(m.reply(testCreateWithFlags)) case constants.ProcDomainShutdownFlags: conn.Write(m.reply(testShutdownReply)) + case constants.ProcDomainSetBlockIOTune: + conn.Write(m.reply(testSetBlockIoTuneReply)) + case constants.ProcDomainGetBlockIOTune: + conn.Write(m.reply(testGetBlockIoTuneReply)) default: fmt.Fprintln(os.Stderr, "unknown procedure", procedure) }