1 package linodego
2 3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net"
8 "time"
9 10 "github.com/linode/linodego/internal/parseabletime"
11 )
12 13 type InterfaceGeneration string
14 15 const (
16 GenerationLegacyConfig InterfaceGeneration = "legacy_config"
17 GenerationLinode InterfaceGeneration = "linode"
18 )
19 20 /*
21 * https://techdocs.akamai.com/linode-api/reference/post-linode-instance
22 */
23 24 // InstanceStatus constants start with Instance and include Linode API Instance Status values
25 type InstanceStatus string
26 27 // InstanceStatus constants reflect the current status of an Instance
28 const (
29 InstanceBooting InstanceStatus = "booting"
30 InstanceRunning InstanceStatus = "running"
31 InstanceOffline InstanceStatus = "offline"
32 InstanceShuttingDown InstanceStatus = "shutting_down"
33 InstanceRebooting InstanceStatus = "rebooting"
34 InstanceProvisioning InstanceStatus = "provisioning"
35 InstanceDeleting InstanceStatus = "deleting"
36 InstanceMigrating InstanceStatus = "migrating"
37 InstanceRebuilding InstanceStatus = "rebuilding"
38 InstanceCloning InstanceStatus = "cloning"
39 InstanceRestoring InstanceStatus = "restoring"
40 InstanceResizing InstanceStatus = "resizing"
41 )
42 43 type InstanceMigrationType string
44 45 const (
46 WarmMigration InstanceMigrationType = "warm"
47 ColdMigration InstanceMigrationType = "cold"
48 )
49 50 // Instance represents a linode object
51 type Instance struct {
52 ID int `json:"id"`
53 Created *time.Time `json:"-"`
54 Updated *time.Time `json:"-"`
55 Region string `json:"region"`
56 Alerts *InstanceAlert `json:"alerts"`
57 Backups *InstanceBackup `json:"backups"`
58 Image string `json:"image"`
59 Group string `json:"group"`
60 IPv4 []*net.IP `json:"ipv4"`
61 IPv6 string `json:"ipv6"`
62 Label string `json:"label"`
63 Type string `json:"type"`
64 Status InstanceStatus `json:"status"`
65 HasUserData bool `json:"has_user_data"`
66 Hypervisor string `json:"hypervisor"`
67 HostUUID string `json:"host_uuid"`
68 Specs *InstanceSpec `json:"specs"`
69 WatchdogEnabled bool `json:"watchdog_enabled"`
70 Tags []string `json:"tags"`
71 PlacementGroup *InstancePlacementGroup `json:"placement_group"`
72 73 // NOTE: Disk encryption may not currently be available to all users.
74 DiskEncryption InstanceDiskEncryption `json:"disk_encryption"`
75 76 LKEClusterID int `json:"lke_cluster_id"`
77 Capabilities []string `json:"capabilities"`
78 79 // Note: Linode interfaces may not currently be available to all users.
80 InterfaceGeneration InterfaceGeneration `json:"interface_generation"`
81 82 // NOTE: MaintenancePolicy can only be used with v4beta.
83 MaintenancePolicy string `json:"maintenance_policy"`
84 85 // NOTE: Locks can only be used with v4beta.
86 Locks []LockType `json:"locks"`
87 }
88 89 // InstanceSpec represents a linode spec
90 type InstanceSpec struct {
91 Disk int `json:"disk"`
92 Memory int `json:"memory"`
93 VCPUs int `json:"vcpus"`
94 Transfer int `json:"transfer"`
95 GPUs int `json:"gpus"`
96 AcceleratedDevices int `json:"accelerated_devices"`
97 }
98 99 // InstanceAlert represents a metric alert
100 type InstanceAlert struct {
101 CPU int `json:"cpu"`
102 IO int `json:"io"`
103 NetworkIn int `json:"network_in"`
104 NetworkOut int `json:"network_out"`
105 TransferQuota int `json:"transfer_quota"`
106 }
107 108 // InstanceBackup represents backup settings for an instance
109 type InstanceBackup struct {
110 Available bool `json:"available,omitempty"` // read-only
111 Enabled bool `json:"enabled,omitempty"` // read-only
112 LastSuccessful *time.Time `json:"-"` // read-only
113 Schedule struct {
114 Day string `json:"day,omitempty"`
115 Window string `json:"window,omitempty"`
116 } `json:"schedule"`
117 }
118 119 type InstanceDiskEncryption string
120 121 const (
122 InstanceDiskEncryptionEnabled InstanceDiskEncryption = "enabled"
123 InstanceDiskEncryptionDisabled InstanceDiskEncryption = "disabled"
124 )
125 126 // InstanceTransfer pool stats for a Linode Instance during the current billing month
127 type InstanceTransfer struct {
128 // Bytes of transfer this instance has consumed
129 Used int `json:"used"`
130 131 // GB of billable transfer this instance has consumed
132 Billable int `json:"billable"`
133 134 // GB of transfer this instance adds to the Transfer pool
135 Quota int `json:"quota"`
136 }
137 138 // Deprecated: use MonthlyInstanceTransferStatsV2 for new implementations
139 //
140 // MonthlyInstanceTransferStats pool stats for a Linode Instance network transfer statistics for a specific month
141 type MonthlyInstanceTransferStats struct {
142 // The amount of inbound public network traffic received by this Linode, in bytes, for a specific year/month.
143 BytesIn int `json:"bytes_in"`
144 145 // The amount of outbound public network traffic sent by this Linode, in bytes, for a specific year/month.
146 BytesOut int `json:"bytes_out"`
147 148 // The total amount of public network traffic sent and received by this Linode, in bytes, for a specific year/month.
149 BytesTotal int `json:"bytes_total"`
150 }
151 152 // MonthlyInstanceTransferStatsV2 pool stats for a Linode Instance network transfer statistics for a specific month
153 type MonthlyInstanceTransferStatsV2 struct {
154 // The amount of inbound public network traffic received by this Linode, in bytes, for a specific year/month.
155 BytesIn uint64 `json:"bytes_in"`
156 157 // The amount of outbound public network traffic sent by this Linode, in bytes, for a specific year/month.
158 BytesOut uint64 `json:"bytes_out"`
159 160 // The total amount of public network traffic sent and received by this Linode, in bytes, for a specific year/month.
161 BytesTotal uint64 `json:"bytes_total"`
162 }
163 164 // InstancePlacementGroup represents information about the placement group
165 // this Linode is a part of.
166 type InstancePlacementGroup struct {
167 ID int `json:"id"`
168 Label string `json:"label"`
169 PlacementGroupType PlacementGroupType `json:"placement_group_type"`
170 PlacementGroupPolicy PlacementGroupPolicy `json:"placement_group_policy"`
171 MigratingTo *int `json:"migrating_to"` // read-only
172 }
173 174 // InstanceMetadataOptions specifies various Instance creation fields
175 // that relate to the Linode Metadata service.
176 type InstanceMetadataOptions struct {
177 // UserData expects a Base64-encoded string
178 UserData string `json:"user_data,omitempty"`
179 }
180 181 // InstancePasswordResetOptions specifies the new password for the Linode
182 type InstancePasswordResetOptions struct {
183 RootPass string `json:"root_pass"`
184 }
185 186 // InstanceCreateOptions require only Region and Type
187 type InstanceCreateOptions struct {
188 Region string `json:"region"`
189 Type string `json:"type"`
190 Label string `json:"label,omitempty"`
191 RootPass string `json:"root_pass,omitempty"`
192 AuthorizedKeys []string `json:"authorized_keys,omitempty"`
193 AuthorizedUsers []string `json:"authorized_users,omitempty"`
194 StackScriptID int `json:"stackscript_id,omitempty"`
195 StackScriptData map[string]string `json:"stackscript_data,omitempty"`
196 BackupID int `json:"backup_id,omitempty"`
197 Image string `json:"image,omitempty"`
198 BackupsEnabled bool `json:"backups_enabled,omitempty"`
199 PrivateIP bool `json:"private_ip,omitempty"`
200 NetworkHelper *bool `json:"network_helper,omitempty"`
201 Tags []string `json:"tags,omitempty"`
202 Metadata *InstanceMetadataOptions `json:"metadata,omitempty"`
203 FirewallID int `json:"firewall_id,omitempty"`
204 InterfaceGeneration InterfaceGeneration `json:"interface_generation,omitempty"`
205 206 // Linode Interfaces to create the new instance with.
207 // Conflicts with Interfaces.
208 // NOTE: Linode Interfaces may not currently be available to all users.
209 LinodeInterfaces []LinodeInterfaceCreateOptions `json:"-"`
210 211 // Legacy (config) Interfaces to create the new instance with.
212 // Conflicts with LinodeInterfaces.
213 Interfaces []InstanceConfigInterfaceCreateOptions `json:"-"`
214 215 // NOTE: Disk encryption may not currently be available to all users.
216 DiskEncryption InstanceDiskEncryption `json:"disk_encryption,omitempty"`
217 218 PlacementGroup *InstanceCreatePlacementGroupOptions `json:"placement_group,omitempty"`
219 220 // Creation fields that need to be set explicitly false, "", or 0 use pointers
221 SwapSize *int `json:"swap_size,omitempty"`
222 Booted *bool `json:"booted,omitempty"`
223 224 // Deprecated: group is a deprecated property denoting a group label for the Linode.
225 Group string `json:"group,omitempty"`
226 227 IPv4 []string `json:"ipv4,omitempty"`
228 229 // NOTE: MaintenancePolicy can only be used with v4beta.
230 MaintenancePolicy *string `json:"maintenance_policy,omitempty"`
231 }
232 233 // InstanceCreatePlacementGroupOptions represents the placement group
234 // to create this Linode under.
235 type InstanceCreatePlacementGroupOptions struct {
236 ID int `json:"id"`
237 CompliantOnly *bool `json:"compliant_only,omitempty"`
238 }
239 240 // InstanceUpdateOptions is an options struct used when Updating an Instance
241 type InstanceUpdateOptions struct {
242 Label string `json:"label,omitempty"`
243 Backups *InstanceBackup `json:"backups,omitempty"`
244 Alerts *InstanceAlert `json:"alerts,omitempty"`
245 WatchdogEnabled *bool `json:"watchdog_enabled,omitempty"`
246 Tags *[]string `json:"tags,omitempty"`
247 248 // Deprecated: group is a deprecated property denoting a group label for the Linode.
249 Group *string `json:"group,omitempty"`
250 251 // NOTE: MaintenancePolicy can only be used with v4beta.
252 MaintenancePolicy *string `json:"maintenance_policy,omitempty"`
253 }
254 255 // MarshalJSON contains logic necessary to populate the `interfaces` field of
256 // InstanceCreateOptions depending on whether Interfaces or LinodeInterfaces
257 // is specified.
258 func (i InstanceCreateOptions) MarshalJSON() ([]byte, error) {
259 type Mask InstanceCreateOptions
260 261 resultData := struct {
262 *Mask
263 264 Interfaces any `json:"interfaces,omitempty"`
265 }{
266 Mask: (*Mask)(&i),
267 Interfaces: nil,
268 }
269 270 if i.Interfaces != nil && i.LinodeInterfaces != nil {
271 return nil, fmt.Errorf("fields Interfaces and LinodeInterfaces cannot be specified together")
272 }
273 274 if i.Interfaces != nil {
275 resultData.Interfaces = i.Interfaces
276 }
277 278 if i.LinodeInterfaces != nil {
279 resultData.Interfaces = i.LinodeInterfaces
280 }
281 282 return json.Marshal(resultData)
283 }
284 285 // UnmarshalJSON contains logic necessary to populate the Interfaces field
286 // depending on the value of interface_generation.
287 func (i *InstanceCreateOptions) UnmarshalJSON(b []byte) error {
288 type Mask InstanceCreateOptions
289 290 p := struct {
291 *Mask
292 293 GenericInterfaces any `json:"interfaces,omitempty"`
294 }{
295 Mask: (*Mask)(i),
296 }
297 298 if err := json.Unmarshal(b, &p); err != nil {
299 return err
300 }
301 302 if p.GenericInterfaces == nil {
303 // No interfaces were given - nothing to do here.
304 return nil
305 }
306 307 if i.InterfaceGeneration == GenerationLinode {
308 data := struct {
309 Interfaces []LinodeInterfaceCreateOptions `json:"interfaces"`
310 }{}
311 312 err := json.Unmarshal(b, &data)
313 i.LinodeInterfaces = data.Interfaces
314 315 return err
316 }
317 318 if i.InterfaceGeneration == GenerationLegacyConfig {
319 data := struct {
320 Interfaces []InstanceConfigInterfaceCreateOptions `json:"interfaces"`
321 }{}
322 323 err := json.Unmarshal(b, &data)
324 i.Interfaces = data.Interfaces
325 326 return err
327 }
328 329 return fmt.Errorf("cannot unmarshal interfaces without valid value for interface_generation")
330 }
331 332 // UnmarshalJSON implements the json.Unmarshaler interface
333 func (i *Instance) UnmarshalJSON(b []byte) error {
334 type Mask Instance
335 336 p := struct {
337 *Mask
338 339 Created *parseabletime.ParseableTime `json:"created"`
340 Updated *parseabletime.ParseableTime `json:"updated"`
341 }{
342 Mask: (*Mask)(i),
343 }
344 345 if err := json.Unmarshal(b, &p); err != nil {
346 return err
347 }
348 349 i.Created = (*time.Time)(p.Created)
350 i.Updated = (*time.Time)(p.Updated)
351 352 return nil
353 }
354 355 // UnmarshalJSON implements the json.Unmarshaler interface
356 func (backup *InstanceBackup) UnmarshalJSON(b []byte) error {
357 type Mask InstanceBackup
358 359 p := struct {
360 *Mask
361 362 LastSuccessful *parseabletime.ParseableTime `json:"last_successful"`
363 }{
364 Mask: (*Mask)(backup),
365 }
366 367 if err := json.Unmarshal(b, &p); err != nil {
368 return err
369 }
370 371 backup.LastSuccessful = (*time.Time)(p.LastSuccessful)
372 373 return nil
374 }
375 376 // GetUpdateOptions converts an Instance to InstanceUpdateOptions for use in UpdateInstance
377 func (i *Instance) GetUpdateOptions() InstanceUpdateOptions {
378 return InstanceUpdateOptions{
379 Label: i.Label,
380 Group: &i.Group,
381 Backups: i.Backups,
382 Alerts: i.Alerts,
383 WatchdogEnabled: &i.WatchdogEnabled,
384 Tags: &i.Tags,
385 MaintenancePolicy: &i.MaintenancePolicy,
386 }
387 }
388 389 // InstanceCloneOptions is an options struct sent when Cloning an Instance
390 type InstanceCloneOptions struct {
391 Region string `json:"region,omitempty"`
392 Type string `json:"type,omitempty"`
393 394 // LinodeID is an optional existing instance to use as the target of the clone
395 LinodeID int `json:"linode_id,omitempty"`
396 Label string `json:"label,omitempty"`
397 BackupsEnabled bool `json:"backups_enabled"`
398 Disks []int `json:"disks,omitempty"`
399 Configs []int `json:"configs,omitempty"`
400 PrivateIP bool `json:"private_ip,omitempty"`
401 Metadata *InstanceMetadataOptions `json:"metadata,omitempty"`
402 PlacementGroup *InstanceCreatePlacementGroupOptions `json:"placement_group,omitempty"`
403 404 // Deprecated: group is a deprecated property denoting a group label for the Linode.
405 Group string `json:"group,omitempty"`
406 }
407 408 // InstanceResizeOptions is an options struct used when resizing an instance
409 type InstanceResizeOptions struct {
410 Type string `json:"type"`
411 MigrationType InstanceMigrationType `json:"migration_type,omitempty"`
412 413 // When enabled, an instance resize will also resize a data disk if the instance has no more than one data disk and one swap disk
414 AllowAutoDiskResize *bool `json:"allow_auto_disk_resize,omitempty"`
415 }
416 417 // InstanceMigrateOptions is an options struct used when migrating an instance
418 type InstanceMigrateOptions struct {
419 Type InstanceMigrationType `json:"type,omitempty"`
420 Region string `json:"region,omitempty"`
421 Upgrade *bool `json:"upgrade,omitempty"`
422 423 PlacementGroup *InstanceCreatePlacementGroupOptions `json:"placement_group,omitempty"`
424 }
425 426 // ListInstances lists linode instances
427 func (c *Client) ListInstances(ctx context.Context, opts *ListOptions) ([]Instance, error) {
428 return getPaginatedResults[Instance](ctx, c, "linode/instances", opts)
429 }
430 431 // GetInstance gets the instance with the provided ID
432 func (c *Client) GetInstance(ctx context.Context, linodeID int) (*Instance, error) {
433 e := formatAPIPath("linode/instances/%d", linodeID)
434 return doGETRequest[Instance](ctx, c, e)
435 }
436 437 // GetInstanceTransfer gets the instance's network transfer pool statistics for the current month.
438 func (c *Client) GetInstanceTransfer(ctx context.Context, linodeID int) (*InstanceTransfer, error) {
439 e := formatAPIPath("linode/instances/%d/transfer", linodeID)
440 return doGETRequest[InstanceTransfer](ctx, c, e)
441 }
442 443 // GetInstanceTransferMonthly gets the instance's network transfer pool statistics for a specific month.
444 func (c *Client) GetInstanceTransferMonthly(ctx context.Context, linodeID, year, month int) (*MonthlyInstanceTransferStats, error) {
445 e := formatAPIPath("linode/instances/%d/transfer/%d/%d", linodeID, year, month)
446 return doGETRequest[MonthlyInstanceTransferStats](ctx, c, e)
447 }
448 449 // GetInstanceTransferMonthlyV2 gets the instance's network transfer pool statistics for a specific month.
450 func (c *Client) GetInstanceTransferMonthlyV2(ctx context.Context, linodeID, year, month int) (*MonthlyInstanceTransferStatsV2, error) {
451 e := formatAPIPath("linode/instances/%d/transfer/%d/%d", linodeID, year, month)
452 return doGETRequest[MonthlyInstanceTransferStatsV2](ctx, c, e)
453 }
454 455 // CreateInstance creates a Linode instance
456 func (c *Client) CreateInstance(ctx context.Context, opts InstanceCreateOptions) (*Instance, error) {
457 return doPOSTRequest[Instance](ctx, c, "linode/instances", opts)
458 }
459 460 // UpdateInstance updates a Linode instance
461 func (c *Client) UpdateInstance(ctx context.Context, linodeID int, opts InstanceUpdateOptions) (*Instance, error) {
462 e := formatAPIPath("linode/instances/%d", linodeID)
463 return doPUTRequest[Instance](ctx, c, e, opts)
464 }
465 466 // RenameInstance renames an Instance
467 func (c *Client) RenameInstance(ctx context.Context, linodeID int, label string) (*Instance, error) {
468 return c.UpdateInstance(ctx, linodeID, InstanceUpdateOptions{Label: label})
469 }
470 471 // DeleteInstance deletes a Linode instance
472 func (c *Client) DeleteInstance(ctx context.Context, linodeID int) error {
473 e := formatAPIPath("linode/instances/%d", linodeID)
474 return doDELETERequest(ctx, c, e)
475 }
476 477 // BootInstance will boot a Linode instance
478 // A configID of 0 will cause Linode to choose the last/best config
479 func (c *Client) BootInstance(ctx context.Context, linodeID int, configID int) error {
480 opts := make(map[string]int)
481 482 if configID != 0 {
483 opts = map[string]int{"config_id": configID}
484 }
485 486 e := formatAPIPath("linode/instances/%d/boot", linodeID)
487 488 return doPOSTRequestNoResponseBody(ctx, c, e, opts)
489 }
490 491 // CloneInstance clone an existing Instances Disks and Configuration profiles to another Linode Instance
492 func (c *Client) CloneInstance(ctx context.Context, linodeID int, opts InstanceCloneOptions) (*Instance, error) {
493 e := formatAPIPath("linode/instances/%d/clone", linodeID)
494 return doPOSTRequest[Instance](ctx, c, e, opts)
495 }
496 497 // ResetInstancePassword resets a Linode instance's root password
498 func (c *Client) ResetInstancePassword(ctx context.Context, linodeID int, opts InstancePasswordResetOptions) error {
499 e := formatAPIPath("linode/instances/%d/password", linodeID)
500 return doPOSTRequestNoResponseBody(ctx, c, e, opts)
501 }
502 503 // RebootInstance reboots a Linode instance
504 // A configID of 0 will cause Linode to choose the last/best config
505 func (c *Client) RebootInstance(ctx context.Context, linodeID int, configID int) error {
506 opts := make(map[string]int)
507 508 if configID != 0 {
509 opts = map[string]int{"config_id": configID}
510 }
511 512 e := formatAPIPath("linode/instances/%d/reboot", linodeID)
513 514 return doPOSTRequestNoResponseBody(ctx, c, e, opts)
515 }
516 517 // InstanceRebuildOptions is a struct representing the options to send to the rebuild linode endpoint
518 type InstanceRebuildOptions struct {
519 Image string `json:"image,omitempty"`
520 RootPass string `json:"root_pass,omitempty"`
521 AuthorizedKeys []string `json:"authorized_keys,omitempty"`
522 AuthorizedUsers []string `json:"authorized_users,omitempty"`
523 StackScriptID int `json:"stackscript_id,omitempty"`
524 StackScriptData map[string]string `json:"stackscript_data,omitempty"`
525 Booted *bool `json:"booted,omitempty"`
526 Metadata *InstanceMetadataOptions `json:"metadata,omitempty"`
527 Type string `json:"type,omitempty"`
528 529 // NOTE: Disk encryption may not currently be available to all users.
530 DiskEncryption InstanceDiskEncryption `json:"disk_encryption,omitempty"`
531 }
532 533 // RebuildInstance Deletes all Disks and Configs on this Linode,
534 // then deploys a new Image to this Linode with the given attributes.
535 func (c *Client) RebuildInstance(ctx context.Context, linodeID int, opts InstanceRebuildOptions) (*Instance, error) {
536 e := formatAPIPath("linode/instances/%d/rebuild", linodeID)
537 return doPOSTRequest[Instance](ctx, c, e, opts)
538 }
539 540 // InstanceRescueOptions fields are those accepted by RescueInstance
541 type InstanceRescueOptions struct {
542 Devices InstanceConfigDeviceMap `json:"devices"`
543 }
544 545 // RescueInstance reboots an instance into a safe environment for performing many system recovery and disk management tasks.
546 // Rescue Mode is based on the Finnix recovery distribution, a self-contained and bootable Linux distribution.
547 // You can also use Rescue Mode for tasks other than disaster recovery, such as formatting disks to use different filesystems,
548 // copying data between disks, and downloading files from a disk via SSH and SFTP.
549 func (c *Client) RescueInstance(ctx context.Context, linodeID int, opts InstanceRescueOptions) error {
550 e := formatAPIPath("linode/instances/%d/rescue", linodeID)
551 return doPOSTRequestNoResponseBody(ctx, c, e, opts)
552 }
553 554 // ResizeInstance resizes an instance to new Linode type
555 func (c *Client) ResizeInstance(ctx context.Context, linodeID int, opts InstanceResizeOptions) error {
556 e := formatAPIPath("linode/instances/%d/resize", linodeID)
557 return doPOSTRequestNoResponseBody(ctx, c, e, opts)
558 }
559 560 // ShutdownInstance - Shutdown an instance
561 func (c *Client) ShutdownInstance(ctx context.Context, id int) error {
562 return c.simpleInstanceAction(ctx, "shutdown", id)
563 }
564 565 // Deprecated: Please use UpgradeInstance instead.
566 // MutateInstance Upgrades a Linode to its next generation.
567 func (c *Client) MutateInstance(ctx context.Context, id int) error {
568 return c.simpleInstanceAction(ctx, "mutate", id)
569 }
570 571 // InstanceUpgradeOptions is a struct representing the options for upgrading a Linode
572 type InstanceUpgradeOptions struct {
573 // Automatically resize disks when resizing a Linode.
574 // When resizing down to a smaller plan your Linode's data must fit within the smaller disk size.
575 AllowAutoDiskResize bool `json:"allow_auto_disk_resize"`
576 }
577 578 // UpgradeInstance upgrades a Linode to its next generation.
579 func (c *Client) UpgradeInstance(ctx context.Context, linodeID int, opts InstanceUpgradeOptions) error {
580 e := formatAPIPath("linode/instances/%d/mutate", linodeID)
581 return doPOSTRequestNoResponseBody(ctx, c, e, opts)
582 }
583 584 // MigrateInstance - Migrate an instance
585 func (c *Client) MigrateInstance(ctx context.Context, linodeID int, opts InstanceMigrateOptions) error {
586 e := formatAPIPath("linode/instances/%d/migrate", linodeID)
587 return doPOSTRequestNoResponseBody(ctx, c, e, opts)
588 }
589 590 // simpleInstanceAction is a helper for Instance actions that take no parameters
591 // and return empty responses `{}` unless they return a standard error
592 func (c *Client) simpleInstanceAction(ctx context.Context, action string, linodeID int) error {
593 e := formatAPIPath("linode/instances/%d/%s", linodeID, action)
594 return doPOSTRequestNoRequestResponseBody(ctx, c, e)
595 }
596