package plugin import ( "context" "encoding/json" "errors" "fmt" "github.com/Akilan1999/p2p-rendering-computation/client" "github.com/Akilan1999/p2p-rendering-computation/config" "github.com/google/uuid" "gopkg.in/yaml.v2" "io/ioutil" "net" "os" "strconv" "text/template" "github.com/apenella/go-ansible/pkg/execute" "github.com/apenella/go-ansible/pkg/options" "github.com/apenella/go-ansible/pkg/playbook" "github.com/apenella/go-ansible/pkg/stdoutcallback/results" "github.com/otiai10/copy" ) // Plugins Array of all plugins detected type Plugins struct { PluginsDetected []*Plugin } // Plugin Information about the plugins available type Plugin struct { FolderName string PluginDescription string path string Execute []*ExecuteIP NumOfPorts int } // ExecuteIP IP Address to execute Ansible instruction type ExecuteIP struct { ContainerID string IPAddress string SSHPortNo string Success bool } // Host Struct for ansible host // Generated from https://zhwt.github.io/yaml-to-go/ type Host struct { All struct { Vars struct { AnsiblePythonInterpreter string `yaml:"ansible_python_interpreter"` } `yaml:"vars"` } `yaml:"all"` Main struct { Hosts struct { Host1 struct { AnsibleHost string `yaml:"ansible_host"` AnsiblePort int `yaml:"ansible_port"` AnsibleUser string `yaml:"ansible_user"` AnsibleSSHPass string `yaml:"ansible_ssh_pass"` AnsibleSudoPass string `yaml:"ansible_sudo_pass"` } `yaml:"host1"` } `yaml:"hosts"` } `yaml:"main"` } // DetectPlugins Detects all the plugins available func DetectPlugins() (*Plugins, error) { config, err := config.ConfigInit(nil) if err != nil { return nil, err } folders, err := ioutil.ReadDir(config.PluginPath) if err != nil { return nil, err } var plugins *Plugins = new(Plugins) for _, f := range folders { if f.IsDir() { //Declare variable plugin of type Plugin var plugin Plugin // Setting name of folder to plugin plugin.FolderName = f.Name() // Getting Description from file description.txt Description, err := ioutil.ReadFile(config.PluginPath + "/" + plugin.FolderName + "/description.txt") // if we os.Open returns an error then handle it if err != nil { return nil, err } // Get Description from description.txt plugin.PluginDescription = string(Description) // Set plugin path plugin.path = config.PluginPath + "/" + plugin.FolderName plugins.PluginsDetected = append(plugins.PluginsDetected, &plugin) // Get the number of ports the plugin needs err = plugin.NumPorts() if err != nil { return nil, err } } } return plugins, nil } // SearchPlugin Detects plugin information based on the // name provided on the parameter func SearchPlugin(pluginname string) (*Plugin, error) { plugins, err := DetectPlugins() if err != nil { return nil, err } // loop ot find the plugin name that matches for _, plugin := range plugins.PluginsDetected { if pluginname == plugin.FolderName { return plugin, nil } } return nil, errors.New("plugin not detected") } // RunPlugin Executes plugins based on the plugin name provided func RunPlugin(pluginName string, IPAddresses []*ExecuteIP) (*Plugin, error) { plugins, err := DetectPlugins() if err != nil { return nil, err } // Variable to store struct information about the plugin var plugindetected *Plugin for _, plugin := range plugins.PluginsDetected { if plugin.FolderName == pluginName { plugindetected = plugin plugindetected.Execute = IPAddresses // Get Execute plugin path from config file config, err := config.ConfigInit(nil) if err != nil { return nil, err } plugindetected.path = config.PluginPath break } } if plugindetected == nil { return nil, errors.New("Plugin not detected") } // Create copy of the plugin the tmp directory // To ensure we execute the plugin from there err = plugindetected.CopyToTmpPlugin() if err != nil { return nil, err } // Executing the plugin err = plugindetected.ExecutePlugin() if err != nil { return nil, err } return plugindetected, nil } // ExecutePlugin Function to execute plugins that are called func (p *Plugin) ExecutePlugin() error { // Run ip address to execute ansible inside for _, execute := range p.Execute { // Modify ansible hosts before executing err := execute.ModifyHost(p) if err != nil { return err } // sets the ports to the plugin folder err = p.AutoSetPorts(execute.ContainerID) if err != nil { return err } err = execute.RunAnsible(p) if err != nil { return err } // If ran successfully then change success flag to true execute.Success = true } return nil } // RunAnsible Executes based on credentials on the struct func (e *ExecuteIP) RunAnsible(p *Plugin) error { ansiblePlaybookConnectionOptions := &options.AnsibleConnectionOptions{ User: "master", } ansiblePlaybookOptions := &playbook.AnsiblePlaybookOptions{ Inventory: p.path + "/" + p.FolderName + "/hosts", ExtraVars: map[string]interface{}{"ansible_ssh_common_args": "-o StrictHostKeyChecking=no"}, } ansiblePlaybookPrivilegeEscalationOptions := &options.AnsiblePrivilegeEscalationOptions{ Become: true, } playbook := &playbook.AnsiblePlaybookCmd{ Playbooks: []string{p.path + "/" + p.FolderName + "/site.yml"}, ConnectionOptions: ansiblePlaybookConnectionOptions, PrivilegeEscalationOptions: ansiblePlaybookPrivilegeEscalationOptions, Options: ansiblePlaybookOptions, Exec: execute.NewDefaultExecute( execute.WithTransformers( results.Prepend("success"), ), ), } err := playbook.Run(context.TODO()) if err != nil { return err } return nil } // ModifyHost adds IP address , port no to the config file func (e *ExecuteIP) ModifyHost(p *Plugin) error { host, err := ReadHost(p.path + "/" + p.FolderName + "/hosts") if err != nil { return err } // Setting ansible host host.Main.Hosts.Host1.AnsibleHost = e.IPAddress // Setting SSH port no host.Main.Hosts.Host1.AnsiblePort, err = strconv.Atoi(e.SSHPortNo) if err != nil { return err } // Setting SSH user name host.Main.Hosts.Host1.AnsibleUser = "master" // Setting SSH password host.Main.Hosts.Host1.AnsibleSSHPass = "password" // Setting SSH sudo password host.Main.Hosts.Host1.AnsibleSudoPass = "password" // write modified information to the hosts yaml file data, err := yaml.Marshal(host) if err != nil { return err } err = ioutil.WriteFile(p.path+"/"+p.FolderName+"/hosts", data, 0777) if err != nil { return err } return nil } // ReadHost Reads host file and adds func ReadHost(filename string) (*Host, error) { buf, err := ioutil.ReadFile(filename) if err != nil { return nil, err } c := &Host{} err = yaml.Unmarshal(buf, c) if err != nil { return nil, fmt.Errorf("in file %q: %v", filename, err) } return c, nil } // RunPluginContainer Runs ansible plugin based on plugin name and container name which // is derived from the tracked containers file // We pass in the group ID as a parameter because when we modify the ports taken func RunPluginContainer(PluginName string, ContainerID string) error { // Gets container information based on container ID ContainerInformation, err := client.GetContainerInformation(ContainerID) if err != nil { return err } // Setting Up IP's for which the plugins will be executed var ExecuteIPs []*ExecuteIP var ExecuteIP ExecuteIP // Getting port no of SSH port for _, port := range ContainerInformation.Container.Ports.PortSet { if port.PortName == "SSH" { ExecuteIP.SSHPortNo = fmt.Sprint(port.ExternalPort) break } } // Handle error if SSH port is not provided if ExecuteIP.SSHPortNo == "" { return errors.New("SSH port not found") } // Split the port no from ip address since current the IP address // field is populated as // : host, _, err := net.SplitHostPort(ContainerInformation.IpAddress) if err != nil { return err } // IP address of the container ExecuteIP.IPAddress = host // Set container ID to ExecutorIP ExecuteIP.ContainerID = ContainerInformation.Id // Append IP to list of executor IP ExecuteIPs = append(ExecuteIPs, &ExecuteIP) // Run plugin to execute plugin _, err = RunPlugin(PluginName, ExecuteIPs) if err != nil { return err } return nil } // CheckRunPlugin Checks if the ID belongs to the group or container // calls the plugin function the appropriate amount of times func CheckRunPlugin(PluginName string, ID string) error { // Check if the ID belongs to the group or container ID id, err := client.CheckID(ID) if err != nil { return err } // When the ID belongs to a group if id == "group" { // gets the group information group, err := client.GetGroup(ID) if err != nil { return err } // Iterate through each container information in the group // and run the plugin in each of them for _, container := range group.TrackContainerList { // runs plugin for each container err := RunPluginContainer(PluginName, container.Id) if err != nil { return err } } } else { // This means the following ID is a container ID err := RunPluginContainer(PluginName, ID) if err != nil { return err } } return nil } // CopyToTmpPlugin This function would ensure that we create a copy of the // plugin in the tmp directory, and it would be executed // from there. This due to the reason of automating port allocation // when running plugins func (p *Plugin) CopyToTmpPlugin() error { // generate rand to UUID this is debug the ansible file if needed id := uuid.New() // copies the plugin to the tmp directory err := copy.Copy(p.path+"/"+p.FolderName, "/tmp/"+id.String()+"_"+p.FolderName) if err != nil { return err } // Set the plugin execution to the tmp location p.path = "/tmp" p.FolderName = id.String() + "_" + p.FolderName return nil } // AutoSetPorts Automatically maps free ports to site.yml file func (p *Plugin) AutoSetPorts(containerID string) error { container, err := client.GetContainerInformation(containerID) if err != nil { return err } // variable that would have a list of ports // to be allocated to the plugin system var ports []int // Counted that increments when a port is taken PortTaken := 0 // setting all external ports available in an array for i, port := range container.Container.Ports.PortSet { if port.IsUsed == false { // Ensuring we break outside the loop once the ports // are set. if PortTaken >= p.NumOfPorts { break } // Setting the following port flag to true container.Container.Ports.PortSet[i].IsUsed = true // Incrementing the variable PortTaken PortTaken++ // Maps to internal since // Inside the machine // internal port -> (maps) same internal port // TURN (i.e FRP) based approach (internal port -> maps to different external port) ports = append(ports, port.InternalPort) } } // parses the site.yml file in the tmp directory t, err := template.ParseFiles(p.path + "/" + p.FolderName + "/site.yml") if err != nil { return err } // opens the output file f, err := os.Create(p.path + "/" + p.FolderName + "/site.yml") if err != nil { return err } // sends the ports to the site.yml file to populate them err = t.Execute(f, ports) if err != nil { return err } // Once the following is done set port to taken // n tracked container list err = container.ModifyContainerInformation() if err != nil { return err } // Once the following is done set port to taken // I(Groups) err = container.ModifyContainerGroups() if err != nil { return err } return nil } // NumPorts Gets the Number the ports the // plugin requires func (p *Plugin) NumPorts() error { jsonFile, err := os.Open(p.path + "/ports.json") // if we os.Open returns an error then handle it if err != nil { return err } // defer the closing of our jsonFile so that we can parse it later on defer jsonFile.Close() // read our opened xmlFile as a byte array. byteValue, _ := ioutil.ReadAll(jsonFile) // we unmarshal our byteArray which contains our // jsonFile's content into 'users' which we defined above json.Unmarshal(byteValue, &p) return nil }