1package vm
2
3import (
4	"math"
5	"strings"
6	"time"
7
8	biagentclient "github.com/cloudfoundry/bosh-agent/agentclient"
9	bias "github.com/cloudfoundry/bosh-agent/agentclient/applyspec"
10	bicloud "github.com/cloudfoundry/bosh-cli/cloud"
11	biconfig "github.com/cloudfoundry/bosh-cli/config"
12	bidisk "github.com/cloudfoundry/bosh-cli/deployment/disk"
13	bideplmanifest "github.com/cloudfoundry/bosh-cli/deployment/manifest"
14	biui "github.com/cloudfoundry/bosh-cli/ui"
15	bosherr "github.com/cloudfoundry/bosh-utils/errors"
16	boshlog "github.com/cloudfoundry/bosh-utils/logger"
17	boshretry "github.com/cloudfoundry/bosh-utils/retrystrategy"
18	boshsys "github.com/cloudfoundry/bosh-utils/system"
19)
20
21type Clock interface {
22	Sleep(time.Duration)
23	Now() time.Time
24}
25
26// go:generate counterfeiter . VM
27
28type VM interface {
29	CID() string
30	Exists() (bool, error)
31	AgentClient() biagentclient.AgentClient
32	WaitUntilReady(timeout time.Duration, delay time.Duration) error
33	Start() error
34	Stop() error
35	Drain() error
36	Apply(bias.ApplySpec) error
37	UpdateDisks(bideplmanifest.DiskPool, biui.Stage) ([]bidisk.Disk, error)
38	WaitToBeRunning(maxAttempts int, delay time.Duration) error
39	AttachDisk(bidisk.Disk) error
40	DetachDisk(bidisk.Disk) error
41	Disks() ([]bidisk.Disk, error)
42	UnmountDisk(bidisk.Disk) error
43	MigrateDisk() error
44	RunScript(script string, options map[string]interface{}) error
45	Delete() error
46	GetState() (biagentclient.AgentState, error)
47}
48
49type vm struct {
50	cid          string
51	vmRepo       biconfig.VMRepo
52	stemcellRepo biconfig.StemcellRepo
53	diskDeployer DiskDeployer
54	agentClient  biagentclient.AgentClient
55	cloud        bicloud.Cloud
56	timeService  Clock
57	fs           boshsys.FileSystem
58	logger       boshlog.Logger
59	logTag       string
60	metadata     bicloud.VMMetadata
61}
62
63func NewVM(
64	cid string,
65	vmRepo biconfig.VMRepo,
66	stemcellRepo biconfig.StemcellRepo,
67	diskDeployer DiskDeployer,
68	agentClient biagentclient.AgentClient,
69	cloud bicloud.Cloud,
70	timeService Clock,
71	fs boshsys.FileSystem,
72	logger boshlog.Logger,
73) VM {
74	return &vm{
75		cid:          cid,
76		vmRepo:       vmRepo,
77		stemcellRepo: stemcellRepo,
78		diskDeployer: diskDeployer,
79		agentClient:  agentClient,
80		cloud:        cloud,
81		timeService:  timeService,
82		fs:           fs,
83		logger:       logger,
84		logTag:       "vm",
85	}
86}
87
88func NewVMWithMetadata(
89	cid string,
90	vmRepo biconfig.VMRepo,
91	stemcellRepo biconfig.StemcellRepo,
92	diskDeployer DiskDeployer,
93	agentClient biagentclient.AgentClient,
94	cloud bicloud.Cloud,
95	timeService Clock,
96	fs boshsys.FileSystem,
97	logger boshlog.Logger,
98	metadata bicloud.VMMetadata,
99) VM {
100	return &vm{
101		cid:          cid,
102		vmRepo:       vmRepo,
103		stemcellRepo: stemcellRepo,
104		diskDeployer: diskDeployer,
105		agentClient:  agentClient,
106		cloud:        cloud,
107		timeService:  timeService,
108		fs:           fs,
109		logger:       logger,
110		logTag:       "vm",
111		metadata:     metadata,
112	}
113}
114
115func (vm *vm) CID() string {
116	return vm.cid
117}
118
119func (vm *vm) Exists() (bool, error) {
120	exists, err := vm.cloud.HasVM(vm.cid)
121	if err != nil {
122		return false, bosherr.WrapErrorf(err, "Checking existence of VM '%s'", vm.cid)
123	}
124	return exists, nil
125}
126
127func (vm *vm) AgentClient() biagentclient.AgentClient {
128	return vm.agentClient
129}
130
131func (vm *vm) WaitUntilReady(timeout time.Duration, delay time.Duration) error {
132	agentPingRetryable := biagentclient.NewPingRetryable(vm.agentClient)
133	agentPingRetryStrategy := boshretry.NewTimeoutRetryStrategy(timeout, delay, agentPingRetryable, vm.timeService, vm.logger)
134	return agentPingRetryStrategy.Try()
135}
136
137func (vm *vm) Start() error {
138	vm.logger.Debug(vm.logTag, "Starting agent")
139	err := vm.agentClient.Start()
140	if err != nil {
141		return bosherr.WrapError(err, "Starting agent")
142	}
143
144	return nil
145}
146
147func (vm *vm) Drain() error {
148	vm.logger.Debug(vm.logTag, "Draining VM")
149	drainTime, err := vm.agentClient.Drain("shutdown")
150	if err != nil {
151		return bosherr.WrapError(err, "Draining VM")
152	}
153
154	for drainTime < 0 {
155		vm.timeService.Sleep(time.Duration(math.Abs(float64(drainTime))) * time.Second)
156		drainTime, err = vm.agentClient.Drain("status")
157		if err != nil {
158			return bosherr.WrapError(err, "Draining VM")
159		}
160	}
161	vm.timeService.Sleep(time.Duration(drainTime) * time.Second)
162
163	return nil
164}
165
166func (vm *vm) Stop() error {
167	vm.logger.Debug(vm.logTag, "Stopping agent")
168	err := vm.agentClient.Stop()
169	if err != nil {
170		return bosherr.WrapError(err, "Stopping agent")
171	}
172
173	return nil
174}
175
176func (vm *vm) Apply(newState bias.ApplySpec) error {
177	vm.logger.Debug(vm.logTag, "Sending apply message to the agent with '%#v'", newState)
178	err := vm.agentClient.Apply(newState)
179	if err != nil {
180		return bosherr.WrapError(err, "Sending apply spec to agent")
181	}
182
183	return nil
184}
185
186func (vm *vm) UpdateDisks(diskPool bideplmanifest.DiskPool, eventLoggerStage biui.Stage) ([]bidisk.Disk, error) {
187	disks, err := vm.diskDeployer.Deploy(diskPool, vm.cloud, vm, eventLoggerStage)
188	if err != nil {
189		return disks, bosherr.WrapError(err, "Deploying disk")
190	}
191	return disks, nil
192}
193
194func (vm *vm) WaitToBeRunning(maxAttempts int, delay time.Duration) error {
195	agentGetStateRetryable := biagentclient.NewGetStateRetryable(vm.agentClient)
196	agentGetStateRetryStrategy := boshretry.NewAttemptRetryStrategy(maxAttempts, delay, agentGetStateRetryable, vm.logger)
197	return agentGetStateRetryStrategy.Try()
198}
199
200func (vm *vm) AttachDisk(disk bidisk.Disk) error {
201	diskHints, err := vm.cloud.AttachDisk(vm.cid, disk.CID())
202	if err != nil {
203		return bosherr.WrapError(err, "Attaching disk in the cloud")
204	}
205
206	err = vm.cloud.SetDiskMetadata(disk.CID(), vm.createDiskMetadata())
207	if err != nil {
208		cloudErr, ok := err.(bicloud.Error)
209		if ok && cloudErr.Type() == bicloud.NotImplementedError {
210			vm.logger.Warn(vm.logTag, "'SetDiskMetadata' not implemented by CPI")
211		} else {
212			return bosherr.WrapErrorf(err, "Setting disk metadata for %s", disk.CID())
213		}
214	}
215
216	err = vm.WaitUntilReady(10*time.Minute, 500*time.Millisecond)
217	if err != nil {
218		return bosherr.WrapError(err, "Waiting for agent to be accessible after attaching disk")
219	}
220
221	if diskHints != nil {
222		err = vm.agentClient.AddPersistentDisk(disk.CID(), diskHints)
223		if err != nil && !strings.Contains(err.Error(), "unknown message add_persistent_disk") {
224			return bosherr.WrapError(err, "Adding persistent disk")
225		}
226	}
227
228	err = vm.agentClient.MountDisk(disk.CID())
229	if err != nil {
230		return bosherr.WrapError(err, "Mounting disk")
231	}
232
233	return nil
234}
235
236func (vm *vm) DetachDisk(disk bidisk.Disk) error {
237
238	err := vm.agentClient.RemovePersistentDisk(disk.CID())
239	if err != nil && !strings.Contains(err.Error(), "Agent responded with error: unknown message remove_persistent_disk") {
240		return bosherr.WrapError(err, "Removing persistent disk")
241	}
242
243	err = vm.cloud.DetachDisk(vm.cid, disk.CID())
244	if err != nil {
245		return bosherr.WrapError(err, "Detaching disk in the cloud")
246	}
247
248	err = vm.WaitUntilReady(10*time.Minute, 500*time.Millisecond)
249	if err != nil {
250		return bosherr.WrapError(err, "Waiting for agent to be accessible after detaching disk")
251	}
252
253	return nil
254}
255
256func (vm *vm) Disks() ([]bidisk.Disk, error) {
257	result := []bidisk.Disk{}
258
259	disks, err := vm.agentClient.ListDisk()
260	if err != nil {
261		return result, bosherr.WrapError(err, "Listing vm disks")
262	}
263
264	for _, diskCID := range disks {
265		disk := bidisk.NewDisk(biconfig.DiskRecord{CID: diskCID}, nil, nil)
266		result = append(result, disk)
267	}
268
269	return result, nil
270}
271
272func (vm *vm) UnmountDisk(disk bidisk.Disk) error {
273	return vm.agentClient.UnmountDisk(disk.CID())
274}
275
276func (vm *vm) MigrateDisk() error {
277	return vm.agentClient.MigrateDisk()
278}
279
280func (vm *vm) RunScript(script string, options map[string]interface{}) error {
281	return vm.agentClient.RunScript(script, options)
282}
283
284func (vm *vm) Delete() error {
285	deleteErr := vm.cloud.DeleteVM(vm.cid)
286	if deleteErr != nil {
287		// allow VMNotFoundError for idempotency
288		cloudErr, ok := deleteErr.(bicloud.Error)
289		if !ok || cloudErr.Type() != bicloud.VMNotFoundError {
290			return bosherr.WrapError(deleteErr, "Deleting vm in the cloud")
291		}
292	}
293
294	err := vm.vmRepo.ClearCurrent()
295	if err != nil {
296		return bosherr.WrapError(err, "Deleting vm from vm repo")
297	}
298
299	err = vm.stemcellRepo.ClearCurrent()
300	if err != nil {
301		return bosherr.WrapError(err, "Clearing current stemcell from stemcell repo")
302	}
303
304	// returns bicloud.Error only if it is a VMNotFoundError
305	return deleteErr
306}
307
308func (vm *vm) GetState() (biagentclient.AgentState, error) {
309	agentState, err := vm.agentClient.GetState()
310
311	if err != nil {
312		return agentState, bosherr.WrapError(err, "Getting vm state")
313	}
314
315	return agentState, nil
316}
317
318func (vm *vm) createDiskMetadata() bicloud.DiskMetadata {
319	diskMetadata := make(bicloud.DiskMetadata)
320	for key, value := range vm.metadata {
321		diskMetadata[key] = value
322	}
323
324	delete(diskMetadata, "job")
325	delete(diskMetadata, "name")
326	delete(diskMetadata, "index")
327	delete(diskMetadata, "created_at")
328	diskMetadata["instance_index"] = vm.metadata["index"]
329	diskMetadata["attached_at"] = vm.timeService.Now().Format(time.RFC3339)
330
331	return diskMetadata
332}
333