github.com/ferranbt/nomad@v0.9.3-0.20190607002617-85c449b7667c/plugins/device/cmd/example/device.go (about) 1 package example 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "sync" 10 "time" 11 12 log "github.com/hashicorp/go-hclog" 13 "github.com/hashicorp/nomad/helper" 14 "github.com/hashicorp/nomad/plugins/base" 15 "github.com/hashicorp/nomad/plugins/device" 16 "github.com/hashicorp/nomad/plugins/shared/hclspec" 17 "github.com/hashicorp/nomad/plugins/shared/structs" 18 "github.com/kr/pretty" 19 "google.golang.org/grpc/codes" 20 "google.golang.org/grpc/status" 21 ) 22 23 const ( 24 // pluginName is the name of the plugin 25 pluginName = "example-fs-device" 26 27 // vendor is the vendor providing the devices 28 vendor = "nomad" 29 30 // deviceType is the type of device being returned 31 deviceType = "file" 32 33 // deviceName is the name of the devices being exposed 34 deviceName = "mock" 35 ) 36 37 var ( 38 // pluginInfo describes the plugin 39 pluginInfo = &base.PluginInfoResponse{ 40 Type: base.PluginTypeDevice, 41 PluginApiVersions: []string{device.ApiVersion010}, 42 PluginVersion: "v0.1.0", 43 Name: pluginName, 44 } 45 46 // configSpec is the specification of the plugin's configuration 47 configSpec = hclspec.NewObject(map[string]*hclspec.Spec{ 48 "dir": hclspec.NewDefault( 49 hclspec.NewAttr("dir", "string", false), 50 hclspec.NewLiteral("\".\""), 51 ), 52 "list_period": hclspec.NewDefault( 53 hclspec.NewAttr("list_period", "string", false), 54 hclspec.NewLiteral("\"5s\""), 55 ), 56 "unhealthy_perm": hclspec.NewDefault( 57 hclspec.NewAttr("unhealthy_perm", "string", false), 58 hclspec.NewLiteral("\"-rwxrwxrwx\""), 59 ), 60 }) 61 ) 62 63 // Config contains configuration information for the plugin. 64 type Config struct { 65 Dir string `codec:"dir"` 66 ListPeriod string `codec:"list_period"` 67 UnhealthyPerm string `codec:"unhealthy_perm"` 68 } 69 70 // FsDevice is an example device plugin. The device plugin exposes files as 71 // devices and periodically polls the directory for new files. If a file has a 72 // given file permission, it is considered unhealthy. This device plugin is 73 // purely for use as an example. 74 type FsDevice struct { 75 logger log.Logger 76 77 // deviceDir is the directory we expose as devices 78 deviceDir string 79 80 // unhealthyPerm is the permissions on a file we consider unhealthy 81 unhealthyPerm string 82 83 // listPeriod is how often we should list the device directory to detect new 84 // devices 85 listPeriod time.Duration 86 87 // devices is the set of detected devices and maps whether they are healthy 88 devices map[string]bool 89 deviceLock sync.RWMutex 90 } 91 92 // NewExampleDevice returns a new example device plugin. 93 func NewExampleDevice(log log.Logger) *FsDevice { 94 return &FsDevice{ 95 logger: log.Named(pluginName), 96 devices: make(map[string]bool), 97 } 98 } 99 100 // PluginInfo returns information describing the plugin. 101 func (d *FsDevice) PluginInfo() (*base.PluginInfoResponse, error) { 102 return pluginInfo, nil 103 } 104 105 // ConfigSchema returns the plugins configuration schema. 106 func (d *FsDevice) ConfigSchema() (*hclspec.Spec, error) { 107 return configSpec, nil 108 } 109 110 // SetConfig is used to set the configuration of the plugin. 111 func (d *FsDevice) SetConfig(c *base.Config) error { 112 var config Config 113 if err := base.MsgPackDecode(c.PluginConfig, &config); err != nil { 114 return err 115 } 116 117 // Save the device directory and the unhealthy permissions 118 d.deviceDir = config.Dir 119 d.unhealthyPerm = config.UnhealthyPerm 120 121 // Convert the poll period 122 period, err := time.ParseDuration(config.ListPeriod) 123 if err != nil { 124 return fmt.Errorf("failed to parse list period %q: %v", config.ListPeriod, err) 125 } 126 d.listPeriod = period 127 128 d.logger.Debug("test debug") 129 d.logger.Info("config set", "config", log.Fmt("% #v", pretty.Formatter(config))) 130 return nil 131 } 132 133 // Fingerprint streams detected devices. If device changes are detected or the 134 // devices health changes, messages will be emitted. 135 func (d *FsDevice) Fingerprint(ctx context.Context) (<-chan *device.FingerprintResponse, error) { 136 if d.deviceDir == "" { 137 return nil, status.New(codes.Internal, "device directory not set in config").Err() 138 } 139 140 outCh := make(chan *device.FingerprintResponse) 141 go d.fingerprint(ctx, outCh) 142 return outCh, nil 143 } 144 145 // fingerprint is the long running goroutine that detects hardware 146 func (d *FsDevice) fingerprint(ctx context.Context, devices chan *device.FingerprintResponse) { 147 defer close(devices) 148 149 // Create a timer that will fire immediately for the first detection 150 ticker := time.NewTimer(0) 151 152 for { 153 select { 154 case <-ctx.Done(): 155 return 156 case <-ticker.C: 157 ticker.Reset(d.listPeriod) 158 } 159 160 d.logger.Trace("scanning for changes") 161 162 files, err := ioutil.ReadDir(d.deviceDir) 163 if err != nil { 164 d.logger.Error("failed to list device directory", "error", err) 165 devices <- device.NewFingerprintError(err) 166 return 167 } 168 169 detected := d.diffFiles(files) 170 if len(detected) == 0 { 171 continue 172 } 173 174 devices <- device.NewFingerprint(getDeviceGroup(detected)) 175 176 } 177 } 178 179 func (d *FsDevice) diffFiles(files []os.FileInfo) []*device.Device { 180 d.deviceLock.Lock() 181 defer d.deviceLock.Unlock() 182 183 // Build an unhealthy message 184 unhealthyDesc := fmt.Sprintf("Device has bad permissions %q", d.unhealthyPerm) 185 186 var changes bool 187 fnames := make(map[string]struct{}) 188 for _, f := range files { 189 name := f.Name() 190 fnames[name] = struct{}{} 191 if f.IsDir() { 192 d.logger.Trace("skipping directory", "directory", name) 193 continue 194 } 195 196 // Determine the health 197 perms := f.Mode().Perm().String() 198 healthy := perms != d.unhealthyPerm 199 d.logger.Trace("checking health", "file perm", perms, "unhealthy perms", d.unhealthyPerm, "healthy", healthy) 200 201 // See if we alreay have the device 202 oldHealth, ok := d.devices[name] 203 if ok && oldHealth == healthy { 204 continue 205 } 206 207 // Health has changed or we have a new object 208 changes = true 209 d.devices[name] = healthy 210 } 211 212 for id := range d.devices { 213 if _, ok := fnames[id]; !ok { 214 delete(d.devices, id) 215 changes = true 216 } 217 } 218 219 // Nothing to do 220 if !changes { 221 return nil 222 } 223 224 // Build the devices 225 detected := make([]*device.Device, 0, len(d.devices)) 226 for name, healthy := range d.devices { 227 var desc string 228 if !healthy { 229 desc = unhealthyDesc 230 } 231 232 detected = append(detected, &device.Device{ 233 ID: name, 234 Healthy: healthy, 235 HealthDesc: desc, 236 }) 237 } 238 239 return detected 240 } 241 242 // getDeviceGroup is a helper to build the DeviceGroup given a set of devices. 243 func getDeviceGroup(devices []*device.Device) *device.DeviceGroup { 244 return &device.DeviceGroup{ 245 Vendor: vendor, 246 Type: deviceType, 247 Name: deviceName, 248 Devices: devices, 249 } 250 } 251 252 // Reserve returns information on how to mount the given devices. 253 func (d *FsDevice) Reserve(deviceIDs []string) (*device.ContainerReservation, error) { 254 if len(deviceIDs) == 0 { 255 return nil, status.New(codes.InvalidArgument, "no device ids given").Err() 256 } 257 258 deviceDir, err := filepath.Abs(d.deviceDir) 259 if err != nil { 260 return nil, status.Newf(codes.Internal, "failed to load device dir abs path").Err() 261 } 262 263 resp := &device.ContainerReservation{} 264 265 for _, id := range deviceIDs { 266 // Check if the device is known 267 if _, ok := d.devices[id]; !ok { 268 return nil, status.Newf(codes.InvalidArgument, "unknown device %q", id).Err() 269 } 270 271 // Add a mount 272 resp.Mounts = append(resp.Mounts, &device.Mount{ 273 TaskPath: fmt.Sprintf("/tmp/task-mounts/%s", id), 274 HostPath: filepath.Join(deviceDir, id), 275 ReadOnly: false, 276 }) 277 } 278 279 return resp, nil 280 } 281 282 // Stats streams statistics for the detected devices. 283 func (d *FsDevice) Stats(ctx context.Context, interval time.Duration) (<-chan *device.StatsResponse, error) { 284 outCh := make(chan *device.StatsResponse) 285 go d.stats(ctx, outCh, interval) 286 return outCh, nil 287 } 288 289 // stats is the long running goroutine that streams device statistics 290 func (d *FsDevice) stats(ctx context.Context, stats chan *device.StatsResponse, interval time.Duration) { 291 defer close(stats) 292 293 // Create a timer that will fire immediately for the first detection 294 ticker := time.NewTimer(0) 295 296 for { 297 select { 298 case <-ctx.Done(): 299 return 300 case <-ticker.C: 301 ticker.Reset(interval) 302 } 303 304 deviceStats, err := d.collectStats() 305 if err != nil { 306 stats <- &device.StatsResponse{ 307 Error: err, 308 } 309 return 310 } 311 if deviceStats == nil { 312 continue 313 } 314 315 stats <- &device.StatsResponse{ 316 Groups: []*device.DeviceGroupStats{deviceStats}, 317 } 318 } 319 } 320 321 func (d *FsDevice) collectStats() (*device.DeviceGroupStats, error) { 322 d.deviceLock.RLock() 323 defer d.deviceLock.RUnlock() 324 l := len(d.devices) 325 if l == 0 { 326 return nil, nil 327 } 328 329 now := time.Now() 330 group := &device.DeviceGroupStats{ 331 Vendor: vendor, 332 Type: deviceType, 333 Name: deviceName, 334 InstanceStats: make(map[string]*device.DeviceStats, l), 335 } 336 337 for k := range d.devices { 338 p := filepath.Join(d.deviceDir, k) 339 f, err := os.Stat(p) 340 if err != nil { 341 return nil, fmt.Errorf("failed to stat %q: %v", p, err) 342 } 343 344 s := &device.DeviceStats{ 345 Summary: &structs.StatValue{ 346 IntNumeratorVal: helper.Int64ToPtr(f.Size()), 347 Unit: "bytes", 348 Desc: "Filesize in bytes", 349 }, 350 Stats: &structs.StatObject{ 351 Attributes: map[string]*structs.StatValue{ 352 "size": { 353 IntNumeratorVal: helper.Int64ToPtr(f.Size()), 354 Unit: "bytes", 355 Desc: "Filesize in bytes", 356 }, 357 "modify_time": { 358 StringVal: helper.StringToPtr(f.ModTime().String()), 359 Desc: "Last modified", 360 }, 361 "mode": { 362 StringVal: helper.StringToPtr(f.Mode().String()), 363 Desc: "File mode", 364 }, 365 }, 366 }, 367 Timestamp: now, 368 } 369 370 group.InstanceStats[k] = s 371 } 372 373 return group, nil 374 }