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  }