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