This guide provides detailed instructions for creating and integrating new plugins into NetScout-Pi.
NetScout-Pi uses a modular plugin system where each plugin is contained in its own directory with configuration and implementation files. This architecture makes it easy to add, remove, or update plugins independently without affecting the core application.
Each plugin consists of:
- A plugin.json file defining metadata and parameters
- A plugin.go file implementing the plugin's functionality
Plugins reside under the app/plugins/plugins/ directory:
app/plugins/plugins/
├── ping/
│ ├── plugin.json # Plugin metadata and parameters
│ └── plugin.go # Plugin implementation
├── traceroute/
│ ├── plugin.json
│ └── plugin.go
└── ...
Create a new directory for your plugin under app/plugins/plugins/:
mkdir -p app/plugins/plugins/my_pluginCreate a plugin.json file that defines your plugin's metadata and parameters:
{
"id": "my_plugin",
"name": "My Plugin",
"description": "Description of what your plugin does",
"icon": "custom_icon",
"parameters": [
{
"id": "param1",
"name": "Parameter 1",
"description": "Description of this parameter",
"type": "string",
"required": true,
"default": "default value"
},
{
"id": "param2",
"name": "Parameter 2",
"description": "Another parameter",
"type": "number",
"required": false,
"default": 10,
"min": 1,
"max": 100,
"step": 1
}
]
}The plugin system supports the following parameter types:
string: Text input fieldnumber: Numeric input with optional min/max/stepboolean: True/false checkboxselect: Dropdown selection with optionsrange: Range slider with min/max/step
Each parameter can have the following properties:
| Property | Description | Required | Applies To |
|---|---|---|---|
| id | Unique identifier for the parameter | Yes | All |
| name | Display name for the parameter | Yes | All |
| description | Help text for the parameter | Yes | All |
| type | Parameter type (string, number, boolean, select, range) | Yes | All |
| required | Whether the parameter is required | Yes | All |
| default | Default value for the parameter | No | All |
| min | Minimum allowed value | No | number, range |
| max | Maximum allowed value | No | number, range |
| step | Increment step | No | number, range |
| options | Array of options for select type | Yes | select |
For select parameters, the options property is an array of objects with value and label properties:
"options": [
{"value": "option1", "label": "Option 1"},
{"value": "option2", "label": "Option 2"}
]Create a plugin.go file that implements your plugin's functionality:
package my_plugin
import (
"fmt"
"time"
)
// Execute handles the plugin execution
func Execute(params map[string]interface{}) (interface{}, error) {
// Get parameters from the params map
param1, _ := params["param1"].(string)
param2Raw, ok := params["param2"].(float64)
if !ok {
param2Raw = 10 // Default value
}
param2 := int(param2Raw)
// Implement your plugin logic here
// This is just an example - replace with your actual implementation
result := map[string]interface{}{
"param1": param1,
"param2": param2,
"result": "Your plugin's result",
"timestamp": time.Now().Format(time.RFC3339),
}
return result, nil
}Important points for plugin implementation:
- Your plugin package name should match the directory name
- The
Executefunction is the entry point for your plugin - Parameters are passed as a map[string]interface{} and need to be type-asserted
- Always provide fallback values for optional parameters
- Return results as a map[string]interface{} for JSON serialization
- Include error handling for potential failures
- Include a timestamp in your results
Open app/plugins/loader.go and update the getPluginExecuteFunc method to include your new plugin:
func (pl *PluginLoader) getPluginExecuteFunc(pluginName string) (func(map[string]interface{}) (interface{}, error), error) {
switch pluginName {
// ... existing plugins ...
case "my_plugin":
return my_plugin.Execute, nil
default:
return nil, fmt.Errorf("plugin implementation not found: %s", pluginName)
}
}In the same app/plugins/loader.go file, add an import for your plugin package:
import (
// ... existing imports ...
"github.com/anoam/netscout-pi/app/plugins/plugins/my_plugin"
)By default, the frontend displays plugin results in a generic JSON format. If you want a custom display format for your plugin, you'll need to:
Modify app/static/js/plugin-manager.js to add a custom display function for your plugin:
// Display plugin results
displayResults: function(data, element) {
// ... existing code ...
switch (this.activePluginId) {
// ... existing plugins ...
case 'my_plugin':
this.displayMyPluginResults(data, element);
break;
// ... existing default case ...
}
},
// Custom display function for your plugin
displayMyPluginResults: function(data, element) {
element.innerHTML = `
<div class="result-card">
<div class="result-header">My Plugin Results</div>
<div class="result-body">
<div class="result-row">
<div class="result-label">Parameter 1</div>
<div class="result-value">${data.param1}</div>
</div>
<div class="result-row">
<div class="result-label">Parameter 2</div>
<div class="result-value">${data.param2}</div>
</div>
<div class="result-row">
<div class="result-label">Result</div>
<div class="result-value">${data.result}</div>
</div>
</div>
</div>
`;
}If your plugin requires special handling in the template, modify app/templates/plugin_page.html.
You can add custom styles for your plugin by:
- Creating a new CSS file in
app/static/css/directory - Referencing it in your custom display function
- Or updating the existing
style.cssfile with plugin-specific styles
-
Build and run the application:
go build ./netscout-pi
-
Navigate to your plugin page at
/plugin/my_plugin -
Configure the parameters and run the plugin
-
Check the results and API response at
/api/plugins/my_plugin/run
-
Check the logs: Look for any error messages in the server logs
-
Test the API directly: Use curl or Postman to call your plugin's API endpoint:
curl -X POST http://localhost:8080/api/plugins/my_plugin/run \ -H "Content-Type: application/json" \ -d '{"param1": "value1", "param2": 42}'
-
Validate plugin.json: Ensure your plugin.json file is valid JSON
-
Check for import errors: Make sure your plugin is properly imported in loader.go
To run external commands in your plugin:
cmd := exec.Command("command", "arg1", "arg2")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return nil, fmt.Errorf("command failed: %v: %s", err, stderr.String())
}
output := stdout.String()
// Process output...For long-running operations, consider implementing timeout handling:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "command", "args")
// ...Always provide meaningful error messages and proper error handling:
if err != nil {
return nil, fmt.Errorf("failed to perform operation: %w", err)
}If your plugin requires external dependencies:
- Add them to the project's
go.modfile - Import them in your plugin.go file
- Document the dependencies in your plugin documentation
Create unit tests for your plugin in a plugin_test.go file:
package my_plugin
import (
"testing"
"reflect"
)
func TestExecute(t *testing.T) {
// Define test cases
testCases := []struct {
name string
params map[string]interface{}
expectedError bool
}{
{
name: "Basic test",
params: map[string]interface{}{
"param1": "test",
"param2": float64(10),
},
expectedError: false,
},
// Add more test cases...
}
// Run test cases
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := Execute(tc.params)
// Check error
if tc.expectedError && err == nil {
t.Errorf("Expected error but got none")
}
if !tc.expectedError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
// Validate result
if err == nil {
// Add assertions for expected results
}
})
}
}For reference, explore these existing plugins:
- Ping: Simple network connectivity test
- Traceroute: Network path tracing
- Port Scanner: Network port scanning
- DNS Lookup: Domain name resolution
When choosing an icon for your plugin, use one of the following values:
network: Network/connectivity iconping: Ping/echo icondns: DNS/domain iconport: Port/service iconroute: Route/path iconspeed: Speed/performance iconinfo: Information iconscan: Scanning iconcustom_icon: Your custom icon (requires additional frontend changes)
When submitting a new plugin, ensure:
- Your plugin follows the structure outlined in this guide
- Include comprehensive documentation in your code
- Add appropriate error handling
- Create unit tests for your plugin
- Update the plugin loader to include your plugin
- Add any custom frontend elements needed for display
- Document any dependencies or special requirements
When developing plugins that run system commands:
- Validate and sanitize all user inputs to prevent command injection
- Use specific command paths instead of relying on PATH environment
- Limit permissions to only what is necessary
- Avoid running commands as root/sudo unless absolutely necessary
- Implement timeouts for all operations
- Sanitize and validate command output before returning to the client
- Type Assertion Errors: Always check the type assertion with the "comma ok" idiom
- Missing Parameters: Provide fallback values for all parameters
- Race Conditions: Use proper synchronization for concurrent operations
- Resource Leaks: Close all resources (files, connections) with defer statements
- Large Response Data: Consider pagination or limiting data size for large results
If your plugin doesn't appear in the dashboard:
- Check that the plugin directory and files are named correctly
- Verify that plugin.json is valid
- Ensure the plugin is properly imported in loader.go
- Check the server logs for any error messages
If your plugin fails to execute:
- Test the plugin via the API directly
- Check parameter handling in your Execute function
- Verify that any external commands or dependencies are available
- Check for permission issues if running system commands