1package hcloud 2 3import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "net" 10 "net/url" 11 "strconv" 12 "time" 13 14 "github.com/hetznercloud/hcloud-go/hcloud/schema" 15) 16 17// NetworkZone specifies a network zone. 18type NetworkZone string 19 20// List of available Network Zones. 21const ( 22 NetworkZoneEUCentral NetworkZone = "eu-central" 23) 24 25// NetworkSubnetType specifies a type of a subnet. 26type NetworkSubnetType string 27 28// List of available network subnet types. 29const ( 30 NetworkSubnetTypeCloud NetworkSubnetType = "cloud" 31 NetworkSubnetTypeServer NetworkSubnetType = "server" 32 NetworkSubnetTypeVSwitch NetworkSubnetType = "vswitch" 33) 34 35// Network represents a network in the Hetzner Cloud. 36type Network struct { 37 ID int 38 Name string 39 Created time.Time 40 IPRange *net.IPNet 41 Subnets []NetworkSubnet 42 Routes []NetworkRoute 43 Servers []*Server 44 Protection NetworkProtection 45 Labels map[string]string 46} 47 48// NetworkSubnet represents a subnet of a network in the Hetzner Cloud. 49type NetworkSubnet struct { 50 Type NetworkSubnetType 51 IPRange *net.IPNet 52 NetworkZone NetworkZone 53 Gateway net.IP 54 VSwitchID int 55} 56 57// NetworkRoute represents a route of a network. 58type NetworkRoute struct { 59 Destination *net.IPNet 60 Gateway net.IP 61} 62 63// NetworkProtection represents the protection level of a network. 64type NetworkProtection struct { 65 Delete bool 66} 67 68// NetworkClient is a client for the network API. 69type NetworkClient struct { 70 client *Client 71} 72 73// GetByID retrieves a network by its ID. If the network does not exist, nil is returned. 74func (c *NetworkClient) GetByID(ctx context.Context, id int) (*Network, *Response, error) { 75 req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/networks/%d", id), nil) 76 if err != nil { 77 return nil, nil, err 78 } 79 80 var body schema.NetworkGetResponse 81 resp, err := c.client.Do(req, &body) 82 if err != nil { 83 if IsError(err, ErrorCodeNotFound) { 84 return nil, resp, nil 85 } 86 return nil, nil, err 87 } 88 return NetworkFromSchema(body.Network), resp, nil 89} 90 91// GetByName retrieves a network by its name. If the network does not exist, nil is returned. 92func (c *NetworkClient) GetByName(ctx context.Context, name string) (*Network, *Response, error) { 93 if name == "" { 94 return nil, nil, nil 95 } 96 Networks, response, err := c.List(ctx, NetworkListOpts{Name: name}) 97 if len(Networks) == 0 { 98 return nil, response, err 99 } 100 return Networks[0], response, err 101} 102 103// Get retrieves a network by its ID if the input can be parsed as an integer, otherwise it 104// retrieves a network by its name. If the network does not exist, nil is returned. 105func (c *NetworkClient) Get(ctx context.Context, idOrName string) (*Network, *Response, error) { 106 if id, err := strconv.Atoi(idOrName); err == nil { 107 return c.GetByID(ctx, int(id)) 108 } 109 return c.GetByName(ctx, idOrName) 110} 111 112// NetworkListOpts specifies options for listing networks. 113type NetworkListOpts struct { 114 ListOpts 115 Name string 116} 117 118func (l NetworkListOpts) values() url.Values { 119 vals := l.ListOpts.values() 120 if l.Name != "" { 121 vals.Add("name", l.Name) 122 } 123 return vals 124} 125 126// List returns a list of networks for a specific page. 127// 128// Please note that filters specified in opts are not taken into account 129// when their value corresponds to their zero value or when they are empty. 130func (c *NetworkClient) List(ctx context.Context, opts NetworkListOpts) ([]*Network, *Response, error) { 131 path := "/networks?" + opts.values().Encode() 132 req, err := c.client.NewRequest(ctx, "GET", path, nil) 133 if err != nil { 134 return nil, nil, err 135 } 136 137 var body schema.NetworkListResponse 138 resp, err := c.client.Do(req, &body) 139 if err != nil { 140 return nil, nil, err 141 } 142 Networks := make([]*Network, 0, len(body.Networks)) 143 for _, s := range body.Networks { 144 Networks = append(Networks, NetworkFromSchema(s)) 145 } 146 return Networks, resp, nil 147} 148 149// All returns all networks. 150func (c *NetworkClient) All(ctx context.Context) ([]*Network, error) { 151 return c.AllWithOpts(ctx, NetworkListOpts{ListOpts: ListOpts{PerPage: 50}}) 152} 153 154// AllWithOpts returns all networks for the given options. 155func (c *NetworkClient) AllWithOpts(ctx context.Context, opts NetworkListOpts) ([]*Network, error) { 156 var allNetworks []*Network 157 158 err := c.client.all(func(page int) (*Response, error) { 159 opts.Page = page 160 Networks, resp, err := c.List(ctx, opts) 161 if err != nil { 162 return resp, err 163 } 164 allNetworks = append(allNetworks, Networks...) 165 return resp, nil 166 }) 167 if err != nil { 168 return nil, err 169 } 170 171 return allNetworks, nil 172} 173 174// Delete deletes a network. 175func (c *NetworkClient) Delete(ctx context.Context, network *Network) (*Response, error) { 176 req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/networks/%d", network.ID), nil) 177 if err != nil { 178 return nil, err 179 } 180 return c.client.Do(req, nil) 181} 182 183// NetworkUpdateOpts specifies options for updating a network. 184type NetworkUpdateOpts struct { 185 Name string 186 Labels map[string]string 187} 188 189// Update updates a network. 190func (c *NetworkClient) Update(ctx context.Context, network *Network, opts NetworkUpdateOpts) (*Network, *Response, error) { 191 reqBody := schema.NetworkUpdateRequest{ 192 Name: opts.Name, 193 } 194 if opts.Labels != nil { 195 reqBody.Labels = &opts.Labels 196 } 197 reqBodyData, err := json.Marshal(reqBody) 198 if err != nil { 199 return nil, nil, err 200 } 201 202 path := fmt.Sprintf("/networks/%d", network.ID) 203 req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) 204 if err != nil { 205 return nil, nil, err 206 } 207 208 respBody := schema.NetworkUpdateResponse{} 209 resp, err := c.client.Do(req, &respBody) 210 if err != nil { 211 return nil, resp, err 212 } 213 return NetworkFromSchema(respBody.Network), resp, nil 214} 215 216// NetworkCreateOpts specifies options for creating a new network. 217type NetworkCreateOpts struct { 218 Name string 219 IPRange *net.IPNet 220 Subnets []NetworkSubnet 221 Routes []NetworkRoute 222 Labels map[string]string 223} 224 225// Validate checks if options are valid. 226func (o NetworkCreateOpts) Validate() error { 227 if o.Name == "" { 228 return errors.New("missing name") 229 } 230 if o.IPRange == nil || o.IPRange.String() == "" { 231 return errors.New("missing IP range") 232 } 233 return nil 234} 235 236// Create creates a new network. 237func (c *NetworkClient) Create(ctx context.Context, opts NetworkCreateOpts) (*Network, *Response, error) { 238 if err := opts.Validate(); err != nil { 239 return nil, nil, err 240 } 241 reqBody := schema.NetworkCreateRequest{ 242 Name: opts.Name, 243 IPRange: opts.IPRange.String(), 244 } 245 for _, subnet := range opts.Subnets { 246 s := schema.NetworkSubnet{ 247 Type: string(subnet.Type), 248 IPRange: subnet.IPRange.String(), 249 NetworkZone: string(subnet.NetworkZone), 250 } 251 if subnet.VSwitchID != 0 { 252 s.VSwitchID = subnet.VSwitchID 253 } 254 reqBody.Subnets = append(reqBody.Subnets, s) 255 } 256 for _, route := range opts.Routes { 257 reqBody.Routes = append(reqBody.Routes, schema.NetworkRoute{ 258 Destination: route.Destination.String(), 259 Gateway: route.Gateway.String(), 260 }) 261 } 262 if opts.Labels != nil { 263 reqBody.Labels = &opts.Labels 264 } 265 reqBodyData, err := json.Marshal(reqBody) 266 if err != nil { 267 return nil, nil, err 268 } 269 req, err := c.client.NewRequest(ctx, "POST", "/networks", bytes.NewReader(reqBodyData)) 270 if err != nil { 271 return nil, nil, err 272 } 273 274 respBody := schema.NetworkCreateResponse{} 275 resp, err := c.client.Do(req, &respBody) 276 if err != nil { 277 return nil, resp, err 278 } 279 return NetworkFromSchema(respBody.Network), resp, nil 280} 281 282// NetworkChangeIPRangeOpts specifies options for changing the IP range of a network. 283type NetworkChangeIPRangeOpts struct { 284 IPRange *net.IPNet 285} 286 287// ChangeIPRange changes the IP range of a network. 288func (c *NetworkClient) ChangeIPRange(ctx context.Context, network *Network, opts NetworkChangeIPRangeOpts) (*Action, *Response, error) { 289 reqBody := schema.NetworkActionChangeIPRangeRequest{ 290 IPRange: opts.IPRange.String(), 291 } 292 reqBodyData, err := json.Marshal(reqBody) 293 if err != nil { 294 return nil, nil, err 295 } 296 297 path := fmt.Sprintf("/networks/%d/actions/change_ip_range", network.ID) 298 req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) 299 if err != nil { 300 return nil, nil, err 301 } 302 303 respBody := schema.NetworkActionChangeIPRangeResponse{} 304 resp, err := c.client.Do(req, &respBody) 305 if err != nil { 306 return nil, resp, err 307 } 308 return ActionFromSchema(respBody.Action), resp, nil 309} 310 311// NetworkAddSubnetOpts specifies options for adding a subnet to a network. 312type NetworkAddSubnetOpts struct { 313 Subnet NetworkSubnet 314} 315 316// AddSubnet adds a subnet to a network. 317func (c *NetworkClient) AddSubnet(ctx context.Context, network *Network, opts NetworkAddSubnetOpts) (*Action, *Response, error) { 318 reqBody := schema.NetworkActionAddSubnetRequest{ 319 Type: string(opts.Subnet.Type), 320 NetworkZone: string(opts.Subnet.NetworkZone), 321 } 322 if opts.Subnet.IPRange != nil { 323 reqBody.IPRange = opts.Subnet.IPRange.String() 324 } 325 if opts.Subnet.VSwitchID != 0 { 326 reqBody.VSwitchID = opts.Subnet.VSwitchID 327 } 328 reqBodyData, err := json.Marshal(reqBody) 329 if err != nil { 330 return nil, nil, err 331 } 332 333 path := fmt.Sprintf("/networks/%d/actions/add_subnet", network.ID) 334 req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) 335 if err != nil { 336 return nil, nil, err 337 } 338 339 respBody := schema.NetworkActionAddSubnetResponse{} 340 resp, err := c.client.Do(req, &respBody) 341 if err != nil { 342 return nil, resp, err 343 } 344 return ActionFromSchema(respBody.Action), resp, nil 345} 346 347// NetworkDeleteSubnetOpts specifies options for deleting a subnet from a network. 348type NetworkDeleteSubnetOpts struct { 349 Subnet NetworkSubnet 350} 351 352// DeleteSubnet deletes a subnet from a network. 353func (c *NetworkClient) DeleteSubnet(ctx context.Context, network *Network, opts NetworkDeleteSubnetOpts) (*Action, *Response, error) { 354 reqBody := schema.NetworkActionDeleteSubnetRequest{ 355 IPRange: opts.Subnet.IPRange.String(), 356 } 357 reqBodyData, err := json.Marshal(reqBody) 358 if err != nil { 359 return nil, nil, err 360 } 361 362 path := fmt.Sprintf("/networks/%d/actions/delete_subnet", network.ID) 363 req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) 364 if err != nil { 365 return nil, nil, err 366 } 367 368 respBody := schema.NetworkActionDeleteSubnetResponse{} 369 resp, err := c.client.Do(req, &respBody) 370 if err != nil { 371 return nil, resp, err 372 } 373 return ActionFromSchema(respBody.Action), resp, nil 374} 375 376// NetworkAddRouteOpts specifies options for adding a route to a network. 377type NetworkAddRouteOpts struct { 378 Route NetworkRoute 379} 380 381// AddRoute adds a route to a network. 382func (c *NetworkClient) AddRoute(ctx context.Context, network *Network, opts NetworkAddRouteOpts) (*Action, *Response, error) { 383 reqBody := schema.NetworkActionAddRouteRequest{ 384 Destination: opts.Route.Destination.String(), 385 Gateway: opts.Route.Gateway.String(), 386 } 387 reqBodyData, err := json.Marshal(reqBody) 388 if err != nil { 389 return nil, nil, err 390 } 391 392 path := fmt.Sprintf("/networks/%d/actions/add_route", network.ID) 393 req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) 394 if err != nil { 395 return nil, nil, err 396 } 397 398 respBody := schema.NetworkActionAddSubnetResponse{} 399 resp, err := c.client.Do(req, &respBody) 400 if err != nil { 401 return nil, resp, err 402 } 403 return ActionFromSchema(respBody.Action), resp, nil 404} 405 406// NetworkDeleteRouteOpts specifies options for deleting a route from a network. 407type NetworkDeleteRouteOpts struct { 408 Route NetworkRoute 409} 410 411// DeleteRoute deletes a route from a network. 412func (c *NetworkClient) DeleteRoute(ctx context.Context, network *Network, opts NetworkDeleteRouteOpts) (*Action, *Response, error) { 413 reqBody := schema.NetworkActionDeleteRouteRequest{ 414 Destination: opts.Route.Destination.String(), 415 Gateway: opts.Route.Gateway.String(), 416 } 417 reqBodyData, err := json.Marshal(reqBody) 418 if err != nil { 419 return nil, nil, err 420 } 421 422 path := fmt.Sprintf("/networks/%d/actions/delete_route", network.ID) 423 req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) 424 if err != nil { 425 return nil, nil, err 426 } 427 428 respBody := schema.NetworkActionDeleteSubnetResponse{} 429 resp, err := c.client.Do(req, &respBody) 430 if err != nil { 431 return nil, resp, err 432 } 433 return ActionFromSchema(respBody.Action), resp, nil 434} 435 436// NetworkChangeProtectionOpts specifies options for changing the resource protection level of a network. 437type NetworkChangeProtectionOpts struct { 438 Delete *bool 439} 440 441// ChangeProtection changes the resource protection level of a network. 442func (c *NetworkClient) ChangeProtection(ctx context.Context, network *Network, opts NetworkChangeProtectionOpts) (*Action, *Response, error) { 443 reqBody := schema.NetworkActionChangeProtectionRequest{ 444 Delete: opts.Delete, 445 } 446 reqBodyData, err := json.Marshal(reqBody) 447 if err != nil { 448 return nil, nil, err 449 } 450 451 path := fmt.Sprintf("/networks/%d/actions/change_protection", network.ID) 452 req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) 453 if err != nil { 454 return nil, nil, err 455 } 456 457 respBody := schema.NetworkActionChangeProtectionResponse{} 458 resp, err := c.client.Do(req, &respBody) 459 if err != nil { 460 return nil, resp, err 461 } 462 return ActionFromSchema(respBody.Action), resp, err 463} 464