-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added: Functionality to select an Ansible playbook specified in the config file. Live updating of the list of Ansible playbooks available if the config file is edited while the Meteor server is running. Functionality to run a selected Ansible playbook on a specified host. Logging of the output of the Ansible command on the UI for manual reviewing of output. * Changed .run to .popen prevents blocking caused by .run * Added dummy data in testing environment * Changed formatting * Storing playbooks as mongo collection, show playbooks in dropdown Potentially could cause a future issue if new playbooks need to be added, could need * Created worst meteor method known to man Not really, just not sure what the meteor way of grabbing the selected option from the selection box is so we're rockin with the .closest call * alt shift f * Added ansible-playbook command and file reading/writing for ansible inventory file Now ready for testing * ensure openssh server will accept root login on clients * Added ansible command to client.provision Also added inventory file creation to startup command instead of method call Also changed structure of inventory file to better reflect what we're actually trying to do. * restart ssh service after editing the config * Added ssh key checking and adding to ansible command * added ansible configuration to default test * Updated Ansible command * Update meteor-dev.tar.gz * Fixed ansible command formatting * Update meteor-dev.tar.gz * Updated playbook exec call to use spawn() Updated genisys inventory to include a provisioned field. Added delete host button to UI w/ functionality. * Changed return values on non-zero exit code from ansible * Cleaned up ansible command output into plain text, placed server methods in separate file * Delete genisys/server/external/meteor-dev.tar.gz In light of recent CVEs, having compressed/unauditable files in repo is probably not a good idea. * Added logging of output from ansible commands, still WIP - Currently doesn't get any output from the ansible commands for some reason, still troubleshooting * Implemented output log viewing/storage * Added case for undefined log request, cleaned formatting * Replaced depreciated pkg_resources with importlib utils. * Return early * Added live updating of playbooks added to config file * Updated error message * Removed commented code --------- Co-authored-by: Robert Gingras <developer@three-point-five.dev>
- Loading branch information
1 parent
9d01249
commit b50414c
Showing
20 changed files
with
390 additions
and
108 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { Mongo } from 'meteor/mongo'; | ||
|
||
export const AnsibleCollection = new Mongo.Collection('AnsibleCollection'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { Mongo } from 'meteor/mongo'; | ||
|
||
export const OutputLogsCollection = new Mongo.Collection('OutputLogs'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { Mongo } from 'meteor/mongo'; | ||
|
||
export const PlaybooksCollection = new Mongo.Collection('PlaybooksCollection') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,65 +1,183 @@ | ||
import { | ||
Meteor | ||
} from 'meteor/meteor'; | ||
import { | ||
check | ||
} from 'meteor/check'; | ||
import { ClientsCollection } from '../clients'; | ||
import fs from 'fs'; | ||
import { Meteor } from "meteor/meteor" | ||
import { check } from "meteor/check" | ||
import { ClientsCollection } from "../clients" | ||
import { AnsibleCollection } from "../ansible" | ||
import { OutputLogsCollection } from "../outputLogs" | ||
const { spawn } = require("child_process") | ||
import fs from "fs" | ||
const yaml = require("js-yaml") | ||
import { CONFIG_FILE_VAR } from "../../../server/serverMethods" | ||
import { PlaybooksCollection } from "../playbooks" | ||
|
||
Meteor.methods({ | ||
'Clients.Provision': function (clientId) { | ||
check(clientId, String); | ||
"Clients.Provision": function (clientId, playbook) { | ||
check(clientId, Mongo.ObjectID) | ||
|
||
const client = ClientsCollection.findOne({ | ||
_id: clientId | ||
}); | ||
// Find client in MongoDB | ||
const client = ClientsCollection.findOne({ | ||
_id: clientId, | ||
}) | ||
|
||
if (!client) { | ||
throw new Meteor.Error('client-not-found', 'That client doesn\'t exist.'); | ||
// Check if client exists | ||
if (!client) { | ||
throw new Meteor.Error("client-not-found", "That client doesn't exist.") | ||
} | ||
|
||
// Check if playbook as been selected | ||
if (playbook.localeCompare("None") === 0) { | ||
throw new Meteor.Error( | ||
"invalid-playbook", | ||
"Please select a playbook to run." | ||
) | ||
} | ||
|
||
// Reading inventory file and adding hostname to inventory file | ||
fs.readFile("inventory", "utf8", function (err, fileContent) { | ||
if (err) { | ||
console.error(`Error reading file: ${err}`) | ||
return | ||
} | ||
|
||
// Check if the hostname is in the file | ||
if (fileContent.includes(client.hostname)) { | ||
console.log(`${client.hostname} already exists in the file.`) | ||
} else { | ||
// If 'client.hostname' is not in the file, add it to the end of the file | ||
fileContent += `${client.hostname} ansible_host=${client.ip}\n` | ||
|
||
// Write the modified content back to the file | ||
fs.writeFile("inventory", fileContent, "utf8", function (err) { | ||
if (err) { | ||
console.error(`Error writing to file: ${err}`) | ||
return | ||
} | ||
console.log(`${client.hostname} added to the file.`) | ||
console.log(`Content of file: ${fileContent}`) | ||
}) | ||
} | ||
}) | ||
|
||
// Building Ansible command | ||
let command = "ansible-playbook" | ||
let cmd_args = [ | ||
`-i`, | ||
`inventory`, | ||
`${playbook}`, | ||
`--limit`, | ||
`${client.hostname}`, | ||
`--ssh-common-args`, | ||
`'-o StrictHostKeyChecking=no'`, | ||
`--user`, | ||
`root`, | ||
] | ||
|
||
// If SSH key found, append to command | ||
const ansibleObject = AnsibleCollection.findOne({ | ||
"ssh-key": { $exists: true }, | ||
}) | ||
if (ansibleObject) { | ||
cmd_args.push(`--private-key`, `${ansibleObject["ssh-key"]}`) | ||
} | ||
|
||
// Run the command | ||
const commandResult = spawn(command, cmd_args) | ||
|
||
let cmd_result = "" | ||
// Print the output of the command as ASCII and log output in MongoDB | ||
commandResult.stdout.on("data", function (data) { | ||
function hexBufferToString(buffer) { | ||
const hexString = buffer.toString("hex") | ||
const hexPairs = hexString.match(/.{1,2}/g) | ||
const asciiString = hexPairs | ||
.map((hex) => String.fromCharCode(parseInt(hex, 16))) | ||
.join("") | ||
return asciiString | ||
} | ||
res = hexBufferToString(data) | ||
|
||
cmd_result += res | ||
}) | ||
|
||
// Return error if cmd_args are invalid/formatted incorrectly | ||
// NOTE: This only runs on errors associated with formatting the command, | ||
// if the Ansible command fails because of something like the host being | ||
// unreachable this will not trigger. | ||
commandResult.stderr.on("data", function (data) { | ||
console.error(data) | ||
return { | ||
status: 400, | ||
message: `${client.hostname} failed provisioning due to command error, potential formatting issue with SSH key, playbook, or client hostname`, | ||
} | ||
}) | ||
|
||
commandResult.on( | ||
"close", | ||
Meteor.bindEnvironment((code) => { | ||
console.log(`ansible-playbook returned exit code ${code}`) | ||
|
||
playbookTimestamp = new Date().getTime() | ||
logLabel = `${client.hostname}-${playbook}-${playbookTimestamp}` | ||
|
||
// Insert log into mongodb | ||
OutputLogsCollection.insert({ | ||
label: logLabel, | ||
text: cmd_result, | ||
timestamp: playbookTimestamp, | ||
}) | ||
console.log("Logged:\n", cmd_result, "\nas:", logLabel) | ||
|
||
if (code != 0) { | ||
return { | ||
status: 400, | ||
message: `Ansible returned exit code ${code} while provisioning ${client.hostname}. Please see output log ${logLabel} on web UI for details.`, | ||
} | ||
} | ||
|
||
fs.access('inventory', fs.constants.F_OK, (err) => { | ||
if (err) { | ||
console.log(`inventory does not exist`); | ||
fs.writeFileSync('inventory', ''); | ||
} else { | ||
console.log(`inventory exists`); | ||
} | ||
}); | ||
|
||
fs.readFileSync('inventory', function (file) { | ||
// check if client.hostname exists in file, do nothing | ||
// if client.hostname is not in file, add to the end of the file and write the file out. | ||
console.log('file', file); | ||
}); | ||
|
||
|
||
|
||
ClientsCollection.update({ | ||
_id: clientId, | ||
}, { | ||
$set: { | ||
provisioned: true, | ||
provisionedAt: new Date() | ||
// provisionedBy: Meteor.user() | ||
} | ||
}); | ||
return { | ||
status: 200, | ||
message: `${client.hostname} successfully provisioned` | ||
}; | ||
status: 100, | ||
message: `Command exexuted on ${client.hostname}, see output logs to view Ansible details.`, | ||
} | ||
}) | ||
) | ||
}, | ||
"Clients.RemoveHost": function (clientId) { | ||
toDelete = ClientsCollection.findOne({ _id: clientId }) | ||
|
||
}, | ||
'TestYaml': function () { | ||
const exampleYaml = Assets.get('example.yml.tpl'); | ||
if (!toDelete) { | ||
throw new Meteor.Error("client-not-found", "That client doesn't exist.") | ||
} | ||
ClientsCollection.remove(toDelete) | ||
return { | ||
status: 200, | ||
message: 'Client successfully deleted.' | ||
} | ||
}, | ||
"Logs.GetSelected": function (logLabel) { | ||
const log = OutputLogsCollection.findOne({ label: logLabel }) | ||
|
||
const replace = { | ||
interface: 'eth0', | ||
subnet: '10.0.0.0/24' | ||
} | ||
// replace {{key}} with value | ||
// fs.writeFile... | ||
if (!log) { | ||
return "" | ||
} | ||
|
||
return log.text | ||
}, | ||
"RefreshConfig": async function () { | ||
console.log("Loading Playbook Collection") | ||
const CONFIG_FILE = yaml.load(fs.readFileSync(String(CONFIG_FILE_VAR), "utf8")) | ||
|
||
PlaybooksCollection.dropCollectionAsync() | ||
AnsibleCollection.dropCollectionAsync() | ||
|
||
// Load playbooks into Mongo | ||
CONFIG_FILE["ansible"]["playbooks"].forEach((element) => { | ||
obj = { playbook: element } | ||
PlaybooksCollection.insertAsync(obj) | ||
}) | ||
|
||
// Putting ansible SSH key location into mongo collection for usage on client | ||
if (CONFIG_FILE["ansible"]["ssh-key"]) { | ||
obj = { "ssh-key": CONFIG_FILE["ansible"]["ssh-key"] } | ||
AnsibleCollection.insertAsync(obj) | ||
} | ||
}); | ||
}, | ||
}) |
Oops, something went wrong.