1package packngo
2
3import (
4	"fmt"
5	"strings"
6)
7
8const deviceBasePath = "/devices"
9
10// DeviceService interface defines available device methods
11type DeviceService interface {
12	List(ProjectID string, listOpt *ListOptions) ([]Device, *Response, error)
13	Get(string) (*Device, *Response, error)
14	GetExtra(deviceID string, includes, excludes []string) (*Device, *Response, error)
15	Create(*DeviceCreateRequest) (*Device, *Response, error)
16	Update(string, *DeviceUpdateRequest) (*Device, *Response, error)
17	Delete(string) (*Response, error)
18	Reboot(string) (*Response, error)
19	PowerOff(string) (*Response, error)
20	PowerOn(string) (*Response, error)
21	Lock(string) (*Response, error)
22	Unlock(string) (*Response, error)
23}
24
25type devicesRoot struct {
26	Devices []Device `json:"devices"`
27	Meta    meta     `json:"meta"`
28}
29
30// Device represents a Packet device
31type Device struct {
32	ID                  string                 `json:"id"`
33	Href                string                 `json:"href,omitempty"`
34	Hostname            string                 `json:"hostname,omitempty"`
35	State               string                 `json:"state,omitempty"`
36	Created             string                 `json:"created_at,omitempty"`
37	Updated             string                 `json:"updated_at,omitempty"`
38	Locked              bool                   `json:"locked,omitempty"`
39	BillingCycle        string                 `json:"billing_cycle,omitempty"`
40	Storage             map[string]interface{} `json:"storage,omitempty"`
41	Tags                []string               `json:"tags,omitempty"`
42	Network             []*IPAddressAssignment `json:"ip_addresses"`
43	Volumes             []*Volume              `json:"volumes"`
44	OS                  *OS                    `json:"operating_system,omitempty"`
45	Plan                *Plan                  `json:"plan,omitempty"`
46	Facility            *Facility              `json:"facility,omitempty"`
47	Project             *Project               `json:"project,omitempty"`
48	ProvisionEvents     []*ProvisionEvent      `json:"provisioning_events,omitempty"`
49	ProvisionPer        float32                `json:"provisioning_percentage,omitempty"`
50	UserData            string                 `json:"userdata,omitempty"`
51	RootPassword        string                 `json:"root_password,omitempty"`
52	IPXEScriptURL       string                 `json:"ipxe_script_url,omitempty"`
53	AlwaysPXE           bool                   `json:"always_pxe,omitempty"`
54	HardwareReservation Href                   `json:"hardware_reservation,omitempty"`
55	SpotInstance        bool                   `json:"spot_instance,omitempty"`
56	SpotPriceMax        float64                `json:"spot_price_max,omitempty"`
57	TerminationTime     *Timestamp             `json:"termination_time,omitempty"`
58	NetworkPorts        []Port                 `json:"network_ports,omitempty"`
59	CustomData          map[string]interface{} `json:"customdata,omitempty"`
60}
61
62type ProvisionEvent struct {
63	ID            string     `json:"id"`
64	Body          string     `json:"body"`
65	CreatedAt     *Timestamp `json:"created_at,omitempty"`
66	Href          string     `json:"href"`
67	Interpolated  string     `json:"interpolated"`
68	Relationships []Href     `json:"relationships"`
69	State         string     `json:"state"`
70	Type          string     `json:"type"`
71}
72
73func (d Device) String() string {
74	return Stringify(d)
75}
76
77// DeviceCreateRequest type used to create a Packet device
78type DeviceCreateRequest struct {
79	Hostname              string     `json:"hostname"`
80	Plan                  string     `json:"plan"`
81	Facility              string     `json:"facility"`
82	OS                    string     `json:"operating_system"`
83	BillingCycle          string     `json:"billing_cycle"`
84	ProjectID             string     `json:"project_id"`
85	UserData              string     `json:"userdata"`
86	Storage               string     `json:"storage,omitempty"`
87	Tags                  []string   `json:"tags"`
88	IPXEScriptURL         string     `json:"ipxe_script_url,omitempty"`
89	PublicIPv4SubnetSize  int        `json:"public_ipv4_subnet_size,omitempty"`
90	AlwaysPXE             bool       `json:"always_pxe,omitempty"`
91	HardwareReservationID string     `json:"hardware_reservation_id,omitempty"`
92	SpotInstance          bool       `json:"spot_instance,omitempty"`
93	SpotPriceMax          float64    `json:"spot_price_max,omitempty,string"`
94	TerminationTime       *Timestamp `json:"termination_time,omitempty"`
95	CustomData            string     `json:"customdata,omitempty"`
96}
97
98// DeviceUpdateRequest type used to update a Packet device
99type DeviceUpdateRequest struct {
100	Hostname      *string   `json:"hostname,omitempty"`
101	Description   *string   `json:"description,omitempty"`
102	UserData      *string   `json:"userdata,omitempty"`
103	Locked        *bool     `json:"locked,omitempty"`
104	Tags          *[]string `json:"tags,omitempty"`
105	AlwaysPXE     *bool     `json:"always_pxe,omitempty"`
106	IPXEScriptURL *string   `json:"ipxe_script_url,omitempty"`
107	CustomData    *string   `json:"customdata,omitempty"`
108}
109
110func (d DeviceCreateRequest) String() string {
111	return Stringify(d)
112}
113
114// DeviceActionRequest type used to execute actions on devices
115type DeviceActionRequest struct {
116	Type string `json:"type"`
117}
118
119func (d DeviceActionRequest) String() string {
120	return Stringify(d)
121}
122
123// DeviceServiceOp implements DeviceService
124type DeviceServiceOp struct {
125	client *Client
126}
127
128// List returns devices on a project
129func (s *DeviceServiceOp) List(projectID string, listOpt *ListOptions) (devices []Device, resp *Response, err error) {
130	params := "include=facility"
131	if listOpt != nil {
132		params = listOpt.createURL()
133	}
134	path := fmt.Sprintf("%s/%s%s?%s", projectBasePath, projectID, deviceBasePath, params)
135
136	for {
137		subset := new(devicesRoot)
138
139		resp, err = s.client.DoRequest("GET", path, nil, subset)
140		if err != nil {
141			return nil, resp, err
142		}
143
144		devices = append(devices, subset.Devices...)
145
146		if subset.Meta.Next != nil && (listOpt == nil || listOpt.Page == 0) {
147			path = subset.Meta.Next.Href
148			if params != "" {
149				path = fmt.Sprintf("%s&%s", path, params)
150			}
151			continue
152		}
153
154		return
155	}
156}
157
158// Get returns a device by id
159func (s *DeviceServiceOp) Get(deviceID string) (*Device, *Response, error) {
160	return s.GetExtra(deviceID, []string{"facility"}, nil)
161}
162
163// GetExtra returns a device by id. Specifying either includes/excludes provides more or less desired
164// detailed information about resources which would otherwise be represented with an href link
165func (s *DeviceServiceOp) GetExtra(deviceID string, includes, excludes []string) (*Device, *Response, error) {
166	path := fmt.Sprintf("%s/%s", deviceBasePath, deviceID)
167	if includes != nil {
168		path += fmt.Sprintf("?include=%s", strings.Join(includes, ","))
169	} else if excludes != nil {
170		path += fmt.Sprintf("?exclude=%s", strings.Join(excludes, ","))
171	}
172	device := new(Device)
173
174	resp, err := s.client.DoRequest("GET", path, nil, device)
175	if err != nil {
176		return nil, resp, err
177	}
178
179	return device, resp, err
180}
181
182// Create creates a new device
183func (s *DeviceServiceOp) Create(createRequest *DeviceCreateRequest) (*Device, *Response, error) {
184	path := fmt.Sprintf("%s/%s%s", projectBasePath, createRequest.ProjectID, deviceBasePath)
185	device := new(Device)
186
187	resp, err := s.client.DoRequest("POST", path, createRequest, device)
188	if err != nil {
189		return nil, resp, err
190	}
191
192	return device, resp, err
193}
194
195// Update updates an existing device
196func (s *DeviceServiceOp) Update(deviceID string, updateRequest *DeviceUpdateRequest) (*Device, *Response, error) {
197	path := fmt.Sprintf("%s/%s?include=facility", deviceBasePath, deviceID)
198	device := new(Device)
199
200	resp, err := s.client.DoRequest("PUT", path, updateRequest, device)
201	if err != nil {
202		return nil, resp, err
203	}
204
205	return device, resp, err
206}
207
208// Delete deletes a device
209func (s *DeviceServiceOp) Delete(deviceID string) (*Response, error) {
210	path := fmt.Sprintf("%s/%s", deviceBasePath, deviceID)
211
212	return s.client.DoRequest("DELETE", path, nil, nil)
213}
214
215// Reboot reboots on a device
216func (s *DeviceServiceOp) Reboot(deviceID string) (*Response, error) {
217	path := fmt.Sprintf("%s/%s/actions", deviceBasePath, deviceID)
218	action := &DeviceActionRequest{Type: "reboot"}
219
220	return s.client.DoRequest("POST", path, action, nil)
221}
222
223// PowerOff powers on a device
224func (s *DeviceServiceOp) PowerOff(deviceID string) (*Response, error) {
225	path := fmt.Sprintf("%s/%s/actions", deviceBasePath, deviceID)
226	action := &DeviceActionRequest{Type: "power_off"}
227
228	return s.client.DoRequest("POST", path, action, nil)
229}
230
231// PowerOn powers on a device
232func (s *DeviceServiceOp) PowerOn(deviceID string) (*Response, error) {
233	path := fmt.Sprintf("%s/%s/actions", deviceBasePath, deviceID)
234	action := &DeviceActionRequest{Type: "power_on"}
235
236	return s.client.DoRequest("POST", path, action, nil)
237}
238
239type lockType struct {
240	Locked bool `json:"locked"`
241}
242
243// Lock sets a device to "locked"
244func (s *DeviceServiceOp) Lock(deviceID string) (*Response, error) {
245	path := fmt.Sprintf("%s/%s", deviceBasePath, deviceID)
246	action := lockType{Locked: true}
247
248	return s.client.DoRequest("PATCH", path, action, nil)
249}
250
251// Unlock sets a device to "unlocked"
252func (s *DeviceServiceOp) Unlock(deviceID string) (*Response, error) {
253	path := fmt.Sprintf("%s/%s", deviceBasePath, deviceID)
254	action := lockType{Locked: false}
255
256	return s.client.DoRequest("PATCH", path, action, nil)
257}
258