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