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  }