init commit
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
service_user: "cosmos"
|
||||||
|
|
||||||
|
gpo_yaml_share: "/media/gpo_yaml"
|
||||||
|
gpo_yaml_path: "{{ gpo_yaml_share }}/gpo.yaml"
|
||||||
|
|
||||||
|
project_folder: "/opt/cosmos/gpo_site"
|
||||||
|
|
||||||
|
api_service_name: "gpo_api"
|
||||||
|
api_service_folder: "{{ project_folder }}/api"
|
||||||
|
api_service_port: "5000"
|
||||||
|
api_service_bind_ip: "0.0.0.0"
|
||||||
|
|
||||||
|
dashboard_web_root: "{{ project_folder }}/dashboard"
|
||||||
|
container_name: "GPO-Dashboard"
|
||||||
|
container_service_name: "gpo_dash"
|
||||||
|
container_http_port: "80"
|
||||||
|
extra_volumes: ""
|
||||||
|
|
||||||
|
...
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>GPO Query</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Query GPOs by Group</h2>
|
||||||
|
<?php if (isset($_GET['group'])) : ?>
|
||||||
|
<form action="index.php" method="GET">
|
||||||
|
<label for="group">Group Name:</label>
|
||||||
|
<input type="text" id="group" name="group" value="<?php echo htmlspecialchars($_GET['group']); ?>" required>
|
||||||
|
<button type="submit">Query</button>
|
||||||
|
</form>
|
||||||
|
<?php else : ?>
|
||||||
|
<form action="index.php" method="GET">
|
||||||
|
<label for="group">Group Name:</label>
|
||||||
|
<input type="text" id="group" name="group" required>
|
||||||
|
<button type="submit">Query</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
if (isset($_GET['group'])) {
|
||||||
|
$group = urlencode($_GET['group']); // Encode the group name for URL
|
||||||
|
$url = "http://10.34.115.140:5000/gpo?group=$group"; // API endpoint
|
||||||
|
// Initialize cURL session
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
|
||||||
|
// Execute the request and fetch the response
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
|
||||||
|
// Check for errors
|
||||||
|
if ($e = curl_error($ch)) {
|
||||||
|
die("Curl error: $e");
|
||||||
|
} else {
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
if ($httpCode != 200) {
|
||||||
|
die("Error: Received HTTP code $httpCode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the JSON response
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
|
||||||
|
// Display the data in a table
|
||||||
|
if (isset($data['error'])) {
|
||||||
|
echo "<p>Error: {$data['error']}</p>";
|
||||||
|
} else {
|
||||||
|
echo "<table><tr><th>AD Group</th><th>GPO</th><th>Local Group</th></tr>";
|
||||||
|
foreach ($data as $item) {
|
||||||
|
echo "<tr><td>{$item['ad_group']}</td><td>{$item['gpo']}</td><td>{$item['local_group']}</td></tr>";
|
||||||
|
}
|
||||||
|
echo "</table>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close cURL session
|
||||||
|
curl_close($ch);
|
||||||
|
} else {
|
||||||
|
echo "<p>No group specified.</p>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/* styles.css */
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #2c3e50; /* Dark background color */
|
||||||
|
color: #bdc3c7; /* Dimmer text color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-info {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-button {
|
||||||
|
background-color: #34495e;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 15px 32px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 4px 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 950px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #34495e; /* Darker background for container */
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); /* Slightly darker shadow */
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
color: #bdc3c7; /* Dimmer text color */
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #bdc3c7; /* Dimmer text color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-columns {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-rows {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start; /* Left justification */
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-column {
|
||||||
|
flex: 0 0 calc(33% - 10px); /* Adjust width of each column */
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 10px; /* Adjust spacing between columns */
|
||||||
|
}
|
||||||
|
|
||||||
|
.subcolumn {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meter {
|
||||||
|
width: calc(90% - 5px);
|
||||||
|
max-width: calc(45% - 5px);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border: 1px solid #7f8c8d; /* Light border color */
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #2c3e50; /* Dark background for meter */
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 0. Load modules (they are usually loaded by default, but be safe)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
Import-Module GroupPolicy -ErrorAction Stop
|
||||||
|
Import-Module ActiveDirectory -ErrorAction Stop
|
||||||
|
$TargetOU = "OU=NA,OU=Manufacturing,OU=Tesla Systems,DC=teslamotors,DC=com"
|
||||||
|
$OutputFile = "C:\Users\matanderson\gpo.yaml"
|
||||||
|
# Add-Content -Path $OutputFile -Value $line
|
||||||
|
if (Test-Path $OutputFile) {
|
||||||
|
Remove-Item -Path $OutputFile -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 1. Grab every OU under $TargetOU (including $TargetOU itself)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
Write-Output "Enumerating OUs under '$TargetOU' ..."
|
||||||
|
Add-Content -Path $OutputFile -Value "---"
|
||||||
|
Add-Content -Path $OutputFile -Value "- root_ou: $TargetOU"
|
||||||
|
$ouObjects = Get-ADObject `
|
||||||
|
-Filter 'ObjectClass -eq "organizationalUnit"' `
|
||||||
|
-SearchBase $TargetOU `
|
||||||
|
-SearchScope Subtree `
|
||||||
|
-Properties gPLink
|
||||||
|
|
||||||
|
# Also grab the target OU itself (in case it is linked directly)
|
||||||
|
$targetOUObj = Get-ADObject `
|
||||||
|
-Identity $TargetOU `
|
||||||
|
-Properties gPLink
|
||||||
|
if ($targetOUObj) {
|
||||||
|
$ouObjects += $targetOUObj
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 2. Build a hashtable: GPO_GUID => list of OU DNs it is linked to
|
||||||
|
# This is entirely opaque voodoo, i have no idea what's going on
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
$gpoLinks = @{}
|
||||||
|
|
||||||
|
foreach ($ou in $ouObjects) {
|
||||||
|
if ($ou.gPLink) {
|
||||||
|
foreach ($link in ($ou.gPLink -split ';')) {
|
||||||
|
$linkTrim = $link.Trim('[', ']')
|
||||||
|
if ($linkTrim -match '\{(?<guid>[0-9a-fA-F-]+)\}') {
|
||||||
|
$guid = $Matches.guid
|
||||||
|
if (-not $gpoLinks.ContainsKey($guid)) {
|
||||||
|
$gpoLinks[$guid] = @()
|
||||||
|
}
|
||||||
|
$gpoLinks[$guid] += $ou.DistinguishedName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $gpoLinks.Count) {
|
||||||
|
Write-Host "No GPOs linked under the searched OU tree." -ForegroundColor Yellow
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Found $($gpoLinks.Count) distinct GPO(s) linked in the OU tree."
|
||||||
|
Add-Content -Path $OutputFile -Value " gpo_links: $($gpoLinks.Count)"
|
||||||
|
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# 3. For each GPO, get an XML report and pull RestrictedGroup nodes
|
||||||
|
# ----------------------------------------------------
|
||||||
|
|
||||||
|
Add-Content -Path $OutputFile -Value " configured_gpos:"
|
||||||
|
foreach ($gpoGuid in $gpoLinks.Keys) {
|
||||||
|
try {
|
||||||
|
$gpo = Get-GPO -Guid $gpoGuid -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Unable to retrieve GPO $gpoGuid – skipping."
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Write-Host "GPO: $($gpo.DisplayName)"
|
||||||
|
Add-Content -Path $OutputFile -Value " $($gpo.DisplayName):"
|
||||||
|
|
||||||
|
# Pull the XML report
|
||||||
|
try {
|
||||||
|
$xmlString = Get-GPOReport -Guid $gpoGuid -ReportType Xml -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Unable to generate XML report for $($gpo.DisplayName)."
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml = [xml]$xmlString
|
||||||
|
|
||||||
|
$ns = New-Object System.Xml.XmlNamespaceManager ($xml.NameTable)
|
||||||
|
$ns.AddNamespace('m', 'http://www.microsoft.com/GroupPolicy/Settings') # default namespace
|
||||||
|
$ns.AddNamespace('q1', 'http://www.microsoft.com/GroupPolicy/Settings/Security')
|
||||||
|
$ns.AddNamespace('t', 'http://www.microsoft.com/GroupPolicy/Types')
|
||||||
|
$ns.AddNamespace('xsi','http://www.w3.org/2001/XMLSchema-instance')
|
||||||
|
|
||||||
|
# Find every <q1:RestrictedGroups> element
|
||||||
|
$restrictedGroups = $xml.SelectNodes('//q1:RestrictedGroups', $ns)
|
||||||
|
|
||||||
|
# Loop and build the string
|
||||||
|
foreach ($rg in $restrictedGroups) {
|
||||||
|
$adGroupName = ($rg.SelectSingleNode('q1:GroupName/t:Name', $ns)).InnerText
|
||||||
|
$localGroupName = ($rg.SelectSingleNode('q1:Memberof/t:Name', $ns)).InnerText
|
||||||
|
$plainAD_Groupname = ($adGroupName -split '\\')[-1]
|
||||||
|
Write-Output "AD Group: $adGroupName - Local Group: $localGroupName"
|
||||||
|
Add-Content -Path $OutputFile -Value " - ad_group: $plainAD_Groupname"
|
||||||
|
Add-Content -Path $OutputFile -Value " local_group: $localGroupName"
|
||||||
|
}
|
||||||
|
Add-Content -Path $OutputFile -Value ""
|
||||||
|
}
|
||||||
|
Add-Content -Path $OutputFile -Value "..."
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
- name: API Service - file and folder handler
|
||||||
|
block:
|
||||||
|
|
||||||
|
- name: API Service - create python service folder
|
||||||
|
file:
|
||||||
|
path: "{{ api_service_folder }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ service_user }}"
|
||||||
|
group: "{{ service_user }}"
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: API Service - template app.py
|
||||||
|
template:
|
||||||
|
src: app.py
|
||||||
|
dest: "{{ api_service_folder }}/app.py"
|
||||||
|
owner: "{{ service_user }}"
|
||||||
|
group: "{{ service_user }}"
|
||||||
|
mode: 0755
|
||||||
|
|
||||||
|
# Create python service venv
|
||||||
|
- name: API Service - Python venv handler
|
||||||
|
block:
|
||||||
|
|
||||||
|
- name: API Service - create python venv requirement file
|
||||||
|
copy:
|
||||||
|
dest: "{{ api_service_folder }}/requirements.txt"
|
||||||
|
content: |
|
||||||
|
flask
|
||||||
|
pytz
|
||||||
|
requests
|
||||||
|
opencv-python
|
||||||
|
pyyaml
|
||||||
|
owner: "{{ service_user }}"
|
||||||
|
group: "{{ service_user }}"
|
||||||
|
mode: 0644
|
||||||
|
|
||||||
|
- name: API Service - build python venv
|
||||||
|
pip:
|
||||||
|
virtualenv: "{{ api_service_folder }}/venv"
|
||||||
|
requirements: "{{ api_service_folder }}/requirements.txt"
|
||||||
|
virtualenv_command: python3 -m venv
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: API Service - api service handler
|
||||||
|
block:
|
||||||
|
|
||||||
|
- name: "API Service - {{ api_service_name }} - stop service if running"
|
||||||
|
ignore_errors: yes
|
||||||
|
systemd:
|
||||||
|
name: "{{ api_service_name }}.service"
|
||||||
|
state: stopped
|
||||||
|
|
||||||
|
- name: "API Service - template {{ api_service_name }}.service"
|
||||||
|
vars:
|
||||||
|
service_name: "{{ api_service_name }}"
|
||||||
|
service_working_folder: "{{ api_service_folder }}"
|
||||||
|
service_exe: "{{ api_service_folder }}/venv/bin/python -u {{ api_service_folder }}/app.py"
|
||||||
|
template:
|
||||||
|
src: "service_template.service"
|
||||||
|
dest: "/etc/systemd/system/{{ api_service_name }}.service"
|
||||||
|
mode: 0644
|
||||||
|
|
||||||
|
- name: "API Service - {{ api_service_name }} - enable and start service"
|
||||||
|
systemd:
|
||||||
|
name: "{{ api_service_name }}.service"
|
||||||
|
state: started
|
||||||
|
enabled: yes
|
||||||
|
daemon_reload: yes
|
||||||
|
|
||||||
|
...
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
###############################################
|
||||||
|
# This part sets up drive history dashboard
|
||||||
|
###############################################
|
||||||
|
|
||||||
|
# Create docker Folder
|
||||||
|
- name: drive history dashboard - create dashboard_web_root folder
|
||||||
|
file:
|
||||||
|
path: "{{ dashboard_web_root }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
|
||||||
|
- name: drive history dashboard - copy files for docker container
|
||||||
|
copy:
|
||||||
|
src: "dashboard/"
|
||||||
|
dest: "{{ dashboard_web_root }}/html"
|
||||||
|
mode: 0755
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
|
||||||
|
- name: docker container
|
||||||
|
block:
|
||||||
|
|
||||||
|
- name: service_control_website - template docker-compose.yaml
|
||||||
|
template:
|
||||||
|
src: docker-compose-php.yaml
|
||||||
|
dest: "{{ dashboard_web_root }}/docker-compose.yaml"
|
||||||
|
mode: 0644
|
||||||
|
|
||||||
|
- name: "service_control_website - Start container at {{ container_http_port }}"
|
||||||
|
shell: "docker-compose -f {{ dashboard_web_root }}/docker-compose.yaml up -d"
|
||||||
|
register: docker_output
|
||||||
|
- debug: |
|
||||||
|
msg="{{ docker_output.stdout_lines }}"
|
||||||
|
msg="{{ docker_output.stderr_lines }}"
|
||||||
|
|
||||||
|
...
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
# Mount GPO Share path
|
||||||
|
#- name: Mount GPO Share path
|
||||||
|
# include_role:
|
||||||
|
# name: "cifs_mount"
|
||||||
|
# vars:
|
||||||
|
# smb_username: "{{ ITMFG_USERNAME }}"
|
||||||
|
# smb_password: "{{ ITMFG_PASSWORD }}"
|
||||||
|
# server_path: "//172.23.1.23/gpo"
|
||||||
|
# target_path: "{{ gpo_yaml_share }}"
|
||||||
|
|
||||||
|
# create python gpo service
|
||||||
|
#- name: GPO Site - python api service
|
||||||
|
# include_tasks: api.yaml
|
||||||
|
|
||||||
|
# create python gpo service
|
||||||
|
- name: GPO Site - dashboard container
|
||||||
|
include_tasks: dashboard.yaml
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
...
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import yaml
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Load GPO data
|
||||||
|
with open('{{ gpo_yaml_path }}', 'r') as file:
|
||||||
|
data = yaml.safe_load(file)
|
||||||
|
|
||||||
|
# Debugging output to check the structure of loaded data
|
||||||
|
# print("Loaded YAML data:", data)
|
||||||
|
|
||||||
|
@app.route('/gpos', methods=['GET'])
|
||||||
|
def get_gpos():
|
||||||
|
group_name = request.args.get('group_name')
|
||||||
|
|
||||||
|
if not group_name:
|
||||||
|
return jsonify({"error": "Group name is required"}), 400
|
||||||
|
|
||||||
|
gpos = []
|
||||||
|
for gpo, links in data['configured_gpos'].items():
|
||||||
|
for link in links:
|
||||||
|
if link['ad_group'] == group_name or link['local_group'] == group_name:
|
||||||
|
gpos.append(gpo)
|
||||||
|
break
|
||||||
|
|
||||||
|
return jsonify({"group_name": group_name, "GPOs": gpos})
|
||||||
|
|
||||||
|
@app.route('/gpo', methods=['GET'])
|
||||||
|
def get_groups():
|
||||||
|
group_name = request.args.get('group')
|
||||||
|
print("Group specified:", group_name)
|
||||||
|
if not group_name:
|
||||||
|
return jsonify({"error": "Group parameter is required"}), 400
|
||||||
|
|
||||||
|
gpos = []
|
||||||
|
for entry in data:
|
||||||
|
configured_gpos = entry.get('configured_gpos', {})
|
||||||
|
for gpo, details in configured_gpos.items():
|
||||||
|
if details is None:
|
||||||
|
continue
|
||||||
|
for detail in details:
|
||||||
|
if detail['ad_group'] == group_name:
|
||||||
|
gpos.append({
|
||||||
|
"gpo": gpo,
|
||||||
|
"local_group": detail['local_group'],
|
||||||
|
"ad_group": detail['ad_group']
|
||||||
|
})
|
||||||
|
return jsonify(gpos)
|
||||||
|
|
||||||
|
# test route
|
||||||
|
@app.route('/test', methods=['GET'])
|
||||||
|
def test():
|
||||||
|
try:
|
||||||
|
root_ou = data[0]['root_ou']
|
||||||
|
except KeyError:
|
||||||
|
# Handle the case where 'root_ou' does not exist in the dictionary
|
||||||
|
return jsonify({"error": "KeyError: 'root_ou' not found in provided data"}), 400
|
||||||
|
except TypeError:
|
||||||
|
# Handle cases where data might be of a different type than expected
|
||||||
|
return jsonify({"error": "TypeError: Data is not a dictionary"}), 400
|
||||||
|
return jsonify({"result": "test", "OU_Root": root_ou})
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, host='{{ api_service_bind_ip }}', port={{ api_service_port }})
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
|
||||||
|
{{ container_service_name }}:
|
||||||
|
container_name: {{ container_name }}
|
||||||
|
image: php:8.0-apache
|
||||||
|
ports:
|
||||||
|
- {{ container_http_port }}:80
|
||||||
|
volumes:
|
||||||
|
- ./html:/var/www/html/
|
||||||
|
{{ extra_volumes }}
|
||||||
|
network_mode: bridge
|
||||||
|
restart: always
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description={{ service_name }}
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
WorkingDirectory={{ service_working_folder }}
|
||||||
|
ExecStart={{ service_exe }}
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user