Repository: hetznercloud/hcloud-go
Pull request #21 (2018-01-05 14:41:59)

Add missing fields and refactor types


Patch:
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..69ecbb4
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Hetzner Cloud GmbH
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/hcloud/action.go b/hcloud/action.go
index fcada36..e673c3f 100644
--- a/hcloud/action.go
+++ b/hcloud/action.go
@@ -18,6 +18,7 @@ type Action struct {
 	Finished     time.Time
 	ErrorCode    string
 	ErrorMessage string
+	Resources    []*ActionResource
 }
 
 // ActionStatus represents an action's status.
@@ -30,6 +31,23 @@ const (
 	ActionStatusError                = "error"
 )
 
+// ActionResource references other resources from an action.
+type ActionResource struct {
+	ID   int
+	Type ActionResourceType
+}
+
+// ActionResourceType represents an action's resource reference type.
+type ActionResourceType string
+
+// List of action resource reference types.
+const (
+	ActionResourceTypeServer     ActionResourceType = "server"
+	ActionResourceTypeImage                         = "image"
+	ActionResourceTypeISO                           = "iso"
+	ActionResourceTypeFloatingIP                    = "floating_ip"
+)
+
 func (a *Action) Error() error {
 	if a.ErrorCode != "" && a.ErrorMessage != "" {
 		return fmt.Errorf("%s (%s)", a.ErrorMessage, a.ErrorCode)
diff --git a/hcloud/floating_ip.go b/hcloud/floating_ip.go
index c7155ae..2746b98 100644
--- a/hcloud/floating_ip.go
+++ b/hcloud/floating_ip.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"net"
 
 	"github.com/hetznercloud/hcloud-go/hcloud/schema"
 )
@@ -14,11 +15,18 @@ import (
 type FloatingIP struct {
 	ID           int
 	Description  string
-	IP           string
+	IP           net.IP
+	Network      *net.IPNet
 	Type         FloatingIPType
 	Server       *Server
 	DNSPtr       map[string]string
 	HomeLocation *Location
+	Blocked      bool
+}
+
+// DNSPtrForIP returns the reverse DNS pointer of the IP address.
+func (f *FloatingIP) DNSPtrForIP(ip net.IP) string {
+	return f.DNSPtr[ip.String()]
 }
 
 // FloatingIPType represents the type of a Floating IP.
diff --git a/hcloud/image.go b/hcloud/image.go
index 9af2890..79da6b1 100644
--- a/hcloud/image.go
+++ b/hcloud/image.go
@@ -19,6 +19,8 @@ type Image struct {
 	ImageSize   float32
 	DiskSize    float32
 	Created     time.Time
+	CreatedFrom *Server
+	BoundTo     *Server
 	RapidDeploy bool
 
 	OSFlavor  string
diff --git a/hcloud/schema.go b/hcloud/schema.go
index e875610..fe55867 100644
--- a/hcloud/schema.go
+++ b/hcloud/schema.go
@@ -1,6 +1,10 @@
 package hcloud
 
-import "github.com/hetznercloud/hcloud-go/hcloud/schema"
+import (
+	"net"
+
+	"github.com/hetznercloud/hcloud-go/hcloud/schema"
+)
 
 // This file provides converter functions to convert models in the
 // schema package to models in the hcloud package.
@@ -8,11 +12,12 @@ import "github.com/hetznercloud/hcloud-go/hcloud/schema"
 // ActionFromSchema converts a schema.Action to an Action.
 func ActionFromSchema(s schema.Action) *Action {
 	action := &Action{
-		ID:       s.ID,
-		Status:   ActionStatus(s.Status),
-		Command:  s.Command,
-		Progress: s.Progress,
-		Started:  s.Started,
+		ID:        s.ID,
+		Status:    ActionStatus(s.Status),
+		Command:   s.Command,
+		Progress:  s.Progress,
+		Started:   s.Started,
+		Resources: []*ActionResource{},
 	}
 	if s.Finished != nil {
 		action.Finished = *s.Finished
@@ -21,6 +26,12 @@ func ActionFromSchema(s schema.Action) *Action {
 		action.ErrorCode = s.Error.Code
 		action.ErrorMessage = s.Error.Message
 	}
+	for _, r := range s.Resources {
+		action.Resources = append(action.Resources, &ActionResource{
+			ID:   r.ID,
+			Type: ActionResourceType(r.Type),
+		})
+	}
 	return action
 }
 
@@ -28,9 +39,9 @@ func ActionFromSchema(s schema.Action) *Action {
 func FloatingIPFromSchema(s schema.FloatingIP) *FloatingIP {
 	f := &FloatingIP{
 		ID:           s.ID,
-		IP:           s.IP,
 		Type:         FloatingIPType(s.Type),
 		HomeLocation: LocationFromSchema(s.HomeLocation),
+		Blocked:      s.Blocked,
 	}
 	if s.Description != nil {
 		f.Description = *s.Description
@@ -38,15 +49,14 @@ func FloatingIPFromSchema(s schema.FloatingIP) *FloatingIP {
 	if s.Server != nil {
 		f.Server = &Server{ID: *s.Server}
 	}
-	if s.DNSPtr.IPv4 != nil {
-		f.DNSPtr = map[string]string{
-			s.IP: *s.DNSPtr.IPv4,
-		}
-	} else if s.DNSPtr.IPv6 != nil {
-		f.DNSPtr = map[string]string{}
-		for _, entry := range s.DNSPtr.IPv6 {
-			f.DNSPtr[entry.IP] = entry.DNSPtr
-		}
+	if f.Type == FloatingIPTypeIPv4 {
+		f.IP = net.ParseIP(s.IP)
+	} else {
+		f.IP, f.Network, _ = net.ParseCIDR(s.IP)
+	}
+	f.DNSPtr = map[string]string{}
+	for _, entry := range s.DNSPtr {
+		f.DNSPtr[entry.IP] = entry.DNSPtr
 	}
 	return f
 }
@@ -74,6 +84,16 @@ func LocationFromSchema(s schema.Location) *Location {
 	}
 }
 
+// DatacenterFromSchema converts a schema.Datacenter to a Datacenter.
+func DatacenterFromSchema(s schema.Datacenter) *Datacenter {
+	return &Datacenter{
+		ID:          s.ID,
+		Name:        s.Name,
+		Description: s.Description,
+		Location:    LocationFromSchema(s.Location),
+	}
+}
+
 // ServerFromSchema converts a schema.Server to a Server.
 func ServerFromSchema(s schema.Server) *Server {
 	server := &Server{
@@ -85,6 +105,8 @@ func ServerFromSchema(s schema.Server) *Server {
 		ServerType:      ServerTypeFromSchema(s.ServerType),
 		IncludedTraffic: s.IncludedTraffic,
 		RescueEnabled:   s.RescueEnabled,
+		Datacenter:      DatacenterFromSchema(s.Datacenter),
+		Locked:          s.Locked,
 	}
 	if s.BackupWindow != nil {
 		server.BackupWindow = *s.BackupWindow
@@ -117,7 +139,7 @@ func ServerPublicNetFromSchema(s schema.ServerPublicNet) ServerPublicNet {
 // a ServerPublicNetIPv4.
 func ServerPublicNetIPv4FromSchema(s schema.ServerPublicNetIPv4) ServerPublicNetIPv4 {
 	return ServerPublicNetIPv4{
-		IP:      s.IP,
+		IP:      net.ParseIP(s.IP),
 		Blocked: s.Blocked,
 		DNSPtr:  s.DNSPtr,
 	}
@@ -127,24 +149,17 @@ func ServerPublicNetIPv4FromSchema(s schema.ServerPublicNetIPv4) ServerPublicNet
 // a ServerPublicNetIPv6.
 func ServerPublicNetIPv6FromSchema(s schema.ServerPublicNetIPv6) ServerPublicNetIPv6 {
 	ipv6 := ServerPublicNetIPv6{
-		IP:      s.IP,
 		Blocked: s.Blocked,
+		DNSPtr:  map[string]string{},
 	}
+	ipv6.IP, ipv6.Network, _ = net.ParseCIDR(s.IP)
+
 	for _, dnsPtr := range s.DNSPtr {
-		ipv6.DNSPtr = append(ipv6.DNSPtr, ServerPublicNetIPv6DNSPtrFromSchema(dnsPtr))
+		ipv6.DNSPtr[dnsPtr.IP] = dnsPtr.DNSPtr
 	}
 	return ipv6
 }
 
-// ServerPublicNetIPv6DNSPtrFromSchema converts a schema.ServerPublicNetIPv6DNSPtr
-// to a ServerPublicNetIPv6DNSPtr.
-func ServerPublicNetIPv6DNSPtrFromSchema(s schema.ServerPublicNetIPv6DNSPtr) ServerPublicNetIPv6DNSPtr {
-	return ServerPublicNetIPv6DNSPtr{
-		IP:     s.IP,
-		DNSPtr: s.DNSPtr,
-	}
-}
-
 // ServerTypeFromSchema converts a schema.ServerType to a ServerType.
 func ServerTypeFromSchema(s schema.ServerType) *ServerType {
 	st := &ServerType{
@@ -191,7 +206,17 @@ func ImageFromSchema(s schema.Image) *Image {
 	if s.OSVersion != nil {
 		i.OSVersion = *s.OSVersion
 	}
-
+	if s.CreatedFrom != nil {
+		i.CreatedFrom = &Server{
+			ID:   s.CreatedFrom.ID,
+			Name: s.CreatedFrom.Name,
+		}
+	}
+	if s.BoundTo != nil {
+		i.BoundTo = &Server{
+			ID: *s.BoundTo,
+		}
+	}
 	return i
 }
 
diff --git a/hcloud/schema/action.go b/hcloud/schema/action.go
index 70fb542..c34c74d 100644
--- a/hcloud/schema/action.go
+++ b/hcloud/schema/action.go
@@ -4,13 +4,20 @@ import "time"
 
 // Action defines the schema of an action.
 type Action struct {
-	ID       int          `json:"id"`
-	Status   string       `json:"status"`
-	Command  string       `json:"command"`
-	Progress int          `json:"progress"`
-	Started  time.Time    `json:"started"`
-	Finished *time.Time   `json:"finished"`
-	Error    *ActionError `json:"error"`
+	ID        int                       `json:"id"`
+	Status    string                    `json:"status"`
+	Command   string                    `json:"command"`
+	Progress  int                       `json:"progress"`
+	Started   time.Time                 `json:"started"`
+	Finished  *time.Time                `json:"finished"`
+	Error     *ActionError              `json:"error"`
+	Resources []ActionResourceReference `json:"resources"`
+}
+
+// ActionResourceReference defines the schema of an action resource reference.
+type ActionResourceReference struct {
+	ID   int    `json:"id"`
+	Type string `json:"type"`
 }
 
 // ActionError defines the schema of an error embedded
diff --git a/hcloud/schema/datacenter.go b/hcloud/schema/datacenter.go
new file mode 100644
index 0000000..0d15080
--- /dev/null
+++ b/hcloud/schema/datacenter.go
@@ -0,0 +1,9 @@
+package schema
+
+// Datacenter defines the schema of a datacenter.
+type Datacenter struct {
+	ID          int      `json:"id"`
+	Name        string   `json:"name"`
+	Description string   `json:"description"`
+	Location    Location `json:"location"`
+}
diff --git a/hcloud/schema/floating_ip.go b/hcloud/schema/floating_ip.go
index 9783568..c539838 100644
--- a/hcloud/schema/floating_ip.go
+++ b/hcloud/schema/floating_ip.go
@@ -1,63 +1,24 @@
 package schema
 
-import (
-	"encoding/json"
-	"errors"
-)
-
 // FloatingIP defines the schema of a Floating IP.
 type FloatingIP struct {
-	ID           int              `json:"id"`
-	Description  *string          `json:"description"`
-	IP           string           `json:"ip"`
-	Type         string           `json:"type"`
-	Server       *int             `json:"server"`
-	DNSPtr       FloatingIPDNSPtr `json:"dns_ptr"`
-	HomeLocation Location         `json:"home_location"`
+	ID           int                `json:"id"`
+	Description  *string            `json:"description"`
+	IP           string             `json:"ip"`
+	Type         string             `json:"type"`
+	Server       *int               `json:"server"`
+	DNSPtr       []FloatingIPDNSPtr `json:"dns_ptr"`
+	HomeLocation Location           `json:"home_location"`
+	Blocked      bool               `json:"blocked"`
 }
 
 // FloatingIPDNSPtr contains reverse DNS information for a
 // IPv4 or IPv6 Floating IP.
 type FloatingIPDNSPtr struct {
-	IPv4 *string
-	IPv6 []FloatingIPDNSPtrIPv6
-}
-
-// FloatingIPDNSPtrIPv6 defines the schema of reverse DNS
-// information for a single IPv6.
-type FloatingIPDNSPtrIPv6 struct {
 	IP     string `json:"ip"`
 	DNSPtr string `json:"dns_ptr"`
 }
 
-// MarshalJSON implements json.Marshaler.
-func (p FloatingIPDNSPtr) MarshalJSON() ([]byte, error) {
-	if p.IPv4 != nil {
-		return json.Marshal(p.IPv4)
-	}
-	if p.IPv6 != nil {
-		return json.Marshal(p.IPv6)
-	}
-	return json.Marshal(nil)
-}
-
-// UnmarshalJSON implements json.Unmarshaler.
-func (p *FloatingIPDNSPtr) UnmarshalJSON(data []byte) error {
-	var ipv4 string
-	if err := json.Unmarshal(data, &ipv4); err == nil {
-		p.IPv4 = &ipv4
-		return nil
-	}
-
-	var ipv6 []FloatingIPDNSPtrIPv6
-	if err := json.Unmarshal(data, &ipv6); err == nil {
-		p.IPv6 = ipv6
-		return nil
-	}
-
-	return errors.New("schema: unable to unmarshal dns_ptr")
-}
-
 // FloatingIPGetResponse defines the schema of the response when
 // retrieving a single Floating IP.
 type FloatingIPGetResponse struct {
diff --git a/hcloud/schema/image.go b/hcloud/schema/image.go
index be1ead4..b58e486 100644
--- a/hcloud/schema/image.go
+++ b/hcloud/schema/image.go
@@ -4,17 +4,25 @@ import "time"
 
 // Image defines the schema of an image.
 type Image struct {
-	ID          int       `json:"id"`
-	Status      string    `json:"status"`
-	Type        string    `json:"type"`
-	Name        *string   `json:"name"`
-	Description string    `json:"description"`
-	ImageSize   *float32  `json:"image_size"`
-	DiskSize    float32   `json:"disk_size"`
-	Created     time.Time `json:"created"`
-	OSFlavor    string    `json:"os_flavor"`
-	OSVersion   *string   `json:"os_version"`
-	RapidDeploy bool      `json:"rapid_deploy"`
+	ID          int               `json:"id"`
+	Status      string            `json:"status"`
+	Type        string            `json:"type"`
+	Name        *string           `json:"name"`
+	Description string            `json:"description"`
+	ImageSize   *float32          `json:"image_size"`
+	DiskSize    float32           `json:"disk_size"`
+	Created     time.Time         `json:"created"`
+	CreatedFrom *ImageCreatedFrom `json:"created_from"`
+	BoundTo     *int              `json:"bound_to"`
+	OSFlavor    string            `json:"os_flavor"`
+	OSVersion   *string           `json:"os_version"`
+	RapidDeploy bool              `json:"rapid_deploy"`
+}
+
+// ImageCreatedFrom defines the schema of the images created from reference.
+type ImageCreatedFrom struct {
+	ID   int    `json:"id"`
+	Name string `json:"name"`
 }
 
 // ImageGetResponse defines the schema of the response when
diff --git a/hcloud/schema/server.go b/hcloud/schema/server.go
index aa6eeed..5d6c6c0 100644
--- a/hcloud/schema/server.go
+++ b/hcloud/schema/server.go
@@ -16,6 +16,8 @@ type Server struct {
 	BackupWindow    *string         `json:"backup_window"`
 	RescueEnabled   bool            `json:"rescue_enabled"`
 	ISO             *ISO            `json:"iso"`
+	Locked          bool            `json:"locked"`
+	Datacenter      Datacenter      `json:"datacenter"`
 }
 
 // ServerPublicNet defines the schema of a server's
diff --git a/hcloud/server.go b/hcloud/server.go
index b979d2a..8684825 100644
--- a/hcloud/server.go
+++ b/hcloud/server.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"net"
 	"net/url"
 	"strconv"
 	"time"
@@ -21,11 +22,13 @@ type Server struct {
 	Created         time.Time
 	PublicNet       ServerPublicNet
 	ServerType      *ServerType
+	Datacenter      *Datacenter
 	IncludedTraffic uint64
 	OutgoingTraffic uint64
 	IngoingTraffic  uint64
 	BackupWindow    string
 	RescueEnabled   bool
+	Locked          bool
 	ISO             *ISO
 }
 
@@ -50,24 +53,24 @@ type ServerPublicNet struct {
 	FloatingIPs []*FloatingIP
 }
 
-// ServerPublicNetIPv4 represents a server's public IPv4 network.
+// ServerPublicNetIPv4 represents a server's public IPv4 address.
 type ServerPublicNetIPv4 struct {
-	IP      string
+	IP      net.IP
 	Blocked bool
 	DNSPtr  string
 }
 
-// ServerPublicNetIPv6 represents a server's public IPv6 network.
+// ServerPublicNetIPv6 represents a server's public IPv6 network and address.
 type ServerPublicNetIPv6 struct {
-	IP      string
+	IP      net.IP
+	Network *net.IPNet
 	Blocked bool
-	DNSPtr  []ServerPublicNetIPv6DNSPtr
+	DNSPtr  map[string]string
 }
 
-// ServerPublicNetIPv6DNSPtr represents a server's public IPv6 reverse DNS.
-type ServerPublicNetIPv6DNSPtr struct {
-	IP     string
-	DNSPtr string
+// DNSPtrForIP returns the reverse dns pointer of the ip address.
+func (s *ServerPublicNetIPv6) DNSPtrForIP(ip net.IP) string {
+	return s.DNSPtr[ip.String()]
 }
 
 // ServerRescueType represents rescue types.


Test patch:
diff --git a/hcloud/floating_ip_test.go b/hcloud/floating_ip_test.go
index a693345..5ccd8b6 100644
--- a/hcloud/floating_ip_test.go
+++ b/hcloud/floating_ip_test.go
@@ -16,7 +16,9 @@ func TestFloatingIPClientGetByID(t *testing.T) {
 	env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) {
 		json.NewEncoder(w).Encode(schema.FloatingIPGetResponse{
 			FloatingIP: schema.FloatingIP{
-				ID: 1,
+				ID:   1,
+				Type: "ipv4",
+				IP:   "131.232.99.1",
 			},
 		})
 	})
@@ -71,8 +73,8 @@ func TestFloatingIPClientList(t *testing.T) {
 		}
 		json.NewEncoder(w).Encode(schema.FloatingIPListResponse{
 			FloatingIPs: []schema.FloatingIP{
-				{ID: 1},
-				{ID: 2},
+				{ID: 1, Type: "ipv4", IP: "131.232.99.1"},
+				{ID: 2, Type: "ipv4", IP: "131.232.99.1"},
 			},
 		})
 	})
@@ -100,9 +102,7 @@ func TestFloatingIPClientCreate(t *testing.T) {
 			t.Error("expected POST")
 		}
 		json.NewEncoder(w).Encode(schema.FloatingIPCreateResponse{
-			FloatingIP: schema.FloatingIP{
-				ID: 1,
-			},
+			FloatingIP: schema.FloatingIP{ID: 1, Type: "ipv4", IP: "131.232.99.1"},
 			Action: &schema.Action{
 				ID: 1,
 			},
diff --git a/hcloud/schema/floating_ip_test.go b/hcloud/schema/floating_ip_test.go
deleted file mode 100644
index 91c2a81..0000000
--- a/hcloud/schema/floating_ip_test.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package schema
-
-import (
-	"bytes"
-	"encoding/json"
-	"testing"
-)
-
-func TestFloatingIPDNSPtrMarshalJSON(t *testing.T) {
-	t.Run("IPv4", func(t *testing.T) {
-		s := "foo.example"
-		data, err := json.Marshal(FloatingIPDNSPtr{IPv4: &s})
-		if err != nil {
-			t.Fatal(err)
-		}
-		if !bytes.Equal(data, []byte(`"foo.example"`)) {
-			t.Errorf("unexpected data: %s", data)
-		}
-	})
-	t.Run("IPv6", func(t *testing.T) {
-		data, err := json.Marshal(FloatingIPDNSPtr{
-			IPv6: []FloatingIPDNSPtrIPv6{
-				{IP: "::1", DNSPtr: "foo.example.com"},
-			},
-		})
-		if err != nil {
-			t.Fatal(err)
-		}
-		if !bytes.Equal(data, []byte(`[{"ip":"::1","dns_ptr":"foo.example.com"}]`)) {
-			t.Errorf("unexpected data: %s", data)
-		}
-	})
-}
-
-func TestFloatingIPDNSPtrUnmarshalJSON(t *testing.T) {
-	t.Run("IPv4", func(t *testing.T) {
-		data := []byte(`"foo.example"`)
-		var p FloatingIPDNSPtr
-		if err := json.Unmarshal(data, &p); err != nil {
-			t.Fatal(err)
-		}
-		if p.IPv4 == nil {
-			t.Fatalf("missing IPv4")
-		}
-		if *p.IPv4 != "foo.example" {
-			t.Errorf("unexpected IPv4: %v", *p.IPv4)
-		}
-		if p.IPv6 != nil {
-			t.Error("IPv6 should be nil")
-		}
-	})
-	t.Run("IPv6", func(t *testing.T) {
-		data := []byte(`[{"ip":"::1","dns_ptr":"foo.example.com"}]`)
-		var p FloatingIPDNSPtr
-		if err := json.Unmarshal(data, &p); err != nil {
-			t.Fatal(err)
-		}
-		if p.IPv4 != nil {
-			t.Error("IPv4 should be nil")
-		}
-		if len(p.IPv6) != 1 {
-			t.Fatalf("unexpected one entry")
-		}
-		if p.IPv6[0].IP != "::1" {
-			t.Errorf("unexpected IP: %v", p.IPv6[0].IP)
-		}
-		if p.IPv6[0].DNSPtr != "foo.example.com" {
-			t.Errorf("unexpected ptr: %v", p.IPv6[0].DNSPtr)
-		}
-	})
-}
diff --git a/hcloud/schema_test.go b/hcloud/schema_test.go
index fa6b712..a96cdff 100644
--- a/hcloud/schema_test.go
+++ b/hcloud/schema_test.go
@@ -58,54 +58,125 @@ func TestActionFromSchema(t *testing.T) {
 	if action.ErrorMessage != "Action failed" {
 		t.Errorf("unexpected error message: %v", action.ErrorMessage)
 	}
+	if len(action.Resources) == 1 {
+		if action.Resources[0].ID != 42 {
+			t.Errorf("unexpected id in resources[0].ID: %v", action.Resources[0].ID)
+		}
+		if action.Resources[0].Type != ActionResourceTypeServer {
+			t.Errorf("unexpected type in resources[0].Type: %v", action.Resources[0].Type)
+		}
+	} else {
+		t.Errorf("unexpected number of resources")
+	}
 }
 
 func TestFloatingIPFromSchema(t *testing.T) {
-	data := []byte(`{
-		"id": 4711,
-		"description": "Web Frontend",
-		"ip": "131.232.99.1",
-		"type": "ipv4",
-		"server": 42,
-		"dns_ptr": "fip01.example.com",
-		"home_location": {
-			"id": 1,
-			"name": "fsn1",
-			"description": "Falkenstein DC Park 1",
-			"country": "DE",
-			"city": "Falkenstein",
-			"latitude": 50.47612,
-			"longitude": 12.370071
+	t.Run("IPv6", func(t *testing.T) {
+		data := []byte(`{
+			"id": 4711,
+			"description": "Web Frontend",
+			"ip": "2001:db8::/64",
+			"type": "ipv6",
+			"server": null,
+			"dns_ptr": [],
+			"blocked": true,
+			"home_location": {
+				"id": 1,
+				"name": "fsn1",
+				"description": "Falkenstein DC Park 1",
+				"country": "DE",
+				"city": "Falkenstein",
+				"latitude": 50.47612,
+				"longitude": 12.370071
+			}
+		}`)
+
+		var s schema.FloatingIP
+		if err := json.Unmarshal(data, &s); err != nil {
+			t.Fatal(err)
 		}
-	}`)
+		floatingIP := FloatingIPFromSchema(s)
 
-	var s schema.FloatingIP
-	if err := json.Unmarshal(data, &s); err != nil {
-		t.Fatal(err)
-	}
-	floatingIP := FloatingIPFromSchema(s)
+		if floatingIP.ID != 4711 {
+			t.Errorf("unexpected ID: %v", floatingIP.ID)
+		}
+		if !floatingIP.Blocked {
+			t.Errorf("unexpected value for Blocked: %v", floatingIP.Blocked)
+		}
+		if floatingIP.Description != "Web Frontend" {
+			t.Errorf("unexpected description: %v", floatingIP.Description)
+		}
+		if floatingIP.IP.String() != "2001:db8::" {
+			t.Errorf("unexpected IP: %v", floatingIP.IP)
+		}
+		if floatingIP.Type != FloatingIPTypeIPv6 {
+			t.Errorf("unexpected Type: %v", floatingIP.Type)
+		}
+		if floatingIP.Server != nil {
+			t.Errorf("unexpected Server: %v", floatingIP.Server)
+		}
+		if floatingIP.DNSPtr == nil || floatingIP.DNSPtrForIP(floatingIP.IP) != "" {
+			t.Errorf("unexpected DNS ptr: %v", floatingIP.DNSPtr)
+		}
+		if floatingIP.HomeLocation == nil || floatingIP.HomeLocation.ID != 1 {
+			t.Errorf("unexpected home location: %v", floatingIP.HomeLocation)
+		}
+	})
 
-	if floatingIP.ID != 4711 {
-		t.Errorf("unexpected ID: %v", floatingIP.ID)
-	}
-	if floatingIP.Description != "Web Frontend" {
-		t.Errorf("unexpected description: %v", floatingIP.Description)
-	}
-	if floatingIP.IP != "131.232.99.1" {
-		t.Errorf("unexpected IP: %v", floatingIP.IP)
-	}
-	if floatingIP.Type != FloatingIPTypeIPv4 {
-		t.Errorf("unexpected type: %v", floatingIP.Type)
-	}
-	if floatingIP.Server == nil || floatingIP.Server.ID != 42 {
-		t.Errorf("unexpected server: %v", floatingIP.Server)
-	}
-	if floatingIP.DNSPtr == nil || floatingIP.DNSPtr[floatingIP.IP] != "fip01.example.com" {
-		t.Errorf("unexpected DNS ptr: %v", floatingIP.DNSPtr)
-	}
-	if floatingIP.HomeLocation == nil || floatingIP.HomeLocation.ID != 1 {
-		t.Errorf("unexpected home location: %v", floatingIP.HomeLocation)
-	}
+	t.Run("IPv4", func(t *testing.T) {
+		data := []byte(`{
+			"id": 4711,
+			"description": "Web Frontend",
+			"ip": "131.232.99.1",
+			"type": "ipv4",
+			"server": 42,
+			"dns_ptr": [{
+				"ip": "131.232.99.1",
+				"dns_ptr": "fip01.example.com"
+			}],
+			"blocked": false,
+			"home_location": {
+				"id": 1,
+				"name": "fsn1",
+				"description": "Falkenstein DC Park 1",
+				"country": "DE",
+				"city": "Falkenstein",
+				"latitude": 50.47612,
+				"longitude": 12.370071
+			}
+		}`)
+
+		var s schema.FloatingIP
+		if err := json.Unmarshal(data, &s); err != nil {
+			t.Fatal(err)
+		}
+		floatingIP := FloatingIPFromSchema(s)
+
+		if floatingIP.ID != 4711 {
+			t.Errorf("unexpected ID: %v", floatingIP.ID)
+		}
+		if floatingIP.Blocked {
+			t.Errorf("unexpected value for Blocked: %v", floatingIP.Blocked)
+		}
+		if floatingIP.Description != "Web Frontend" {
+			t.Errorf("unexpected description: %v", floatingIP.Description)
+		}
+		if floatingIP.IP.String() != "131.232.99.1" {
+			t.Errorf("unexpected IP: %v", floatingIP.IP)
+		}
+		if floatingIP.Type != FloatingIPTypeIPv4 {
+			t.Errorf("unexpected type: %v", floatingIP.Type)
+		}
+		if floatingIP.Server == nil || floatingIP.Server.ID != 42 {
+			t.Errorf("unexpected server: %v", floatingIP.Server)
+		}
+		if floatingIP.DNSPtr == nil || floatingIP.DNSPtrForIP(floatingIP.IP) != "fip01.example.com" {
+			t.Errorf("unexpected DNS ptr: %v", floatingIP.DNSPtr)
+		}
+		if floatingIP.HomeLocation == nil || floatingIP.HomeLocation.ID != 1 {
+			t.Errorf("unexpected home location: %v", floatingIP.HomeLocation)
+		}
+	})
 }
 
 func TestISOFromSchema(t *testing.T) {
@@ -136,6 +207,52 @@ func TestISOFromSchema(t *testing.T) {
 	}
 }
 
+func TestDatacenterFromSchema(t *testing.T) {
+	data := []byte(`{
+		"id": 1,
+		"name": "fsn1-dc8",
+		"description": "Falkenstein 1 DC 8",
+		"location": {
+			"id": 1,
+			"name": "fsn1",
+			"description": "Falkenstein DC Park 1",
+			"country": "DE",
+			"city": "Falkenstein",
+			"latitude": 50.47612,
+			"longitude": 12.370071
+		},
+		"server_types": {
+			"supported": [
+				1,
+				1,
+				2,
+				3
+			],
+			"available": [
+				1,
+				1,
+				2,
+				3
+			]
+		}
+	}`)
+
+	var s schema.Datacenter
+	if err := json.Unmarshal(data, &s); err != nil {
+		t.Fatal(err)
+	}
+	datacenter := DatacenterFromSchema(s)
+	if datacenter.ID != 1 {
+		t.Errorf("unexpected ID: %v", datacenter.ID)
+	}
+	if datacenter.Name != "fsn1-dc8" {
+		t.Errorf("unexpected Name: %v", datacenter.Name)
+	}
+	if datacenter.Location == nil || datacenter.Location.ID != 1 {
+		t.Errorf("unexpected Location: %v", datacenter.Location)
+	}
+}
+
 func TestServerFromSchema(t *testing.T) {
 	data := []byte(`{
 		"id": 1,
@@ -172,7 +289,22 @@ func TestServerFromSchema(t *testing.T) {
 			"name": "FreeBSD-11.0-RELEASE-amd64-dvd1",
 			"description": "FreeBSD 11.0 x64",
 			"type": "public"
-		}
+		},
+		"datacenter": {
+			"id": 1,
+			"name": "fsn1-dc8",
+			"description": "Falkenstein 1 DC 8",
+			"location": {
+				"id": 1,
+				"name": "fsn1",
+				"description": "Falkenstein DC Park 1",
+				"country": "DE",
+				"city": "Falkenstein",
+				"latitude": 50.47612,
+				"longitude": 12.370071
+			}
+		},
+		"locked": true
 	}`)
 
 	var s schema.Server
@@ -193,7 +325,7 @@ func TestServerFromSchema(t *testing.T) {
 	if !server.Created.Equal(time.Date(2017, 8, 16, 17, 29, 14, 0, time.UTC)) {
 		t.Errorf("unexpected created date: %v", server.Created)
 	}
-	if server.PublicNet.IPv4.IP != "1.2.3.4" {
+	if server.PublicNet.IPv4.IP.String() != "1.2.3.4" {
 		t.Errorf("unexpected public net IPv4 IP: %v", server.PublicNet.IPv4.IP)
 	}
 	if server.ServerType.ID != 2 {
@@ -217,10 +349,33 @@ func TestServerFromSchema(t *testing.T) {
 	if server.ISO == nil || server.ISO.ID != 4711 {
 		t.Errorf("unexpected ISO: %v", server.ISO)
 	}
+	if server.Datacenter == nil || server.Datacenter.ID != 1 {
+		t.Errorf("unexpected Datacenter: %v", server.Datacenter)
+	}
+	if !server.Locked {
+		t.Errorf("unexpected value for Locked: %v", server.Locked)
+	}
 }
 
 func TestServerFromSchemaNoTraffic(t *testing.T) {
 	data := []byte(`{
+		"public_net": {
+			"ipv4": {
+				"ip": "1.2.3.4",
+				"blocked": false,
+				"dns_ptr": "server01.example.com"
+			},
+			"ipv6": {
+				"ip": "2a01:4f8:1c11:3400::/64",
+				"blocked": false,
+				"dns_ptr": [
+					{
+						"ip": "2a01:4f8:1c11:3400::1/64",
+						"dns_ptr": "server01.example.com"
+					}
+				]
+			}
+		},
 		"outgoing_traffic": null,
 		"ingoing_traffic": null
 	}`)
@@ -247,11 +402,11 @@ func TestServerPublicNetFromSchema(t *testing.T) {
 			"dns_ptr": "server.example.com"
 		},
 		"ipv6": {
-        		"ip": "2a01:4f8:1c19:1403::/64",
-        		"blocked": false,
-        		"dns_ptr": []
-      		},
-      		"floating_ips": [4]
+			"ip": "2a01:4f8:1c19:1403::/64",
+			"blocked": false,
+			"dns_ptr": []
+		},
+		"floating_ips": [4]
 	}`)
 
 	var s schema.ServerPublicNet
@@ -260,10 +415,10 @@ func TestServerPublicNetFromSchema(t *testing.T) {
 	}
 	publicNet := ServerPublicNetFromSchema(s)
 
-	if publicNet.IPv4.IP != "1.2.3.4" {
+	if publicNet.IPv4.IP.String() != "1.2.3.4" {
 		t.Errorf("unexpected IPv4 IP: %v", publicNet.IPv4.IP)
 	}
-	if publicNet.IPv6.IP != "2a01:4f8:1c19:1403::/64" {
+	if publicNet.IPv6.Network.String() != "2a01:4f8:1c19:1403::/64" {
 		t.Errorf("unexpected IPv6 IP: %v", publicNet.IPv6.IP)
 	}
 	if len(publicNet.FloatingIPs) != 1 || publicNet.FloatingIPs[0].ID != 4 {
@@ -284,7 +439,7 @@ func TestServerPublicNetIPv4FromSchema(t *testing.T) {
 	}
 	ipv4 := ServerPublicNetIPv4FromSchema(s)
 
-	if ipv4.IP != "1.2.3.4" {
+	if ipv4.IP.String() != "1.2.3.4" {
 		t.Errorf("unexpected IP: %v", ipv4.IP)
 	}
 	if !ipv4.Blocked {
@@ -313,7 +468,7 @@ func TestServerPublicNetIPv6FromSchema(t *testing.T) {
 	}
 	ipv6 := ServerPublicNetIPv6FromSchema(s)
 
-	if ipv6.IP != "2a01:4f8:1c11:3400::/64" {
+	if ipv6.Network.String() != "2a01:4f8:1c11:3400::/64" {
 		t.Errorf("unexpected IP: %v", ipv6.IP)
 	}
 	if !ipv6.Blocked {
@@ -324,26 +479,6 @@ func TestServerPublicNetIPv6FromSchema(t *testing.T) {
 	}
 }
 
-func TestServerPublicNetIPv6DNSPtrFromSchema(t *testing.T) {
-	data := []byte(`{
-		"ip": "2a01:4f8:1c11:3400::1/64",
-		"dns_ptr": "server01.example.com"
-	}`)
-
-	var s schema.ServerPublicNetIPv6DNSPtr
-	if err := json.Unmarshal(data, &s); err != nil {
-		t.Fatal(err)
-	}
-	dnsPtr := ServerPublicNetIPv6DNSPtrFromSchema(s)
-
-	if dnsPtr.IP != "2a01:4f8:1c11:3400::1/64" {
-		t.Errorf("unexpected IP: %v", dnsPtr.IP)
-	}
-	if dnsPtr.DNSPtr != "server01.example.com" {
-		t.Errorf("unexpected DNS ptr: %v", dnsPtr.DNSPtr)
-	}
-}
-
 func TestServerTypeFromSchema(t *testing.T) {
 	data := []byte(`{
 		"id": 1,
@@ -478,8 +613,10 @@ func TestImageFromSchema(t *testing.T) {
 		"image_size": 2.3,
 		"disk_size": 10,
 		"created": "2016-01-30T23:55:01Z",
-		"created_from": "my_server1",
-		"created_from_id": 1,
+		"created_from": {
+			"id": 1,
+			"name": "my-server1"
+		},
 		"bound_to": 1,
 		"os_flavor": "ubuntu",
 		"os_version": "16.04",
@@ -516,6 +653,12 @@ func TestImageFromSchema(t *testing.T) {
 	if !image.Created.Equal(time.Date(2016, 1, 30, 23, 55, 1, 0, time.UTC)) {
 		t.Errorf("unexpected Created: %v", image.Created)
 	}
+	if image.CreatedFrom == nil || image.CreatedFrom.ID != 1 || image.CreatedFrom.Name != "my-server1" {
+		t.Errorf("unexpected CreatedFrom: %v", image.CreatedFrom)
+	}
+	if image.BoundTo == nil || image.BoundTo.ID != 1 {
+		t.Errorf("unexpected BoundTo: %v", image.BoundTo)
+	}
 	if image.OSVersion != "16.04" {
 		t.Errorf("unexpected OSVersion: %v", image.OSVersion)
 	}