commit e88675331c0ccefae4698b43fe40723850d17f8f Author: kprkpr Date: Wed Mar 13 11:06:13 2019 +0100 First commit, only with CLI tools diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90bcb97 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +**/thumbs.bd +*.bak +api.log +client/config.ps1 +BD/database* +reports/ +__pycache__/ +.vscode/ +cooks/*.yaml \ No newline at end of file diff --git a/BD/emptydatabase.db b/BD/emptydatabase.db new file mode 100644 index 0000000..99fc337 Binary files /dev/null and b/BD/emptydatabase.db differ diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..2baa61d --- /dev/null +++ b/README.MD @@ -0,0 +1,51 @@ +# What is "Powerful Computer Manager"? +Powerful Computer Manager is the super-simple-but-powerful program to automatizate tasks in groups of computers (In a Active Directory site, or not) in a unattended way. +It inteed to be a simple SSMS alternative, that inventarizes computers and install/remove software in it, tweak system of run arbitrary commands. + +You only have to write simple _cooks_ that are small yaml text files, create groups of computers and assign _cooks_ to this computers, and will be run in the destination computers + + +# Disclaimer + +I'm using this program in the company I work with 10 computers with Windows 8.1/10 with various software (MSI,EXE,...) and tweaking services, and it works right + +It's not super-stable and error proof, then, I don't have any responsability if something crashes, but you can open a Issue and will see it + +# Roadmap + +GUI administration in Python to superseed Powershell CLI control program and be cross-platform + +# Requirements + +## Server +For server you can use Linux or Windows, and in theory any environment that can run Python 3 can work as server +- Python3 with: + - Falcon + - Waitress-serve + - Json (Python3 have it preinstalled) + - Yaml + Python3 deps can be installed it using pip install command (pip install waitress falcon pyyaml) +- For controlling the program, you have to use "control.ps1" using Powershell. Powershell comes with Windows, and can be installed (PWCore) in Linux and macOS (I didn't tested it). + +## Clients +- Windows: + - Windows 7 SP1 absolute minimal, but its only officially tested in 8.1 and 10, Windows 7 is being old now. + - It needs Powershell 5.0+, lower versions doesn't have some commands, not tested. In Windows 7 and 8.1, you have to update Powershell. (With Powershell 3.0/4.0, some commands like Get-Package doesn't work) + You can update from here: https://docs.microsoft.com/en-us/powershell/scripting/setup/installing-windows-powershell?view=powershell-6#upgrading-existing-windows-powershell + +- Linux: + - Not supported yet, but when Windows be stable, I will try to do it in some way + + +# Instalation. Example installing Server and using a GPO of Active Directory/Task Schedule to make it work in clients +- Copy this folder to a folder in your server +- Install dependencies +- Open sysopt.py file and change values if needed +- Use bat file if you are in Windows to run the python server, or sh file if you are in Linux environment +- Go to Client folder and copy "configexample.ps1" to "config.ps1" and change their values to yours. +- For clients to work, they have to run "client\client.ps1" every 30 mins/1 hour or so, I did using Active Directory and a Task Scheduled GPO (Copying client.ps1 and config.ps1 files from Client to SysVol\DomainName\Scripts\) + - User: SYSTEM (Or NT\SYSTEM) + - Command: powershell.exe + - Args: -executionpolicy bypass -windowstyle hidden -noninteractive -nologo -file "\\SERVER\SysVol\DOMAINNAME\scripts\client.ps1" + Note: client.ps1 has to have control.ps1 file with it +- If you don't have Active Directory, you can do the Task Scheduled task in every computer without problems, and using another Shared folder, it will work \ No newline at end of file diff --git a/api.py b/api.py new file mode 100644 index 0000000..39d5502 --- /dev/null +++ b/api.py @@ -0,0 +1,541 @@ +#!/usr/bin/python3 +# Build 5 +##It can be run directly with "waitress-serve --port=3333 api:api" +import falcon +import random +import os +import time +import sql # SQL work file +import json,yaml +from datetime import datetime + +import sysopt #File of options + +def logit(text): + text = str(text) + now = datetime.now() + print ("Log: " + text) + with open('api.log', 'a') as file: + file.write("Log ("+now.strftime("%x %X") + "): ") + file.write(text) + file.write('\n') + +class getComputerExists(object): + def on_get(self, request, response): + logit(request) + ComputerName,UUID = None, None + for key, value in request.params.items(): + if key == "ComputerName": + ComputerName = value + if key == "UUID": + UUID = value + + if ComputerName is None: + response.media = {'RESULT': '0'} + else: + result = sql.select("SELECT COUNT(*) 'RESULT' FROM COMPUTERS WHERE Name='"+ComputerName+"' AND UUID='"+UUID+"'") + + if result[0]['RESULT'] == 0: # 0 or 1 + if sql.select("SELECT COUNT(*) 'RESULT' FROM COMPUTERS WHERE Name='"+ComputerName+"' AND UUID IS NULL")[0]['RESULT'] != 0: + sql.insert("UPDATE COMPUTERS SET UUID='"+UUID+"' WHERE Name='"+ComputerName+"'") + response.media = {'RESULT': '1'} + return + if sysopt.addComputers == True: + sql.insert("INSERT INTO COMPUTERS (`Name`,`UUID`) VALUES('"+ComputerName+"','"+UUID+"')") + response.media = {'RESULT': '1'} + else: + response.media = {'RESULT': '0'} + else: #Exists + response.media = {'RESULT': '1'} + +class getGroups(object): #Get local groups of a Computer + def on_get(self,request, response): + logit(request) + ComputerID=None + for key, value in request.params.items(): + if key == "ComputerName": + ComputerID = str(sql.select("SELECT ID_C FROM COMPUTERS WHERE Name='"+value+"'")[0]['ID_C']) + if key == "ComputerID": + ComputerID = value + + if ComputerID is None: + data = sql.select("SELECT * FROM GROUPS") + response.media = data + else: + data = sql.select("SELECT * FROM GROUPS WHERE (SELECT ID_G FROM COMPUTER_GROUP WHERE ID_C = '"+ComputerID+"')") + response.media = data + +class getComputers(object): + def on_get(self,request, response): + logit(request) + ComputerID=None + for key, value in request.params.items(): + if key == "ComputerName": + ComputerID = str(sql.select("SELECT ID_C FROM COMPUTERS WHERE Name='"+value+"'")[0]['ID_C']) + if key == "ComputerID": + ComputerID = value + + if ComputerID is None: + data = sql.select("SELECT * FROM COMPUTERS") + response.media = data + else: + data = sql.select("SELECT * FROM COMPUTERS WHERE ID_C = '"+ComputerID+"')") + response.media = data + +class getComputersGrp(object): #List of computers in a group + def on_get(self,request, response): + logit(request) + GroupID=None + for key, value in request.params.items(): + if key == "GroupName": + GroupID = str(sql.select("SELECT ID_G FROM GROUPS WHERE Name='"+value+"'")[0]['ID_G']) + if key == "GroupID": + GroupID = value + data = sql.select("SELECT * FROM COMPUTERS WHERE ID_C IN (SELECT ID_C FROM COMPUTER_GROUP WHERE ID_G='"+GroupID+"') ORDER BY Name") + response.media = data + +class getCook(object): # Get the list of cooks for a computer to implement (Var all=1 for see all of, implemented or not) + def on_get(self,request, response): + logit(request) + ComputerID,GroupID, SeeAll =None, None, None + for key, value in request.params.items(): + if key == "ComputerName": + ComputerID = str(sql.select("SELECT ID_C FROM COMPUTERS WHERE Name='"+value+"'")[0]['ID_C']) + if key == "ComputerID": + ComputerID = value + if key == "GroupName": + GroupID = str(sql.select("SELECT ID_G FROM GROUPS WHERE Name='"+value+"'")[0]['ID_G']) + if key == "GroupID": + GroupID = value + if key == "SeeAll": + SeeAll = str(value) + + if ComputerID is None and GroupID is None: + response.media = {'ERROR': 'I need a Group or Computer to search'} + elif ComputerID is not None and SeeAll is None: + data = sql.select("SELECT * FROM COOKS_IDG WHERE ID_G IN (SELECT ID_G FROM COMPUTER_GROUP WHERE ID_C = '"+ComputerID+"')") #All cooks for this computer + fordelete = [] + for i in range(len(data)): + for key, value in data[i].items(): #Iterate in a dict (json) + if key == 'CookName': + CookName = value #Name + with open('cooks/'+CookName+'.yaml', 'r') as myfile: + filecook = myfile.read() + coun = sql.select("SELECT COUNT(*) 'RESULT' FROM COOKS_STATUS WHERE ID_C='"+ComputerID+"' AND CookName='"+CookName+"' AND Revision='"+str(yaml.safe_load(filecook)['revision'])+"' AND `Error`='0'") + if coun[0]['RESULT'] == 1: + fordelete.append(i) #Its good, do not say to client + for x in reversed(fordelete): #Deleting cooks that are implemented in client. Reverse order (Because is an array and index..) + data.pop(x) + response.media = data + else: + response.media = sql.select("SELECT * FROM COOKS_IDG WHERE ID_G IN (SELECT ID_G FROM COMPUTER_GROUP WHERE ID_C = '"+ComputerID+"')") #All cooks for this computer + + +class setCook(object): #Assign Cook to group + def on_get(self, request, response): + logit(request) + GroupID, CookName = None, None # Initialize + for key, value in request.params.items(): + if key == "CookName": + exists = os.path.isfile('cooks/'+value+'.yaml') + CookName = value + if key == "GroupName": + GroupID = str(sql.select("SELECT ID_G FROM GROUPS WHERE Name='"+value+"'")[0]['ID_G']) + if key == "GroupID": + GroupID = value + if GroupID is None or exists is False: + response.media = {'ERROR': 'GroupID is not defined or Cook not exists'} + elif int(sql.select("SELECT COUNT(*) 'COUNT' FROM COOKS_IDG WHERE `ID_G`='"+GroupID+"' AND `CookName`='"+CookName+"'")[0]['COUNT']) > 0: + response.media = {'WARNING': 'This union GROUP-CookName exists'} + else: + result = sql.insert("INSERT INTO COOKS_IDG (`ID_G`,`CookName`) VALUES ('"+GroupID+"','"+CookName+"')") + response.media = result + +class delCookGrp(object): #Delete cook from a group + def on_get(self,request,response): + logit(request) + GroupID, CookName = None, None # Initialize + for key, value in request.params.items(): + if key == "CookName": + exists = os.path.isfile('cooks/'+value+'.yaml') + if exists: + CookName = value + if key == "GroupName": + GroupID = str(sql.select("SELECT ID_G FROM GROUPS WHERE Name='"+value+"'")[0]['ID_G']) + if key == "GroupID": + GroupID = value + + if GroupID is not None and CookName is not None: + result = sql.insert("DELETE FROM COOKS_IDG WHERE ID_G='"+GroupID+"' AND CookName='"+CookName+"'") + response.media = result + else: + response.media = {'RESULT': 'Error, no Group, or CookName does\'t exists'} + +class delEmptyPcsGroup(object): #Delete all computers from a group + def on_get(self,request,response): + logit(request) + GroupID = None # Initialize + for key, value in request.params.items(): + if key == "GroupName": + GroupID = str(sql.select("SELECT ID_G FROM GROUPS WHERE Name='"+value+"'")[0]['ID_G']) + if key == "GroupID": + GroupID = value + + if GroupID is not None: + result = sql.insert("DELETE FROM COMPUTER_GROUP WHERE ID_G='"+GroupID+"'") + response.media = result + else: + response.media = {'RESULT': 'Error, this group doesn\'t exists'} + + + +class getCookGrp(object): # Get cooks from a Group + def on_get(self,request,response): + logit(request) + GroupID = None # Initialize + for key, value in request.params.items(): + if key == "GroupName": + GroupID = str(sql.select("SELECT ID_G FROM GROUPS WHERE Name='"+value+"'")[0]['ID_G']) + if key == "GroupID": + GroupID = value + + if GroupID is not None: + result = sql.select("SELECT * FROM COOKS_IDG WHERE ID_G='"+GroupID+"'") + response.media = result + else: + response.media = {'RESULT': 'Error, no Group selected'} + +class getGrpCook(object): # Get Groups of a Cook + def on_get(self,request,response): + logit(request) + CookName = None # Initialize + for key, value in request.params.items(): + if key == "CookName": + exists = os.path.isfile('cooks/'+value+'.yaml') + if exists: + CookName = value + + if CookName is not None: + response.media = sql.select("SELECT Name FROM GROUPS WHERE ID_G IN (SELECT ID_G FROM COOKS_IDG WHERE CookName='"+CookName+"')") + else: + response.media = {'RESULT': 'Error, no Cook selected'} + +class getStatusCook(object): # Get Status of a Cook (If Brief=1 is sent too, brief status (Completed:X, updated:X..)) + def on_get(self,request,response): + logit(request) + CookName = None # Initialize + for key, value in request.params.items(): + if key == "CookName": + exists = os.path.isfile('cooks/'+value+'.yaml') + if exists: + CookName = value + + if CookName is not None: + response.media = sql.select("SELECT Name, Revision, Error FROM COMPUTERS,COOKS_STATUS WHERE CookName = '"+CookName+"' AND COMPUTERS.ID_C=COOKS_STATUS.ID_C") + else: + response.media = {'RESULT': 'Error, no Cook selected'} + +class getLastRevisionCook(object): # Get Number Revision (Revision=X) + def on_get(self,request,response): + logit(request) + CookName = None # Initialize + for key, value in request.params.items(): + if key == "CookName": + exists = os.path.isfile('cooks/'+value+'.yaml') + if exists: + CookName = value + + if CookName is not None: + with open('cooks/'+CookName+'.yaml', 'r') as myfile: + filecook = myfile.read() + response.media = {'Revision': str(yaml.safe_load(filecook)['revision'])} + else: + response.media = {'RESULT': 'Error, no Cook selected'} + +class addComputer(object): + def on_get(self, request, response): + logit(request) + ComputerName = None + for key,value in request.params.items(): + if key == "ComputerName": + ComputerName = value + + if ComputerName is None: + response.media = {'RESULT': 'Error, you need a ComputerName to add'} + else: + result = sql.insert("INSERT INTO COMPUTERS (Name) VALUES ('"+ComputerName+"')") + response.media = result + +class addGroup(object): + def on_get(self, request, response): + logit(request) + GroupName= None + for key,value in request.params.items(): + if key == "GroupName": + GroupName = value + + + if GroupName is None: + response.media = {'RESULT': 'Error, you need a GroupName to add'} + else: + result = sql.insert("INSERT INTO GROUPS (Name) VALUES ('"+GroupName+"')") + response.media = result + +class delGroup(object): #Delete group + def on_get(self, request, response): + logit(request) + GroupID= None + for key,value in request.params.items(): + if key == "GroupID": + GroupID = value + if key == "GroupName": + GroupID = str(sql.select("SELECT ID_G FROM GROUPS WHERE Name='"+value+"'")[0]['ID_G']) + + if GroupID is None: + response.media = {'RESULT': 'Error, you need a GroupName to delete'} + else: + sql.insert("DELETE FROM COMPUTER_GROUP WHERE ID_G='"+GroupID+"'") + sql.insert("DELETE FROM COOKS_IDG WHERE ID_G='"+GroupID+"'") + result = sql.insert("DELETE FROM GROUPS WHERE ID_G='"+GroupID+"'") + response.media = result + +class updGroup(object): #Delete group + def on_get(self, request, response): + logit(request) + GroupID, GroupNewName= None, None + for key,value in request.params.items(): + if key == "GroupID": + GroupID = value + if key == "GroupName": + GroupID = str(sql.select("SELECT ID_G FROM GROUPS WHERE Name='"+value+"'")[0]['ID_G']) + if key == "GroupNewName": + Count = str(sql.select("SELECT COUNT(*) 'Count' FROM GROUPS WHERE Name='"+value+"'")[0]['Count']) + if Count == "0": + GroupNewName = value + else: + response.media = {'RESULT': 'Error, New group name exists'} + + if GroupID is None or GroupNewName is None: + response.media = {'RESULT': 'Error, you need a GroupName and new name to update name'} + else: + result = sql.insert("UPDATE GROUPS SET Name='"+GroupNewName+"' WHERE ID_G='"+GroupID+"'") + response.media = result + +class addGrpComputer(object): #Add computer to a group (Local) + def on_get(self, request, response): + logit(request) + ComputerID, GroupID= None, None + for key, value in request.params.items(): + if key == "ComputerName": + ComputerID = str(sql.select("SELECT ID_C FROM COMPUTERS WHERE Name='"+value+"'")[0]['ID_C']) + if key == "ComputerID": + ComputerID = value + if key == "GroupName": + GroupID = str(sql.select("SELECT ID_G FROM GROUPS WHERE Name='"+value+"'")[0]['ID_G']) + if key == "GroupID": + GroupID = value + + if ComputerID is None or GroupID is None: + response.media = {'RESULT': 'Error, you need a Name and Group to add'} + else: + result = sql.insert("INSERT INTO COMPUTER_GROUP (ID_C,ID_G) VALUES ('"+ComputerID+"','"+GroupID+"')") + response.media = result + +class delGrpComputer(object): #Del computer from a group + def on_get(self, request, response): + logit(request) + ComputerID, GroupID= None, None + for key, value in request.params.items(): + if key == "ComputerName": + ComputerID = str(sql.select("SELECT ID_C FROM COMPUTERS WHERE Name='"+value+"'")[0]['ID_C']) + if key == "ComputerID": + ComputerID = value + if key == "GroupName": + GroupID = str(sql.select("SELECT ID_G FROM GROUPS WHERE Name='"+value+"'")[0]['ID_G']) + if key == "GroupID": + GroupID = value + + if ComputerID is None or GroupID is None: + response.media = {'RESULT': 'Error, you need a Name and Group to add'} + else: + result = sql.insert("DELETE FROM COMPUTER_GROUP WHERE ID_C='"+ComputerID+"' AND ID_G='"+GroupID+"'") + response.media = result + + +class delComputer(object): #Delete computer + def on_get(self, request, response): + logit(request) + ComputerID= None + for key, value in request.params.items(): + if key == "ComputerName": + ComputerID = str(sql.select("SELECT ID_C FROM COMPUTERS WHERE Name='"+value+"'")[0]['ID_C']) + if key == "ComputerID": + ComputerID = value + + if ComputerID is None: + response.media = {'RESULT': 'Error, you need a Name/ComputerID to update data'} + else: + sql.insert("DELETE FROM COMPUTER_GROUP WHERE ID_C='"+ComputerID+"'") + sql.insert("DELETE FROM COOKS_STATUS WHERE ID_C='"+ComputerID+"'") + resp = sql.insert("DELETE FROM COMPUTERS WHERE ID_C='"+ComputerID+"'") + logit(resp) + + +class updComputer(object): + def on_get(self, request, response): + logit(request) + ComputerID, UUID= None, None + for key, value in request.params.items(): + if key == "ComputerName": + try: + ComputerID = str(sql.select("SELECT ID_C FROM COMPUTERS WHERE Name='"+value+"'")[0]['ID_C']) + except: + response.media = {'RESULT': 'Error: Computer not exists in database'} + break + if key == "ComputerID": + ComputerID = value + if key == "UUID": + UUID = value + if ComputerID is None and response.media is None: + response.media = {'RESULT': 'Error, you need a ComputerName/ComputerID to update data'} + elif response.media is None and ComputerID is not None and UUID is not None: + Count = str(sql.select("SELECT COUNT(*) 'Count' FROM COMPUTERS WHERE UUID='"+UUID+"' AND ID_C='"+ComputerID+"'")[0]['Count']) + if Count == "0": + response.media = {'RESULT': 'Error, computer doesn\'t exists in database'} + else: + for key, value in request.params.items(): + if key == "SOVersion": + response.media = sql.insert("UPDATE COMPUTERS SET SOVersion ='"+value+"' WHERE ID_C='"+ComputerID+"'") + if key == "SOBit": + response.media = sql.insert("UPDATE COMPUTERS SET SOBit ='"+value+"' WHERE ID_C='"+ComputerID+"'") + if key == "SOCaption": + response.media = sql.insert("UPDATE COMPUTERS SET SOCaption ='"+value+"' WHERE ID_C='"+ComputerID+"'") + if key == "LastConnection": + response.media = sql.insert("UPDATE COMPUTERS SET LastConnection ='"+value+"' WHERE ID_C='"+ComputerID+"'") + if key == "LastUser": + response.media = sql.insert("UPDATE COMPUTERS SET LastUser='"+value+"' WHERE ID_C='"+ComputerID+"'") + if key == "RAM": + response.media = sql.insert("UPDATE COMPUTERS SET RAM='"+value+"' WHERE ID_C='"+ComputerID+"'") + if key == "CPUName": + response.media = sql.insert("UPDATE COMPUTERS SET CPUName='"+value+"' WHERE ID_C='"+ComputerID+"'") + + +class updCookName(object): + def on_get(self, request, response): + logit(request) + CookName, CookNewName= None, None #Initialize + for key, value in request.params.items(): + if key == "CookName": + if os.path.isfile('cooks/'+value+'.yaml'): + CookName= value + else: + response.media = {'RESULT': 'Error: Cook not exists in folder'} + break + if key == "CookNewName": + if os.path.isfile('cooks/'+value+'.yaml'): + response.media = {'RESULT': 'Error: There is a cook with the new name in folder!'} + break + else: + CookNewName= value + if CookName is None or CookNewName is None: + response.media = {'RESULT': 'Error, you need the old and new Cook Name to update data'} + elif response.media is None: + old_file = os.path.join("cooks", CookName+'.yaml') + new_file = os.path.join("cooks", CookNewName+'.yaml') + os.rename(old_file, new_file) + sql.insert("UPDATE COOKS_IDG SET CookName ='"+CookNewName+"' WHERE CookName='"+CookName+"'") + response.media = sql.insert("UPDATE COOKS_STATUS SET CookName ='"+CookNewName+"' WHERE CookName='"+CookName+"'") + + +class loadCook(object): + def on_get(self, request, response): + logit(request) + CookName, ComputerID, UUID= None, None, None #Initialize + for key, value in request.params.items(): + if key == "CookName": + if os.path.isfile('cooks/'+value+'.yaml'): + CookName= value + else: + response.media = {'RESULT': 'Error: Cook not exists in folder'} + break + if key == "ComputerName": + ComputerID = str(sql.select("SELECT ID_C FROM COMPUTERS WHERE Name='"+value+"'")[0]['ID_C']) + if key == "ComputerID": + ComputerID = value + if key == "UUID": + UUID = value + + if CookName is None and response.media is None: + response.media = {'RESULT': 'Error, you need a CookName to load it'} + elif response.media is None: + Count = str(sql.select("SELECT COUNT(*) 'Count' FROM COMPUTERS WHERE UUID='"+UUID+"' AND ID_C='"+ComputerID+"'")[0]['Count']) + if Count == "0": + response.media = {'RESULT': 'Error, computer doesn\'t exists in database'} + else: + with open('cooks/'+CookName+'.yaml', 'r') as myfile: + data = myfile.read() + response.media = yaml.safe_load(data) + +class setCookStatus(object): + def on_get(self, request, response): + logit(request) + CookName, ComputerID, Revision, Error,ErrorDesc= None, None, None, None, "" + for key, value in request.params.items(): + if key == "CookName": + if os.path.isfile('cooks/'+value+'.yaml'): # You have to know that cook exists. + CookName= value + else: + response.media = {'RESULT': 'Error: Cook not exists in folder'} + break + if key == "ComputerName": + ComputerID = str(sql.select("SELECT ID_C FROM COMPUTERS WHERE Name='"+value+"'")[0]['ID_C']) + if key == "ComputerID": + ComputerID = value + if key == "Revision": + Revision = value + if key == "Error": + Error = value + if key == "ErrorDesc": + ErrorDesc = value + + if CookName is None and response.media is None: + response.media = {'RESULT': 'Error, you need a CookName to load it'} + elif response.media is None and CookName is not None and ComputerID is not None and Revision is not None and Error is not None: + statt = sql.select("SELECT COUNT(*) 'RESULT' FROM COOKS_STATUS WHERE CookName='"+CookName+"' AND ID_C='"+ComputerID+"'")[0]['RESULT'] + if statt == 0: + #INSERT, NEW + response.media = sql.insert("INSERT INTO COOKS_STATUS (CookName,ID_C,Revision,`Error`,`ErrorDesc`) VALUES ('"+CookName+"', '"+ComputerID+"', '"+Revision+"','"+Error+"','"+ErrorDesc+"')") + else: + #UPDATE, NOT NEW + response.media = sql.insert("UPDATE COOKS_STATUS SET Revision='"+Revision+"',`Error`='"+Error+"',`ErrorDesc`='"+ErrorDesc+"' WHERE ID_C='"+ComputerID+"' AND CookName='"+CookName+"'") + else: + response.media = {'RESULT': 'Error in parameters...'} + + logit(response.media) + + +api = falcon.API() +api.add_route('/get/computers', getComputers()) #Get list of computer +api.add_route('/get/computerexists', getComputerExists()) #Returns 0 or 1 (name status) +api.add_route('/get/groups', getGroups()) #Get groups of a computer (Or list if not args) +api.add_route('/get/computersgrp', getComputersGrp()) #Get computers in a group +api.add_route('/get/cook', getCook()) #Get cooks from a group +api.add_route('/set/cook', setCook()) #Assign cook to group +api.add_route('/del/cookgrp', delCookGrp()) # Deassign cook from a group +api.add_route('/del/emptypcsgroup', delEmptyPcsGroup()) # Delete all computers from a group +api.add_route('/get/cookgrp', getCookGrp()) # See cooks that have a determinated group +api.add_route('/get/grpcook', getGrpCook()) # See groups of a determinated cook +api.add_route('/get/statuscook', getStatusCook()) # See status of a cook +api.add_route('/get/lastrevisioncook', getLastRevisionCook()) # Returns number of last revision of a cook +api.add_route('/add/computer', addComputer()) #Add computer +api.add_route('/del/computer', delComputer()) #Delete computer +api.add_route('/add/group', addGroup()) #Add group to the list of local groups +api.add_route('/del/group', delGroup()) #Delete group +api.add_route('/upd/group', updGroup()) #Update group name +api.add_route('/add/grpcomputer', addGrpComputer()) #Add computer to a group +api.add_route('/del/grpcomputer', delGrpComputer()) #Delete computer from a group + +api.add_route('/upd/computer', updComputer()) #Update data of computer +api.add_route('/upd/cookname', updCookName()) #Update file name of cook and SQL references to it. +api.add_route('/load/cook', loadCook()) # Load a cook (Transfer in json way) +api.add_route('/set/cookstatus', setCookStatus()) # Update status of a cook in a computer diff --git a/client/ADcomputerData.ps1 b/client/ADcomputerData.ps1 new file mode 100644 index 0000000..568fe61 --- /dev/null +++ b/client/ADcomputerData.ps1 @@ -0,0 +1,76 @@ +$root = $PSCommandPath | Split-Path -Parent +. $root\config.ps1 + +$table = (Invoke-RestMethod -Method Get -Uri "$server/get/computers") # Get all computers +$DNSPrefix = (get-DnsClientGlobalSetting).SuffixSearchList[0] # Get DNS Prefix, needed to search computers in some environments + + +$array = @() + +function getPrograms($computername) { + + + #Define the variable to hold the location of Currently Installed Programs + + $UninstallKey="SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall" + + #Create an instance of the Registry Object and open the HKLM base key + + $reg=[microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine',$computername) + + #Drill down into the Uninstall key using the OpenSubKey Method + + $regkey=$reg.OpenSubKey($UninstallKey) + + #Retrieve an array of string that contain all the subkey names + + $subkeys=$regkey.GetSubKeyNames() + + #Open each Subkey and use GetValue Method to return the required values for each + + $allprogs = @() + + foreach($key in $subkeys){ + + $thisKey=$UninstallKey+"\\"+$key + + $thisSubKey=$reg.OpenSubKey($thisKey) + + $obj = New-Object PSObject + + $obj | Add-Member -MemberType NoteProperty -Name "ComputerName" -Value $computername + + $obj | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $($thisSubKey.GetValue("DisplayName")) + + $obj | Add-Member -MemberType NoteProperty -Name "DisplayVersion" -Value $($thisSubKey.GetValue("DisplayVersion")) + + $obj | Add-Member -MemberType NoteProperty -Name "InstallLocation" -Value $($thisSubKey.GetValue("InstallLocation")) + + $obj | Add-Member -MemberType NoteProperty -Name "Publisher" -Value $($thisSubKey.GetValue("Publisher")) + + + $allprogs += $obj + + } + + return $allprogs +} + + +foreach ($comp in $table){ + $name = -join($comp.Name,".",$DNSPrefix); + if (Test-Connection -ComputerName $name -Quiet){ + Write-Output $name + $array += getPrograms($name) + }else{ + Write-Output "$name Offline" + } + +} + +#$array | Where-Object { $_.DisplayName } | select ComputerName, DisplayName, DisplayVersion, Publisher | ft -auto + +$array | Where-Object { $_.DisplayName } | select ComputerName, DisplayName, DisplayVersion | export-csv "$PSScriptRoot\..\Reports\software.csv" -NoTypeInformation -Delimiter ";" -Encoding ASCII +Write-Host "Guardado en ..\Reports\software.csv" +#$content = [System.IO.File]::ReadAllText("$PSScriptRoot\Reports\software.csv").Replace("A","") +#[System.IO.File]::WriteAllText("$PSScriptRoot\Reports\software.csv", $content) \ No newline at end of file diff --git a/client/PCM.fbp b/client/PCM.fbp new file mode 100644 index 0000000..52ac445 --- /dev/null +++ b/client/PCM.fbp @@ -0,0 +1,310 @@ + + + + + ; + C++ + 1 + source_name + 0 + 0 + res + UTF-8 + connect + + 1000 + none + + 0 + MyProject1 + + . + + 1 + 1 + 1 + 1 + UI + 0 + 0 + + 0 + wxAUI_MGR_DEFAULT + + wxBOTH + + 1 + 1 + impl_virtual + + + + 0 + wxID_ANY + + + MainFrame + + 749,396 + wxDEFAULT_FRAME_STYLE + ; ; forward_declare + + + + + wxTAB_TRAVERSAL + 1 + + + + 1 + 1 + + + 0 + wxID_ANY + MyMenuBar + + + m_menubar1 + protected + + + + ; ; forward_declare + + + + + + Equipos + m_computers + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Añadir equipo + scomp_addc + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Borrar equipo + scomp_deletec + none + + + + + + Grupos + m_groups + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Añadir Grupo + sgroup_addg + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Borrar grupo + sgroup_delg + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Ver equipos de un grupo + sgroup_seeg + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Vaciar grupo + sgroup_emptyg + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Cambiar nombre de un grupo + sgroup_reng + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Recetas asignadas a un grupo + sgroup_cooksg + none + + + + + + Recetas + m_cooks + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Recetas de un grupo + scook_cooksg + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Añadir receta a un grupo + scook_addcookg + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Borrar receta de un grupo + scook_deletecookg + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Renombrar receta + scook_renamecook + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Detalles de implantación de una receta + scook_detailscook + none + + + + + + Reportes + m_reports + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Reporte de equipos + srep_rcomp + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Reporte de grupos + srep_rgroup + none + + + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Reporte de recetas + srep_rcook + none + + + + + + + + diff --git a/client/client.ps1 b/client/client.ps1 new file mode 100644 index 0000000..6f3ff34 --- /dev/null +++ b/client/client.ps1 @@ -0,0 +1,218 @@ +# Build 2 +$root = $PSCommandPath | Split-Path -Parent +. $root\config.ps1 + +$computerName = $env:COMPUTERNAME +$UUID=(get-wmiobject Win32_ComputerSystemProduct).UUID + +$exists = Invoke-RestMethod -Method Get -Uri "$server/get/computerexists?ComputerName=$computerName&UUID=$UUID" +if ($exists.Result -eq 0){ + Write-Host "Computer outside database:" $computerName + exit +} + +$Timestamp = [int64](([datetime]::UtcNow)-(get-date "1/1/1970")).TotalSeconds +# Hardware Data +$SOData = Get-CimInstance Win32_OperatingSystem +$SOVersion = $SOData.Version +if ($SOVersion -match "^10.0."){ #If its Windows 10, add revision number for knowing hotfix installed + $revi = -join(".",(Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion' UBR).UBR) + $SOVersion += $revi +} + +# Update Computer Data +$CPUInfo = Get-WmiObject Win32_Processor +$RAMInstalled = Get-WmiObject CIM_PhysicalMemory | Measure-Object -Property capacity -Sum | ForEach-Object {[math]::round(($_.sum / 1MB),2)} +$paramInvoke = -join("$server/upd/computer?ComputerName=$computerName&UUID=",$UUID,"&SOVersion=",$SOVersion,"&SOCaption=",$SOData.Caption,"&SOBit=",$SOData.OsArchitecture.Substring(0,2),"&LastConnection=",$Timestamp,"&RAM=",$RAMInstalled,"&CPUName=",$CPUInfo.Name) +Invoke-RestMethod -Method Get -Uri $paramInvoke | out-null +# More info +$lastUser = (Get-CimInstance -ClassName Win32_ComputerSystem -Property UserName -ComputerName .).UserName +Invoke-RestMethod -Method Get -Uri "$server/upd/computer?ComputerName=$computerName&UUID=$UUID&LastUser=$lastUser" | out-null + + +$cooks = Invoke-RestMethod -Method Get -Uri "$server/get/cook?ComputerName=$computerName&UUID=$UUID" + +foreach ($CookName in $cooks){ + Set-Location $env:temp # For downloading/copying items, use system temp location + $CookName = $CookName.CookName + $cook = Invoke-RestMethod -Method Get -Uri "$server/load/cook?CookName=$CookName&ComputerName=$computerName&UUID=$UUID" + Write-Host "Receta:" $cook.name + $CookRevision = $cook.revision + $err = 0 + $exit = 0 + $inif = $false + $if = $true + $filesCopied = New-Object System.Collections.ArrayList # For REPOTOLOCAL, has a list of files copied to delete when finish (Do not store forever in temp) + foreach ($step in $cook.steps){ + if ($err -eq 1 -and $noerror -eq 1){$err = 0; $errvar = ""} #If "noerror" is active, do not count errors + if ($err -eq 1 -or $exit -eq 1){break} # Halt if err ocurred (And noerror directive is not active) + $step = $step.Split("|") + $param = $step[1] + Write-Host $step[0] "-" $step[1] + if($inif -eq $true -and $if -eq $false){ # Only can see "ENDIF" if is in IF and is not true + if ($step[0] -ne "ENDIF" -and $step[0] -ne "ELSE"){ + Write-Host $step[0] "Not executed, IF not meet" + $step[0] = "" #Disable command + $step[1] = "" + } + } + + switch ($step[0].ToUpper()) { #Command + "UNINSTALL" { # Remove program + Get-Package -Name "$param*" -ErrorAction Continue #This will return error if program is not installed, do not see it. + if ($? -eq $true){ #If its True, is that package exists + Get-Package -Name "$param*" -ErrorVariable errvar | Uninstall-Package -ErrorVariable errvar + if ($? -eq $false){ # If fail then put 1 (When fail, powershell returns False) + $err = 1 + } + } + } + "SERV_DISABLE" { # Disable a service + Set-Service $param -StartupType Disabled -Status Stopped -ErrorVariable errvar + if ($? -eq $false){ #If its False, it was a problem + $err = 1 + } + } + "SERV_ENABLE" { # Enable a service + Set-Service $param -StartupType Automatic -Status Running -ErrorVariable errvar + if ($? -eq $false){ #If its False, it was a problem + $err = 1 + } + } + "KILL_PROCESS" { + $p = Get-Process -Name "$param" + if ($? -eq $true){ # Only do something if exists + Stop-Process -InputObject $p -Force -ErrorVariable errvar -ErrorAction Continue + #if ($p.HasExited -eq $false){ + # $err = 1 + #} + } + } + "CMD" { # Run a cmd command. Note: Runs at high priv. + cmd.exe /c "$param" + if ($? -eq $false){ # Error in CMD + $err = 1 + } + } + "PWCMD" { # Run a powershell command. Note: Runs as high priv. + Invoke-Expression $param + if ($? -eq $false){ # Error in CMD + $err = 1 + } + } + "REPOTOLOCAL" { # Copy file from repo location to local ($env:temp) for use in cmd or other things + Copy-Item "$resources\$param" $env:temp -ErrorVariable errvar -Recurse -Force + if ($? -eq $false){ # Error in Copy + $err = 1 + } + $filesCopied.Add($param) > $null #Add to list + } + "REMOVE" { # Remove files / folders + Remove-Item "$param" -Recurse -Force -ErrorAction Continue # They not see errors (Because error will be file not found) + } + "INSTALLMSI" { # Installs a .msi file (From $env:temp) + Start-Process msiexec.exe -Wait -ArgumentList "/norestart /quiet /I $param" -ErrorVariable errvar + if ($? -eq $false){ # Error in MSI + $err = 1 + } + } + "REGFILE" { # Imports a .reg file + reg import .\$param + if ($? -eq $false){ # Error importing reg file + $err = 1 + } + } + "MSG" { # Display a message + msg * "$param" + } + {$_ -in "SLEEP","PAUSE"}{ # Pause exec some seconds + [int]$secs = $param + Start-Sleep -Seconds $secs + } + "NOERROR" { #All within NOERROR doesn't generate errors and stop scripts + $noerror = 1 + } + "ENDNOERROR" { + $noerror = 0 + } + "IFSOFTWAREINST" { # If with software + $inif = $true #This controls IF start/stop + Get-Package -Name "$param*" -ErrorAction SilentlyContinue #This will return error if program is not installed, do not see it. + $if=$? # True -> Exists ; False -> Not exists + } + "IFSOFTWAREVER" { # If with software + $inif = $true #This controls IF start/stop + $parts = $param.Split(";") + if ($parts[1] -ne ""){ #Exists uri and filename + $p_name = $parts[0] + $p_ver = $parts[1] + Get-Package -Name "$p_name*" -MinimumVersion "$p_ver" -MaximumVersion "$p_ver" -ErrorAction SilentlyContinue #This will return error if program is not installed, do not see it. + $if=$? # True -> Exists ; False -> Not exists + }else{ #Doesn't sent right + $err = 1 + $errvar = "Param not set right. Exiting..." + } + } + "IFPATHEXISTS" { # If only if a path exists (File, Folder, Registry Key..) + $inif = $true #This controls IF start/stop + Test-Path $param -PathType Any -ErrorAction SilentlyContinue + $if=$? + } + "IFPWCMD" { # If with powershell command + $inif = $true #This controls IF start/stop + Invoke-Expression $param #Executes powershell command + $if=$? # True -> Exists ; False -> Not exists + } + "ELSE" { # Turn bool $if + $if = !$if + } + "ENDIF"{ # End the if + $inif = $false + $if = $true + } + "DOWNLOAD" { #Download a file. This is a bit problematic one, will use ; to separate download and filename, and its forced to use it.. + $parts = $param.Split(";") + if ($parts[1] -ne ""){ #Exists uri and filename + $progressPreference = 'silentlyContinue' + Invoke-WebRequest -Uri $parts[0] -OutFile $parts[1] -UseBasicParsing -ErrorVariable errvar + $progressPreference = 'Continue' + $filesCopied.Add($parts[1]) > $null #Add to list + }else{ #Doesn't sent right + $err = 1 + $errvar = "Param not set right. Exiting..." + } + } + "EXIT"{ # Exits cook completly. If some param, exit will be with "error" + if ($param){ #Exit with error message + $noerror = 0 + $err = 1 + $errvar = $param + $exit = 1 + }else{ #Exit as sucessful + $err = 0 + $errvar = "" + $exit = 1 + } + } + Default {} + } + } + # Send results + if ($errvar){ #There is an error if this has something + $errvar = $($errvar | Out-String) + $errvar = [System.Convert]::ToBase64String([System.Text.Encoding]::UNICODE.GetBytes($errvar)) + $err = 1 + }else{ + $errvar="" + } + if ($cook.runever -eq "1"){ + $err = -1 # This is for run but not for error, is by cook saw. + } + Invoke-RestMethod -Method Get -Uri "$server/set/cookstatus?ComputerName=$computerName&UUID=$UUID&CookName=$CookName&Revision=$CookRevision&Error=$err&ErrorDesc=$errvar" | out-null + + #Delete files copied to temp for saving space + foreach ($element in $filesCopied) { + Remove-Item "$env:temp\$element" -ErrorAction SilentlyContinue -Recurse -Force + } + +} \ No newline at end of file diff --git a/client/configexample.ps1 b/client/configexample.ps1 new file mode 100644 index 0000000..ee03950 --- /dev/null +++ b/client/configexample.ps1 @@ -0,0 +1,2 @@ +$server = "http://miserver.dominio:3333" #Server that runs api service +$resources = "\\MISERVER\REPOFOLDER" #Where files for copy are (Not cooks) \ No newline at end of file diff --git a/client/control.ps1 b/client/control.ps1 new file mode 100644 index 0000000..5d69a84 --- /dev/null +++ b/client/control.ps1 @@ -0,0 +1,317 @@ +# Build 1 +$root = $PSCommandPath | Split-Path -Parent +. $root\config.ps1 + +function seeGroups { + $answer = Invoke-RestMethod -Method Get -Uri "$server/get/groups" + foreach ($st in $answer){ + Write-Host "Grupo:" $st.Name + } +} +function seeComputers { + $answer = Invoke-RestMethod -Method Get -Uri "$server/get/computers" + foreach ($st in $answer){ + Write-Host "Ordenador:" $st.Name + } +} + +function menuReportes { + + do + { + Write-Host "================ Menu reportes ================" + + + Write-Host "1) Reporte de Software" + Write-Host "Q) Salir" + $input = Read-Host "Elegir una Opcion" + switch ($input) + { + '1' { + Clear-Host + . "$PSScriptRoot\ADcomputerData.ps1" + pause + } 'q' { + return + } + } + + } + until ($input -eq 'q') + + +} + +function menuEquipos { + + do + { + Write-Host "================ Menu equipos ================" + + + Write-Host "1) Reporte de Equipos en pantalla" + Write-Host "2) Añadir equipo" + Write-Host "3) Borrar equipo" + Write-Host "4) Añadir equipo a un grupo" + Write-Host "5) Borrar equipo de un grupo" + Write-Host "Q) Salir" + $input = Read-Host "Elegir una Opcion" + switch ($input) + { + '1' { + Clear-Host + seeComputers #Function + pause + } '2' { + Clear-Host + $newHost = Read-Host -Prompt "Nombre del equipo" + if (Test-Connection -ComputerName $newHost -Quiet){ + Write-Output "$newHost Online" + Write-Output "Añadiendo equipo" + Invoke-RestMethod -Method Get -Uri "$server/add/computer?ComputerName=$newHost" + }else{ + Write-Output "$newHost Offline" + } + pause + + } '3' { + Clear-Host + $hhost = Read-Host -Prompt "Nombre del equipo a eliminar" + Invoke-RestMethod -Method Get -Uri "$server/del/computer?ComputerName=$hhost" + } '4' { + Clear-Host + $computer = Read-Host -Prompt "Nombre del equipo" + # See all groups + seeGroups #Function + $newGroup = Read-Host -Prompt "Nombre del grupo a añadir" + Invoke-RestMethod -Method Get -Uri "$server/add/grpcomputer?ComputerName=$computer&GroupName=$newGroup" + pause + } '5' { + Clear-Host + $computer = Read-Host -Prompt "Nombre del equipo" + # See all groups + seeGroups #Function + $newGroup = Read-Host -Prompt "Nombre del grupo a quitar" + Invoke-RestMethod -Method Get -Uri "$server/add/grpcomputer?ComputerName=$computer&GroupName=$newGroup" + pause + } 'q' { + return + } + } + } + until ($input -eq 'q') + + +} + +function menuGrupos { + + do + { + Write-Host "================ Menu grupos ================" + + + Write-Host "1) Reporte de Grupos en pantalla" + Write-Host "2) Ver equipos de un grupo" + Write-Host "3) Añadir grupo" + Write-Host "4) Borrar grupo" + Write-Host "5) Vaciar equipos del grupo" + Write-Host "6) Cambiar nombre del grupo" + Write-Host "7) Ver recetas de un grupo" + Write-Host "Q) Salir" + $input = Read-Host "Elegir una Opción" + switch ($input) + { + '1' { + Clear-Host + seeGroups #Function + pause + } '2' { + Clear-Host + seeGroups + $GroupName = Read-Host -Prompt "Nombre del grupo a ver" + if ($GroupName){ + $answer = Invoke-RestMethod -Method Get -Uri "$server/get/computersgrp?GroupName=$GroupName" + foreach ($st in $answer){ + Write-Host "Ordenador:" $st.Name + } + } + pause + } '3' { + Clear-Host + $newGroup = Read-Host -Prompt "Nombre del grupo a añadir" + Invoke-RestMethod -Method Get -Uri "$server/add/group?GroupName=$newGroup" + pause + + } '4' { + Clear-Host + seeGroups #Function + $grpName = Read-Host -Prompt "Nombre del grupo a eliminar" + Invoke-RestMethod -Method Get -Uri "$server/del/group?GroupName=$grpName" + } '5' { + Clear-Host + seeGroups #Function + $grpName = Read-Host -Prompt "Nombre del grupo a quitar los equipos" + Invoke-RestMethod -Method Get -Uri "$server/del/emptypcsgroup?GroupName=$grpName" + } '6' { + Clear-Host + # See all groups + seeGroups #Function + $GroupName = Read-Host -Prompt "Nombre del grupo" + $GroupNewName = Read-Host -Prompt "Nombre nuevo" + Invoke-RestMethod -Method Get -Uri "$server/upd/group?GroupName=$GroupName&GroupNewName=$GroupNewName" + pause + } '7' { + Clear-Host + # See all groups + seeGroups #Function + $GroupName = Read-Host -Prompt "Nombre del grupo para ver recetas" + if ($GroupName){ + $answer = Invoke-RestMethod -Method Get -Uri "$server/get/cookgrp?GroupName=$GroupName" + foreach ($st in $answer){ + Write-Host "Receta:" $st.CookName + } + }else{ + Write-Host "No hay grupo" + } + pause + } 'q' { + return + } + } + } + until ($input -eq 'q') + +} + + +function menuCooks { + + do + { + Write-Host "================ Menu recetas (Cooks) ================" + + + Write-Host "1) Grupos de una receta" + Write-Host "2) Estado de una receta" + Write-Host "3) Detalles de cada equipo de una receta" + Write-Host "4) Añadir receta a un grupo" + Write-Host "5) Borrar receta de un grupo" + Write-Host "6) Renombrar receta" + Write-Host "Q) Salir" + $input = Read-Host "Elegir una Opción" + switch -regex ($input) + { + '1' { + Clear-Host + $CookName = Read-Host -Prompt "Nombre de la receta" + Write-Host "Grupos de la receta $CookName" + $grpcook = Invoke-RestMethod -Method Get -Uri "$server/get/grpcook?CookName=$CookName" + foreach ($st in $grpcook){ + Write-Host "Grupo:" $st.Name + } + pause + } '2|3' { + Clear-Host + $CookName = Read-Host -Prompt "Nombre de la receta" + + Write-Host "Grupos de la receta" + $grpcook = Invoke-RestMethod -Method Get -Uri "$server/get/grpcook?CookName=$CookName" + foreach ($st in $grpcook){ + Write-Host "Grupo:" $st.Name + } + + $cookrevision = Invoke-RestMethod -Method Get -Uri "$server/get/lastrevisioncook?CookName=$CookName" + $answer = Invoke-RestMethod -Method Get -Uri "$server/get/statuscook?CookName=$CookName" + + if ($_ -eq '3'){ + foreach ($st in $answer){ + Write-Host "Ordenador:" $st.Name ", revision" $st.Revision ", error" $st.Error + } + Write-Host "------Resumen y errores------" + } + $estadistica = @{} + $estadistica.Total = 0 + $estadistica.Error = 0 + $estadistica.NotLast = 0 + foreach ($st in $answer){ + $estadistica.Total++ + if ($st.Error -eq '1'){ + Write-Host "Ordenador en error:" $st.Name + $estadistica.Error++ + } + if ($st.Revision -ne $cookrevision.Revision){ + Write-Host "Ordenador sin la última revisión de la receta:" $st.Name + $estadistica.NotLast++ + } + } + Write-Host "Total implementado:" $estadistica.Total + Write-Host "Errores:" $estadistica.Error + Write-Host "Sin aplicar ultima revisión:" $estadistica.NotLast + pause + } '4' { + Clear-Host + + $CookName = Read-Host -Prompt "Nombre de la receta a añadir" + seeGroups #Function + $GroupName = Read-Host -Prompt "Nombre del grupo que quieres que lo tenga" + Invoke-RestMethod -Method Get -Uri "$server/set/cook?GroupName=$GroupName&CookName=$CookName" + pause + + } '5' { + Clear-Host + $CookName = Read-Host -Prompt "Nombre del receta a eliminar de un grupo" + $grpcook = Invoke-RestMethod -Method Get -Uri "$server/get/grpcook?CookName=$CookName" + foreach ($st in $grpcook){ + Write-Host "Grupo:" $st.Name + } + $GroupName = Read-Host -Prompt "Nombre del grupo" + Invoke-RestMethod -Method Get -Uri "$server/del/cookgrp?GroupName=$GroupName&CookName=$CookName" + pause + } '6' { + Clear-Host + $CookName = Read-Host -Prompt "Nombre del receta actual" + $CookNewName = Read-Host -Prompt "Nuevo nombre" + Invoke-RestMethod -Method Get -Uri "$server/upd/cookname?CookNewName=$CookNewName&CookName=$CookName" + pause + } 'q' { + return + } + } + } + until ($input -eq 'q') + +} + + +do +{ + Write-Host "================ Menú principal ================" + + + Write-Host "1) Reportes" + Write-Host "2) Equipos" + Write-Host "3) Grupos (Locales)" + Write-Host "4) Recetas" + Write-Host "Q) Salir" + $input = Read-Host "Elegir una Opción" + switch ($input) + { + '1' { + Clear-Host + menuReportes + } '2' { + Clear-Host + menuEquipos + } '3' { + Clear-Host + menuGrupos + } '4' { + Clear-Host + menuCooks + } 'q' { + return + } + } +} +until ($input -eq 'q') diff --git a/cooks/README.MD b/cooks/README.MD new file mode 100644 index 0000000..4f060ab --- /dev/null +++ b/cooks/README.MD @@ -0,0 +1 @@ +# Here you have to put cook files in .yaml \ No newline at end of file diff --git a/doc/example_cooks/install_exefile.yaml b/doc/example_cooks/install_exefile.yaml new file mode 100644 index 0000000..3880c56 --- /dev/null +++ b/doc/example_cooks/install_exefile.yaml @@ -0,0 +1,5 @@ +name: Install exe file, example with WinSCP +revision: 1 +steps: + - REPOTOLOCAL|WinSCP-5.13.4-Setup.exe + - CMD|WinSCP-5.13.4-Setup.exe /VERYSILENT /NORESTART \ No newline at end of file diff --git a/doc/example_cooks/install_msioptions.yaml b/doc/example_cooks/install_msioptions.yaml new file mode 100644 index 0000000..7b83eff --- /dev/null +++ b/doc/example_cooks/install_msioptions.yaml @@ -0,0 +1,6 @@ +name: Install MSI with options (Do not put standard silent options!) +revision: 2 +steps: + - REPOTOLOCAL|msiwithoptions.msi + - INSTALLMSI|msiwithoptions.msi ADDLOCAL=Server SERVER_REGISTER_AS_SERVICE=1 SERVER_ADD_FIREWALL_EXCEPTION=1 SERVER_ALLOW_SAS=1 + - REMOVE|C:\ProgramData\Microsoft\Windows\Start Menu\Programs\ProgramThatIDoNotWantUserToSee|Delete start menu folder \ No newline at end of file diff --git a/doc/example_cooks/install_simplemsi.yaml b/doc/example_cooks/install_simplemsi.yaml new file mode 100644 index 0000000..f1e6e69 --- /dev/null +++ b/doc/example_cooks/install_simplemsi.yaml @@ -0,0 +1,5 @@ +name: Install MSI File. By default, it is silent +revision: 1 +steps: + - REPOTOLOCAL|simplemsi.msi + - INSTALLMSI|simplemsi.msi \ No newline at end of file diff --git a/doc/example_cooks/remove_program.yaml b/doc/example_cooks/remove_program.yaml new file mode 100644 index 0000000..b5c32e1 --- /dev/null +++ b/doc/example_cooks/remove_program.yaml @@ -0,0 +1,7 @@ +name: Remove WinRar as Example +revision: 4 +steps: + - UNINSTALL|WinRAR|This uninstalls WinRAR from Control Panel directly (Powershell powered uninstall) + - NOERROR|Do not fail if this fails + - CMD|"C:\Program Files\WinRAR\Uninstall.exe" /S + - ENDNOERROR|End of never exit if fail \ No newline at end of file diff --git a/doc/file_hierarchy.md b/doc/file_hierarchy.md new file mode 100644 index 0000000..1d2ba8e --- /dev/null +++ b/doc/file_hierarchy.md @@ -0,0 +1,24 @@ +# File hierarchy + +## Root folder + - api.py -> Server file. It manages all requests from clients and connects to database + - loadserver.bat -> A helper to run api.py on Windows + - sql.py -> Helper for api.py sql sentences + - sysopt.py -> Config of api and sql + +## Client folder + - client.ps1 -> This file has to run every X time (I suggest every hour) on every client and as NT\SYSTEM or local admin. You can do this with a Task Schedule as SYSTEM in a GPO (Active Directory) + - control.ps1 -> This is the cli of the program. With that you manages the client-server program. It has to be in same folder as client.ps1 file + +## BD folder + - database.db -> Database file in sqlite3 + +## cooks folder + It has the cooks in yaml format. + Warning: Do not change name of cooks created and assigned directly. + +## Doc folder + Documentation... + +## REPO folder (This isn't needeed to be there) + You have to have this shared on LAN, this is the folder that cooks use to retrieve your files (As .exe/.msi) to use in scripts. diff --git a/doc/how_works.md b/doc/how_works.md new file mode 100644 index 0000000..029531b --- /dev/null +++ b/doc/how_works.md @@ -0,0 +1,13 @@ +# Very-Fast instructions +You have to create a cook +In "Examples" you have Cook Examples + +Then, you have to create a group using the CLI. +In the groups is where you assign computers and cooks + +Cooks -> Group +Computers -> Group + +You can have more than 1 cook in a group + +You can call the groups equal to cooks and assign this cook to the group and you have a 1<->1 \ No newline at end of file diff --git a/doc/howto_writecook.md b/doc/howto_writecook.md new file mode 100644 index 0000000..1ebb360 --- /dev/null +++ b/doc/howto_writecook.md @@ -0,0 +1,48 @@ +# How to +Cooks are in yaml format, and have this structure. + +First: Name, a line like below, with the name +"name: My Super Cook" +Then, the revision number, when this number changes, cook will be reapplied +"revision: 1" +If the cook has to run everytime client is called (And not only one time per new revision), you have to add: +"runever: 1" (1-> Everytime; 0 or no line: Run only one time) +And then, steps. First a "steps:" line, and then line by line the steps of the cook +"steps: + - PWCMD|White-Output "Hello World" +" +Steps are ever put like "Type of step" | "Command" +You can put comments in every line putting another "|" and comments at finish, like.. "CMD|WinSCP-5.13.4-Setup.exe /VERYSILENT /NORESTART|Note: Installing WinSCP" + +Cook finishes executing if they had and error (Except in a NOERROR block), or if finishes right. + +Note: Cooks start in directory $env:temp (%TEMP%) from SYSTEM user. + +# Type of commands +- CMD and PWCMD: cmd command or Powershell command. Cmd commands can not work properly if has quotes (Because its run as cmd /c ""), if they are a Powershell equivalent, please use it. Normally cmd command uses are for install/uninstall exe programs or some like that, because powershell doesn't wait to exit a exe command +- REPOTOLOCAL: Copy a file from REPO folder configured to local in temp folder to use it. You can copy files or folders with same command. +- INSTALLMSI: Installs an MSI file silently (Params /quiet /norestart are put, do not repeat). You can add parameters (Like INSTALLMSI|superapp.msi ADDLOCAL=extension SETASDEFAULT=1) +- NOERROR and ENDNOERROR: This is for creating a block of instructions that not captures errors, then cook works ever if instruction fails + Example: + NOERROR|For deleting a file that maybe can not exist + PWCMD|Remove-Item C:\Windows\loginstall.txt + ENDNOERROR|Finish +- SERV_DISABLE and SERV_ENABLE: Enables or disables a Windows Service +- MSG|Display a message to user +- COMMENT or REM: Makes cook comments (COMMENT|This is a comment) +- KILL_PROCESS: Kill a process name if exists +- UNINSTALL: Uninstalls a program by name if its installed. It searchs it by Powershell command 'Get-Package -Name "Program*"', take care of it. You can use to see name +- REMOVE: Removes a file or folder path from local computer. It's recursive +- SLEEP and PAUSE: Pauses execution seconds specified in param +- DOWNLOAD: This downloads a file from network/internet to $env:temp/filename. Its *required* to put a filename, that goes this way: DOWNLOAD|http://www.example.org/file.zip;filename.zip. ";" is that separates URL and filename. It seems to be a limit in PowerShell Invoke-WebRequest that limits this downloads to some like 128MB +- EXIT: Terminates a cook in this instruction. If a param is specified, exit will be with error code and the param as text of error. (Ej: EXIT|Error at seeking for file) + +## If types +Inside a cook, you can use IF/ELSE/ENDIF scheme. +- IFSOFTWAREINST: Runs the if, if a program is installed. +- IFSOFTWAREVER: Runs the if, if a program is installed and has X version. (Run with two param: "Name;Version") +- IFPATHEXISTS: Runs if file exists +- IFPWCMD: If powershell command returns $true (Executed succesfully), if is run +- ELSE: Else.. +- ENDIF + diff --git a/doc/naming_limits.md b/doc/naming_limits.md new file mode 100644 index 0000000..ea494e1 --- /dev/null +++ b/doc/naming_limits.md @@ -0,0 +1,3 @@ +Cooks have to be only alfanumeric caracters and "-","_" and must end with .yaml + +Groups have to be alfanumeric and "-","_" \ No newline at end of file diff --git a/loadserver.bat b/loadserver.bat new file mode 100644 index 0000000..17908f8 --- /dev/null +++ b/loadserver.bat @@ -0,0 +1,3 @@ +REM Starting Server. Will only work if python and dependencies are installed and in PATH +cd /D "%~dp0" +waitress-serve --port=3333 api:api \ No newline at end of file diff --git a/loadserver.sh b/loadserver.sh new file mode 100644 index 0000000..0825fe9 --- /dev/null +++ b/loadserver.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd "$(dirname "$(realpath "$0")")"; +waitress-serve --port=3333 api:api \ No newline at end of file diff --git a/sql.py b/sql.py new file mode 100644 index 0000000..efd3cf6 --- /dev/null +++ b/sql.py @@ -0,0 +1,45 @@ +import sqlite3 +import sysopt #File of options +from shutil import copyfile +from os import path +from datetime import datetime + +if not path.isfile('BD/database.db'): + copyfile('BD/emptydatabase.db','BD/database.db') + +def logit(text): + text = str(text) + now = datetime.now() + print ("Log: " + text) + with open('api.log', 'a') as file: + file.write("Log ("+now.strftime("%x %X") + "): ") + file.write(text) + file.write('\n') + +def dict_factory(cursor, row): + d = {} + for idx, col in enumerate(cursor.description): + d[col[0]] = row[idx] + return d + + +def select(query): + conn = sqlite3.connect('BD/database.db') + conn.row_factory = dict_factory + if sysopt.debugsql==True: + logit("SQL: "+query) + cur = conn.execute(query) + return cur.fetchall() + +def insert(query): + conn = sqlite3.connect('BD/database.db') + if sysopt.debugsql==True: + logit(query) + try: + c = conn.cursor() + c.execute("SQL: "+query) + conn.commit() + conn.close() + return {'RESULT': 'OK'} + except: + return {'RESULT': 'SQLite3 Error'} diff --git a/sysopt.py b/sysopt.py new file mode 100644 index 0000000..ac3eff9 --- /dev/null +++ b/sysopt.py @@ -0,0 +1,3 @@ + +addComputers = False # False/True accept auto add computers that start client program and connect to server +debugsql = True # True is show in log all SQL queries