Repository: hetznercloud/hcloud-go
Pull request #157 (2020-11-03 09:44:50)

Support Subnetwork Type vswitch


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 858c72b..146cf03 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,9 +1,10 @@
 # Changes
 
-## master
+## v1.23.0
 
 * Add missing constants for all resource specific error codes
 * Expose metrics for Servers and Load Balancers
+* Add support for vSwitch Subnetworks
 
 ## v1.22.0
 
diff --git a/hcloud/error.go b/hcloud/error.go
index eabd0c7..efc310b 100644
--- a/hcloud/error.go
+++ b/hcloud/error.go
@@ -42,8 +42,9 @@ const (
 	ErrorCodeLoadBalancerNotAttachedToNetwork ErrorCode = "load_balancer_not_attached_to_network" // The Load Balancer is not attached to a network
 
 	// Network related error codes
-	ErrorCodeIPNotAvailable    ErrorCode = "ip_not_available"    // The provided Network IP is not available
-	ErrorCodeNoSubnetAvailable ErrorCode = "no_subnet_available" // No Subnet or IP is available for the Load Balancer/Server within the network
+	ErrorCodeIPNotAvailable     ErrorCode = "ip_not_available"        // The provided Network IP is not available
+	ErrorCodeNoSubnetAvailable  ErrorCode = "no_subnet_available"     // No Subnet or IP is available for the Load Balancer/Server within the network
+	ErrorCodeVSwitchAlreadyUsed ErrorCode = "vswitch_id_already_used" // The given Robot vSwitch ID is already registered in another network
 
 	// Volume related error codes
 	ErrorCodeNoSpaceLeftInLocation ErrorCode = "no_space_left_in_location" // There is no volume space left in the given location
diff --git a/hcloud/hcloud.go b/hcloud/hcloud.go
index 1adf029..ec1dfa8 100644
--- a/hcloud/hcloud.go
+++ b/hcloud/hcloud.go
@@ -2,4 +2,4 @@
 package hcloud
 
 // Version is the library's version following Semantic Versioning.
-const Version = "1.22.0"
+const Version = "1.23.0"
diff --git a/hcloud/network.go b/hcloud/network.go
index 4b72679..8657592 100644
--- a/hcloud/network.go
+++ b/hcloud/network.go
@@ -27,8 +27,9 @@ type NetworkSubnetType string
 
 // List of available network subnet types.
 const (
-	NetworkSubnetTypeCloud  NetworkSubnetType = "cloud"
-	NetworkSubnetTypeServer NetworkSubnetType = "server"
+	NetworkSubnetTypeCloud   NetworkSubnetType = "cloud"
+	NetworkSubnetTypeServer  NetworkSubnetType = "server"
+	NetworkSubnetTypeVSwitch NetworkSubnetType = "vswitch"
 )
 
 // Network represents a network in the Hetzner Cloud.
@@ -50,6 +51,7 @@ type NetworkSubnet struct {
 	IPRange     *net.IPNet
 	NetworkZone NetworkZone
 	Gateway     net.IP
+	VSwitchID   int
 }
 
 // NetworkRoute represents a route of a network.
@@ -241,11 +243,15 @@ func (c *NetworkClient) Create(ctx context.Context, opts NetworkCreateOpts) (*Ne
 		IPRange: opts.IPRange.String(),
 	}
 	for _, subnet := range opts.Subnets {
-		reqBody.Subnets = append(reqBody.Subnets, schema.NetworkSubnet{
+		s := schema.NetworkSubnet{
 			Type:        string(subnet.Type),
 			IPRange:     subnet.IPRange.String(),
 			NetworkZone: string(subnet.NetworkZone),
-		})
+		}
+		if subnet.VSwitchID != 0 {
+			s.VSwitchID = subnet.VSwitchID
+		}
+		reqBody.Subnets = append(reqBody.Subnets, s)
 	}
 	for _, route := range opts.Routes {
 		reqBody.Routes = append(reqBody.Routes, schema.NetworkRoute{
@@ -316,6 +322,9 @@ func (c *NetworkClient) AddSubnet(ctx context.Context, network *Network, opts Ne
 	if opts.Subnet.IPRange != nil {
 		reqBody.IPRange = opts.Subnet.IPRange.String()
 	}
+	if opts.Subnet.VSwitchID != 0 {
+		reqBody.VSwitchID = opts.Subnet.VSwitchID
+	}
 	reqBodyData, err := json.Marshal(reqBody)
 	if err != nil {
 		return nil, nil, err
diff --git a/hcloud/schema.go b/hcloud/schema.go
index c5f5bf0..6a04df0 100644
--- a/hcloud/schema.go
+++ b/hcloud/schema.go
@@ -372,6 +372,7 @@ func NetworkSubnetFromSchema(s schema.NetworkSubnet) NetworkSubnet {
 		Type:        NetworkSubnetType(s.Type),
 		NetworkZone: NetworkZone(s.NetworkZone),
 		Gateway:     net.ParseIP(s.Gateway),
+		VSwitchID:   s.VSwitchID,
 	}
 	_, sn.IPRange, _ = net.ParseCIDR(s.IPRange)
 	return sn
diff --git a/hcloud/schema/network.go b/hcloud/schema/network.go
index e1c97e4..87e184d 100644
--- a/hcloud/schema/network.go
+++ b/hcloud/schema/network.go
@@ -20,7 +20,8 @@ type NetworkSubnet struct {
 	Type        string `json:"type"`
 	IPRange     string `json:"ip_range"`
 	NetworkZone string `json:"network_zone"`
-	Gateway     string `json:"gateway"`
+	Gateway     string `json:"gateway,omitempty"`
+	VSwitchID   int    `json:"vswitch_id,omitempty"`
 }
 
 // NetworkRoute represents a route of a network.
@@ -91,6 +92,7 @@ type NetworkActionAddSubnetRequest struct {
 	IPRange     string `json:"ip_range,omitempty"`
 	NetworkZone string `json:"network_zone"`
 	Gateway     string `json:"gateway"`
+	VSwitchID   int    `json:"vswitch_id,omitempty"`
 }
 
 // NetworkActionAddSubnetResponse defines the schema of the response when


Test patch:
diff --git a/hcloud/network_test.go b/hcloud/network_test.go
index 98e88a2..9f60639 100644
--- a/hcloud/network_test.go
+++ b/hcloud/network_test.go
@@ -460,6 +460,53 @@ func TestNetworkClientAddSubnet(t *testing.T) {
 			t.Errorf("unexpected action ID: %d", action.ID)
 		}
 	})
+
+	t.Run("type vswitch with ip range", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/networks/1/actions/add_subnet", func(w http.ResponseWriter, r *http.Request) {
+			var reqBody schema.NetworkActionAddSubnetRequest
+			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+				t.Fatal(err)
+			}
+			if reqBody.Type != "vswitch" {
+				t.Errorf("unexpected Type: %v", reqBody.Type)
+			}
+			if reqBody.IPRange != "10.0.1.0/24" {
+				t.Errorf("unexpected IPRange: %v", reqBody.IPRange)
+			}
+			if reqBody.NetworkZone != "eu-central" {
+				t.Errorf("unexpected NetworkZone: %v", reqBody.NetworkZone)
+			}
+			if reqBody.VSwitchID != 123 {
+				t.Errorf("unexpected VSwitchID: %v", reqBody.VSwitchID)
+			}
+			json.NewEncoder(w).Encode(schema.NetworkActionAddSubnetResponse{
+				Action: schema.Action{
+					ID: 1,
+				},
+			})
+		})
+
+		ctx := context.Background()
+		_, ipRange, _ := net.ParseCIDR("10.0.1.0/24")
+		opts := NetworkAddSubnetOpts{
+			Subnet: NetworkSubnet{
+				Type:        NetworkSubnetTypeVSwitch,
+				IPRange:     ipRange,
+				NetworkZone: NetworkZoneEUCentral,
+				VSwitchID:   123,
+			},
+		}
+		action, _, err := env.Client.Network.AddSubnet(ctx, &Network{ID: 1}, opts)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if action.ID != 1 {
+			t.Errorf("unexpected action ID: %d", action.ID)
+		}
+	})
 }
 
 func TestNetworkClientDeleteSubnet(t *testing.T) {
diff --git a/hcloud/schema_test.go b/hcloud/schema_test.go
index 07bf82b..05de0fc 100644
--- a/hcloud/schema_test.go
+++ b/hcloud/schema_test.go
@@ -1132,6 +1132,39 @@ func TestNetworkSubnetFromSchema(t *testing.T) {
 		if networkSubnet.Gateway.String() != "10.0.0.1" {
 			t.Errorf("unexpected Gateway: %v", networkSubnet.Gateway)
 		}
+		if networkSubnet.VSwitchID != 0 {
+			t.Errorf("unexpected VSwitchID: %v", networkSubnet.VSwitchID)
+		}
+	})
+
+	t.Run("type vswitch", func(t *testing.T) {
+		data := []byte(`{
+			"type": "vswitch",
+			"ip_range": "10.0.1.0/24",
+			"network_zone": "eu-central",
+			"gateway": "10.0.0.1",
+			"vswitch_id": 123
+		}`)
+		var s schema.NetworkSubnet
+		if err := json.Unmarshal(data, &s); err != nil {
+			t.Fatal(err)
+		}
+		networkSubnet := NetworkSubnetFromSchema(s)
+		if networkSubnet.NetworkZone != "eu-central" {
+			t.Errorf("unexpected NetworkZone: %v", networkSubnet.NetworkZone)
+		}
+		if networkSubnet.Type != "vswitch" {
+			t.Errorf("unexpected Type: %v", networkSubnet.Type)
+		}
+		if networkSubnet.IPRange.String() != "10.0.1.0/24" {
+			t.Errorf("unexpected IPRange: %v", networkSubnet.IPRange)
+		}
+		if networkSubnet.Gateway.String() != "10.0.0.1" {
+			t.Errorf("unexpected Gateway: %v", networkSubnet.Gateway)
+		}
+		if networkSubnet.VSwitchID != 123 {
+			t.Errorf("unexpected VSwitchID: %v", networkSubnet.VSwitchID)
+		}
 	})
 }