1package gsclient
2
3import (
4	"context"
5	"errors"
6	"net/http"
7	"path"
8)
9
10// ServerOperator provides an interface for operations on servers.
11type ServerOperator interface {
12	GetServer(ctx context.Context, id string) (Server, error)
13	GetServerList(ctx context.Context) ([]Server, error)
14	GetServersByLocation(ctx context.Context, id string) ([]Server, error)
15	CreateServer(ctx context.Context, body ServerCreateRequest) (ServerCreateResponse, error)
16	UpdateServer(ctx context.Context, id string, body ServerUpdateRequest) error
17	DeleteServer(ctx context.Context, id string) error
18	StartServer(ctx context.Context, id string) error
19	StopServer(ctx context.Context, id string) error
20	ShutdownServer(ctx context.Context, id string) error
21	IsServerOn(ctx context.Context, id string) (bool, error)
22	GetServerMetricList(ctx context.Context, id string) ([]ServerMetric, error)
23	GetServerEventList(ctx context.Context, id string) ([]Event, error)
24	GetDeletedServers(ctx context.Context) ([]Server, error)
25}
26
27// ServerList holds a list of servers.
28type ServerList struct {
29	// Array of servers.
30	List map[string]ServerProperties `json:"servers"`
31}
32
33// DeletedServerList holds a list of deleted servers.
34type DeletedServerList struct {
35	// Array of deleted servers.
36	List map[string]ServerProperties `json:"deleted_servers"`
37}
38
39// Server represents a single server.
40type Server struct {
41	// Properties of a server.
42	Properties ServerProperties `json:"server"`
43}
44
45// ServerProperties holds properties of a server.
46type ServerProperties struct {
47	// The UUID of an object is always unique, and refers to a specific object.
48	ObjectUUID string `json:"object_uuid"`
49
50	// The human-readable name of the object. It supports the full UTF-8 character set, with a maximum of 64 characters.
51	Name string `json:"name"`
52
53	// Indicates the amount of memory in GB.
54	Memory int `json:"memory"`
55
56	// Number of server cores.
57	Cores int `json:"cores"`
58
59	// Specifies the hardware settings for the virtual machine.
60	HardwareProfile string `json:"hardware_profile"`
61
62	// Status indicates the status of the object. it could be in-provisioning or active
63	Status string `json:"status"`
64
65	// Helps to identify which data center an object belongs to.
66	LocationUUID string `json:"location_uuid"`
67
68	// The power status of the server.
69	Power bool `json:"power"`
70
71	// **DEPRECATED** The price for the current period since the last bill.
72	CurrentPrice float64 `json:"current_price"`
73
74	// Which Availability-Zone the Server is placed.
75	AvailabilityZone string `json:"availability_zone"`
76
77	// If the server should be auto-started in case of a failure (default=true).
78	AutoRecovery bool `json:"auto_recovery"`
79
80	// Legacy-Hardware emulation instead of virtio hardware.
81	// If enabled, hot-plugging cores, memory, storage, network, etc. will not work,
82	// but the server will most likely run every x86 compatible operating system.
83	// This mode comes with a performance penalty, as emulated hardware does not benefit from the virtio driver infrastructure.
84	Legacy bool `json:"legacy"`
85
86	// The token used by the panel to open the websocket VNC connection to the server console.
87	ConsoleToken string `json:"console_token"`
88
89	// Total minutes of memory used.
90	UsageInMinutesMemory int `json:"usage_in_minutes_memory"`
91
92	// Total minutes of cores used.
93	UsageInMinutesCores int `json:"usage_in_minutes_cores"`
94
95	// List of labels.
96	Labels []string `json:"labels"`
97
98	// Information about other objects which are related to this server. Object could be IPs, storages, networks, and ISO images.
99	Relations ServerRelations `json:"relations"`
100
101	// Defines the date and time the object was initially created.
102	CreateTime GSTime `json:"create_time"`
103
104	// Defines the date and time of the last object change.
105	ChangeTime GSTime `json:"change_time"`
106}
107
108// ServerRelations holds a list of server relations.
109// It shows the relations between a server and ISO images/Networks/IP addresses/Storages.
110type ServerRelations struct {
111	// Array of object (ServerIsoImageRelationProperties).
112	IsoImages []ServerIsoImageRelationProperties `json:"isoimages"`
113
114	// Array of object (ServerNetworkRelationProperties).
115	Networks []ServerNetworkRelationProperties `json:"networks"`
116
117	// Array of object (ServerIPRelationProperties).
118	PublicIPs []ServerIPRelationProperties `json:"public_ips"`
119
120	// Array of object (ServerStorageRelationProperties).
121	Storages []ServerStorageRelationProperties `json:"storages"`
122}
123
124// ServerCreateRequest represents a request for creating a server.
125type ServerCreateRequest struct {
126	// The human-readable name of the object. It supports the full UTF-8 character set, with a maximum of 64 characters.
127	Name string `json:"name"`
128
129	// The amount of server memory in GB.
130	Memory int `json:"memory"`
131
132	// The number of server cores.
133	Cores int `json:"cores"`
134
135	// Specifies the hardware settings for the virtual machine.
136	// Allowed values: DefaultServerHardware, NestedServerHardware, LegacyServerHardware, CiscoCSRServerHardware,
137	// SophosUTMServerHardware, F5BigipServerHardware, Q35ServerHardware, Q35NestedServerHardware.
138	HardwareProfile ServerHardwareProfile `json:"hardware_profile,omitempty"`
139
140	// Defines which Availability-Zone the Server is placed. Can be empty.
141	AvailablityZone string `json:"availability_zone,omitempty"`
142
143	// List of labels. Can be empty.
144	Labels []string `json:"labels,omitempty"`
145
146	// Status indicates the status of the object. Can be empty.
147	Status string `json:"status,omitempty"`
148
149	// If the server should be auto-started in case of a failure (default=true when AutoRecovery=nil).
150	AutoRecovery *bool `json:"auto_recovery,omitempty"`
151
152	// The information about other object which are related to this server. the object could be ip, storage, network, and isoimage.
153	// **Caution**: This field is deprecated.
154	Relations *ServerCreateRequestRelations `json:"relations,omitempty"`
155}
156
157// ServerCreateRequestRelations holds a list of a server's relations.
158type ServerCreateRequestRelations struct {
159	// Array of objects (ServerCreateRequestIsoimage).
160	IsoImages []ServerCreateRequestIsoimage `json:"isoimages"`
161
162	// Array of objects (ServerCreateRequestNetwork).
163	Networks []ServerCreateRequestNetwork `json:"networks"`
164
165	// Array of objects (ServerCreateRequestIP).
166	PublicIPs []ServerCreateRequestIP `json:"public_ips"`
167
168	// Array of objects (ServerCreateRequestStorage).
169	Storages []ServerCreateRequestStorage `json:"storages"`
170}
171
172// ServerCreateResponse represents a response for creating a server.
173type ServerCreateResponse struct {
174	// UUID of object being created. Same as ServerUUID.
175	ObjectUUID string `json:"object_uuid"`
176
177	// UUID of the request.
178	RequestUUID string `json:"request_uuid"`
179
180	// UUID of server being created. Same as ObjectUUID.
181	ServerUUID string `json:"server_uuid"`
182
183	// UUIDs of attached networks.
184	NetworkUUIDs []string `json:"network_uuids"`
185
186	// UUIDs of attached storages.
187	StorageUUIDs []string `json:"storage_uuids"`
188
189	// UUIDs of attached IP addresses.
190	IPaddrUUIDs []string `json:"ipaddr_uuids"`
191}
192
193// ServerPowerUpdateRequest reresents a request for updating server's power state.
194type ServerPowerUpdateRequest struct {
195	// Power=true => server is on.
196	// Power=false => server if off.
197	Power bool `json:"power"`
198}
199
200// ServerCreateRequestStorage represents a relation between a server and a storage.
201type ServerCreateRequestStorage struct {
202	// UUID of the storage being attached to the server.
203	StorageUUID string `json:"storage_uuid"`
204
205	// Is the storage a boot device?
206	BootDevice bool `json:"bootdevice,omitempty"`
207}
208
209// ServerCreateRequestNetwork represents a relation between a server and a network.
210type ServerCreateRequestNetwork struct {
211	// UUID of the networks being attached to the server.
212	NetworkUUID string `json:"network_uuid"`
213
214	// Is the network a boot device?
215	BootDevice bool `json:"bootdevice,omitempty"`
216}
217
218// ServerCreateRequestIP represents a relation between a server and an IP address.
219type ServerCreateRequestIP struct {
220	// UUID of the IP address being attached to the server.
221	IPaddrUUID string `json:"ipaddr_uuid"`
222}
223
224// ServerCreateRequestIsoimage represents a relation between a server and an ISO image.
225type ServerCreateRequestIsoimage struct {
226	// UUID of the ISO-image being attached to the server.
227	IsoimageUUID string `json:"isoimage_uuid"`
228}
229
230// ServerUpdateRequest represents a request for updating a server.
231type ServerUpdateRequest struct {
232	// The human-readable name of the object. It supports the full UTF-8 character set, with a maximum of 64 characters.
233	// Leave it if you do not want to update the name.
234	Name string `json:"name,omitempty"`
235
236	// Defines which Availability-Zone the Server is placed. Leave it if you do not want to update the zone.
237	AvailablityZone string `json:"availability_zone,omitempty"`
238
239	// The amount of server memory in GB. Leave it if you do not want to update the memory.
240	Memory int `json:"memory,omitempty"`
241
242	// The number of server cores. Leave it if you do not want to update the number of the cpu cores.
243	Cores int `json:"cores,omitempty"`
244
245	// List of labels. Leave it if you do not want to update the list of labels.
246	Labels *[]string `json:"labels,omitempty"`
247
248	// If the server should be auto-started in case of a failure (default=true).
249	// Leave it if you do not want to update this feature of the server.
250	AutoRecovery *bool `json:"auto_recovery,omitempty"`
251}
252
253// ServerMetricList holds a list of a server's metrics.
254type ServerMetricList struct {
255	// Array of a server's metrics
256	List []ServerMetricProperties `json:"server_metrics"`
257}
258
259// ServerMetric represents a single metric of a server.
260type ServerMetric struct {
261	// Properties of a server metric.
262	Properties ServerMetricProperties `json:"server_metric"`
263}
264
265// ServerMetricProperties holds properties of a server metric.
266type ServerMetricProperties struct {
267	// Defines the begin of the time range.
268	BeginTime GSTime `json:"begin_time"`
269
270	// Defines the end of the time range.
271	EndTime GSTime `json:"end_time"`
272
273	// The UUID of an object is always unique, and refers to a specific object.
274	PaaSServiceUUID string `json:"paas_service_uuid"`
275
276	// Core usage.
277	CoreUsage struct {
278		// Value.
279		Value float64 `json:"value"`
280
281		// Unit of value.
282		Unit string `json:"unit"`
283	} `json:"core_usage"`
284
285	// Storage usage.
286	StorageSize struct {
287		// Value.
288		Value float64 `json:"value"`
289
290		// Unit of value.
291		Unit string `json:"unit"`
292	} `json:"storage_size"`
293}
294
295// ServerHardwareProfile represents the type of server.
296type ServerHardwareProfile string
297
298// All available server's hardware types.
299const (
300	DefaultServerHardware   ServerHardwareProfile = "default"
301	NestedServerHardware    ServerHardwareProfile = "nested"
302	LegacyServerHardware    ServerHardwareProfile = "legacy"
303	CiscoCSRServerHardware  ServerHardwareProfile = "cisco_csr"
304	SophosUTMServerHardware ServerHardwareProfile = "sophos_utm"
305	F5BigipServerHardware   ServerHardwareProfile = "f5_bigip"
306	Q35ServerHardware       ServerHardwareProfile = "q35"
307	Q35NestedServerHardware ServerHardwareProfile = "q35_nested"
308)
309
310// GetServer gets a specific server based on given list.
311//
312// See: https://gridscale.io/en//api-documentation/index.html#operation/getServer
313func (c *Client) GetServer(ctx context.Context, id string) (Server, error) {
314	if !isValidUUID(id) {
315		return Server{}, errors.New("'id' is invalid")
316	}
317	r := gsRequest{
318		uri:                 path.Join(apiServerBase, id),
319		method:              http.MethodGet,
320		skipCheckingRequest: true,
321	}
322	var response Server
323	err := r.execute(ctx, *c, &response)
324	return response, err
325}
326
327// GetServerList gets a list of available servers.
328//
329// See: https://gridscale.io/en//api-documentation/index.html#operation/getServers
330func (c *Client) GetServerList(ctx context.Context) ([]Server, error) {
331	r := gsRequest{
332		uri:                 apiServerBase,
333		method:              http.MethodGet,
334		skipCheckingRequest: true,
335	}
336	var response ServerList
337	var servers []Server
338	err := r.execute(ctx, *c, &response)
339	for _, properties := range response.List {
340		servers = append(servers, Server{
341			Properties: properties,
342		})
343	}
344	return servers, err
345}
346
347// CreateServer creates a new server in a project. Normally you want to use
348// `Q35ServerHardware` as hardware profile.
349//
350// See: https://gridscale.io/en//api-documentation/index.html#operation/createServer
351func (c *Client) CreateServer(ctx context.Context, body ServerCreateRequest) (ServerCreateResponse, error) {
352	// check if these slices are nil
353	// make them be empty slice instead of nil
354	// so that JSON structure will be valid
355	if body.Relations != nil && body.Relations.PublicIPs == nil {
356		body.Relations.PublicIPs = make([]ServerCreateRequestIP, 0)
357	}
358	if body.Relations != nil && body.Relations.Networks == nil {
359		body.Relations.Networks = make([]ServerCreateRequestNetwork, 0)
360	}
361	if body.Relations != nil && body.Relations.IsoImages == nil {
362		body.Relations.IsoImages = make([]ServerCreateRequestIsoimage, 0)
363	}
364	if body.Relations != nil && body.Relations.Storages == nil {
365		body.Relations.Storages = make([]ServerCreateRequestStorage, 0)
366	}
367	r := gsRequest{
368		uri:    apiServerBase,
369		method: http.MethodPost,
370		body:   body,
371	}
372	var response ServerCreateResponse
373	err := r.execute(ctx, *c, &response)
374	// this fixed the endpoint's bug temporarily when creating server with/without
375	//'relations' field.
376	if response.ServerUUID == "" && response.ObjectUUID != "" {
377		response.ServerUUID = response.ObjectUUID
378	} else if response.ObjectUUID == "" && response.ServerUUID != "" {
379		response.ObjectUUID = response.ServerUUID
380	}
381	return response, err
382}
383
384// DeleteServer removes a specific server.
385//
386// See: https://gridscale.io/en//api-documentation/index.html#operation/deleteServer
387func (c *Client) DeleteServer(ctx context.Context, id string) error {
388	if !isValidUUID(id) {
389		return errors.New("'id' is invalid")
390	}
391	r := gsRequest{
392		uri:    path.Join(apiServerBase, id),
393		method: http.MethodDelete,
394	}
395	return r.execute(ctx, *c, nil)
396}
397
398// UpdateServer updates a specific server.
399//
400// See: https://gridscale.io/en//api-documentation/index.html#operation/updateServer
401func (c *Client) UpdateServer(ctx context.Context, id string, body ServerUpdateRequest) error {
402	if !isValidUUID(id) {
403		return errors.New("'id' is invalid")
404	}
405	r := gsRequest{
406		uri:    path.Join(apiServerBase, id),
407		method: http.MethodPatch,
408		body:   body,
409	}
410	return r.execute(ctx, *c, nil)
411}
412
413// GetServerEventList gets a list of a specific server's events.
414//
415// See: https://gridscale.io/en//api-documentation/index.html#operation/getServerEvents
416func (c *Client) GetServerEventList(ctx context.Context, id string) ([]Event, error) {
417	if !isValidUUID(id) {
418		return nil, errors.New("'id' is invalid")
419	}
420	r := gsRequest{
421		uri:                 path.Join(apiServerBase, id, "events"),
422		method:              http.MethodGet,
423		skipCheckingRequest: true,
424	}
425	var response EventList
426	var serverEvents []Event
427	err := r.execute(ctx, *c, &response)
428	for _, properties := range response.List {
429		serverEvents = append(serverEvents, Event{Properties: properties})
430	}
431	return serverEvents, err
432}
433
434// GetServerMetricList gets a list of a specific server's metrics.
435//
436// See: https://gridscale.io/en//api-documentation/index.html#operation/getServerMetrics
437func (c *Client) GetServerMetricList(ctx context.Context, id string) ([]ServerMetric, error) {
438	if !isValidUUID(id) {
439		return nil, errors.New("'id' is invalid")
440	}
441	r := gsRequest{
442		uri:                 path.Join(apiServerBase, id, "metrics"),
443		method:              http.MethodGet,
444		skipCheckingRequest: true,
445	}
446	var response ServerMetricList
447	var serverMetrics []ServerMetric
448	err := r.execute(ctx, *c, &response)
449	for _, properties := range response.List {
450		serverMetrics = append(serverMetrics, ServerMetric{Properties: properties})
451	}
452	return serverMetrics, err
453}
454
455// IsServerOn returns true if the server's power is on, otherwise returns false.
456func (c *Client) IsServerOn(ctx context.Context, id string) (bool, error) {
457	server, err := c.GetServer(ctx, id)
458	if err != nil {
459		return false, err
460	}
461	return server.Properties.Power, nil
462}
463
464// setServerPowerState turn on/off a specific server.
465// turnOn=true to turn on, turnOn=false to turn off.
466func (c *Client) setServerPowerState(ctx context.Context, id string, powerState bool) error {
467	isOn, err := c.IsServerOn(ctx, id)
468	if err != nil {
469		return err
470	}
471	if isOn == powerState {
472		return nil
473	}
474	r := gsRequest{
475		uri:    path.Join(apiServerBase, id, "power"),
476		method: http.MethodPatch,
477		body: ServerPowerUpdateRequest{
478			Power: powerState,
479		},
480	}
481	err = r.execute(ctx, *c, nil)
482	if err != nil {
483		return err
484	}
485	if c.Synchronous() {
486		return c.waitForServerPowerStatus(ctx, id, powerState)
487	}
488	return nil
489}
490
491// StartServer starts a server.
492func (c *Client) StartServer(ctx context.Context, id string) error {
493	return c.setServerPowerState(ctx, id, true)
494}
495
496// StopServer stops a server.
497func (c *Client) StopServer(ctx context.Context, id string) error {
498	return c.setServerPowerState(ctx, id, false)
499}
500
501// ShutdownServer shutdowns a specific server.
502func (c *Client) ShutdownServer(ctx context.Context, id string) error {
503	// Make sure the server exists and that it isn't already in the state we need it to be
504	server, err := c.GetServer(ctx, id)
505	if err != nil {
506		return err
507	}
508	if !server.Properties.Power {
509		return nil
510	}
511	r := gsRequest{
512		uri:    path.Join(apiServerBase, id, "shutdown"),
513		method: http.MethodPatch,
514		body:   map[string]string{},
515	}
516
517	err = r.execute(ctx, *c, nil)
518	if err != nil {
519		return err
520	}
521
522	if c.Synchronous() {
523		// If we get an error, which includes a timeout, power off the server instead.
524		err = c.waitForServerPowerStatus(ctx, id, false)
525		if err != nil {
526			return err
527		}
528	}
529	return nil
530}
531
532// GetServersByLocation gets a list of servers by location.
533//
534// See: https://gridscale.io/en//api-documentation/index.html#operation/getLocationServers
535func (c *Client) GetServersByLocation(ctx context.Context, id string) ([]Server, error) {
536	if !isValidUUID(id) {
537		return nil, errors.New("'id' is invalid")
538	}
539	r := gsRequest{
540		uri:                 path.Join(apiLocationBase, id, "servers"),
541		method:              http.MethodGet,
542		skipCheckingRequest: true,
543	}
544	var response ServerList
545	var servers []Server
546	err := r.execute(ctx, *c, &response)
547	for _, properties := range response.List {
548		servers = append(servers, Server{Properties: properties})
549	}
550	return servers, err
551}
552
553// GetDeletedServers gets a list of deleted servers.
554//
555// See: https://gridscale.io/en//api-documentation/index.html#operation/getDeletedServers
556func (c *Client) GetDeletedServers(ctx context.Context) ([]Server, error) {
557	r := gsRequest{
558		uri:                 path.Join(apiDeletedBase, "servers"),
559		method:              http.MethodGet,
560		skipCheckingRequest: true,
561	}
562	var response DeletedServerList
563	var servers []Server
564	err := r.execute(ctx, *c, &response)
565	for _, properties := range response.List {
566		servers = append(servers, Server{Properties: properties})
567	}
568	return servers, err
569}
570
571// waitForServerPowerStatus allows to wait for a server changing its power status.
572func (c *Client) waitForServerPowerStatus(ctx context.Context, id string, status bool) error {
573	return retryWithContext(ctx, func() (bool, error) {
574		server, err := c.GetServer(ctx, id)
575		return server.Properties.Power != status, err
576	}, c.DelayInterval())
577}
578