mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-18 20:20:11 +01:00
Easy AWS cloud deployment for private instances via CloudFormation template (#52)
* wip * fix file ref * update dockerfile * mute chown * add build script for AWS CF template construction add comment about script and AWS deployment * move aws stuff into its own folder * edit readme
This commit is contained in:
parent
caea4ea1b2
commit
4dd1887bd1
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,4 +6,5 @@ node_modules
|
|||||||
__pycache__
|
__pycache__
|
||||||
v-env
|
v-env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
aws_cf_deploy_anything_llm.json
|
||||||
|
|
||||||
|
54
aws/cloudformation/DEPLOY.md
Normal file
54
aws/cloudformation/DEPLOY.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# How to deploy a private AnythingLLM instance on AWS
|
||||||
|
|
||||||
|
With an AWS account you can easily deploy a private AnythingLLM instance on AWS. This will create a url that you can access from any browser over HTTP (HTTPS not supported). This single instance will run on your own keys and they will not be exposed - however if you want your instance to be protected it is highly recommend that you set the `AUTH_TOKEN` and `JWT_SECRET` variables in the `docker/` ENV.
|
||||||
|
|
||||||
|
[Refer to .env.example](../../docker/HOW_TO_USE_DOCKER.md) for data format.
|
||||||
|
|
||||||
|
The output of this cloudformation stack will be:
|
||||||
|
- 1 EC2 Instance
|
||||||
|
- 1 Security Group with 0.0.0.0/0 access on Ports 22 & 3001
|
||||||
|
- 1 EC2 Instance Volume `gb2` of 10Gib minimum
|
||||||
|
|
||||||
|
**Requirements**
|
||||||
|
- An AWS account with billing information.
|
||||||
|
- AnythingLLM can run within the free tier using a t2.micro and 10Gib SSD hard disk volume
|
||||||
|
- `.env` file that is filled out with your settings and set up in the `docker/` folder
|
||||||
|
|
||||||
|
## How to deploy on AWS
|
||||||
|
|
||||||
|
1. Generate your specific cloudformation document by running `yarn generate:cloudformation` from the project root directory.
|
||||||
|
2. Log in to your AWS account
|
||||||
|
3. Open [CloudFormation](https://us-west-1.console.aws.amazon.com/cloudformation/home)
|
||||||
|
4. Ensure you are deploying in a geographic zone that is nearest to your physical location to reduce latency.
|
||||||
|
5. Click `Create Stack`
|
||||||
|
|
||||||
|
![Create Stack](/images/screenshots/create_stack.png)
|
||||||
|
|
||||||
|
6. Upload your `aws_cf_deploy_anything_llm.json` to the stack
|
||||||
|
|
||||||
|
![Upload Stack](/images/screenshots/upload.png)
|
||||||
|
|
||||||
|
7. Click `Next` and give your stack a name. This is superficial.
|
||||||
|
8. No other changes are needed, just proceed though each step
|
||||||
|
9. Click `Submit`
|
||||||
|
10. Wait for stack events to finish and be marked as `Completed`
|
||||||
|
11. View `Outputs` tab.
|
||||||
|
|
||||||
|
![Stack Output](/images/screenshots/cf_outputs.png)
|
||||||
|
|
||||||
|
## Please read this notice before submitting issues about your deployment
|
||||||
|
|
||||||
|
**Note:**
|
||||||
|
Your instance will not be available instantly. Depending on the instance size you launched with it can take anywhere from 10-20 minutes to fully boot up.
|
||||||
|
|
||||||
|
If you want to check the instances progress, navigate to [your deployed EC2 instances](https://us-west-1.console.aws.amazon.com/ec2/home) and connect to your instance via SSH in browser.
|
||||||
|
|
||||||
|
Once connected run `sudo tail -f /var/log/cloud-init-output.log` and wait for the file to conclude deployment of the docker image.
|
||||||
|
You should see an output like this
|
||||||
|
```
|
||||||
|
[+] Running 2/2
|
||||||
|
⠿ Network docker_anything-llm Created
|
||||||
|
⠿ Container anything-llm Started
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally, your use of this deployment process means you are responsible for any costs of these AWS resources fully.
|
244
aws/cloudformation/cf_template.template
Normal file
244
aws/cloudformation/cf_template.template
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
{
|
||||||
|
"AWSTemplateFormatVersion": "2010-09-09",
|
||||||
|
"Description": "Create a stack that runs AnythingLLM on a single instance",
|
||||||
|
"Parameters": {
|
||||||
|
"InstanceType": {
|
||||||
|
"Description": "EC2 instance type",
|
||||||
|
"Type": "String",
|
||||||
|
"Default": "t2.micro"
|
||||||
|
},
|
||||||
|
"InstanceVolume": {
|
||||||
|
"Description": "Storage size of disk on Instance in GB",
|
||||||
|
"Type": "Number",
|
||||||
|
"Default": 10,
|
||||||
|
"MinValue": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Resources": {
|
||||||
|
"AnythingLLMInstance": {
|
||||||
|
"Type": "AWS::EC2::Instance",
|
||||||
|
"Properties": {
|
||||||
|
"ImageId": {
|
||||||
|
"Fn::FindInMap": [
|
||||||
|
"Region2AMI",
|
||||||
|
{
|
||||||
|
"Ref": "AWS::Region"
|
||||||
|
},
|
||||||
|
"AMI"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"InstanceType": {
|
||||||
|
"Ref": "InstanceType"
|
||||||
|
},
|
||||||
|
"SecurityGroupIds": [
|
||||||
|
{
|
||||||
|
"Ref": "AnythingLLMInstanceSecurityGroup"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"BlockDeviceMappings": [
|
||||||
|
{
|
||||||
|
"DeviceName": {
|
||||||
|
"Fn::FindInMap": [
|
||||||
|
"Region2AMI",
|
||||||
|
{
|
||||||
|
"Ref": "AWS::Region"
|
||||||
|
},
|
||||||
|
"RootDeviceName"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Ebs": {
|
||||||
|
"VolumeSize": {
|
||||||
|
"Ref": "InstanceVolume"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"UserData": {
|
||||||
|
"Fn::Base64": {
|
||||||
|
"Fn::Join": [
|
||||||
|
"",
|
||||||
|
[
|
||||||
|
"Content-Type: multipart/mixed; boundary=\"//\"\n",
|
||||||
|
"MIME-Version: 1.0\n",
|
||||||
|
"\n",
|
||||||
|
"--//\n",
|
||||||
|
"Content-Type: text/cloud-config; charset=\"us-ascii\"\n",
|
||||||
|
"MIME-Version: 1.0\n",
|
||||||
|
"Content-Transfer-Encoding: 7bit\n",
|
||||||
|
"Content-Disposition: attachment; filename=\"cloud-config.txt\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"#cloud-config\n",
|
||||||
|
"cloud_final_modules:\n",
|
||||||
|
"- [scripts-user, always]\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"--//\n",
|
||||||
|
"Content-Type: text/x-shellscript; charset=\"us-ascii\"\n",
|
||||||
|
"MIME-Version: 1.0\n",
|
||||||
|
"Content-Transfer-Encoding: 7bit\n",
|
||||||
|
"Content-Disposition: attachment; filename=\"userdata.txt\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"#!/bin/bash\n",
|
||||||
|
"# check output of userdata script with sudo tail -f /var/log/cloud-init-output.log\n",
|
||||||
|
"sudo yum install docker -y\n",
|
||||||
|
"sudo usermod -a -G docker ec2-user\n",
|
||||||
|
"curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose\n",
|
||||||
|
"sudo chmod +x /usr/local/bin/docker-compose\n",
|
||||||
|
"sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose\n",
|
||||||
|
"sudo systemctl enable docker\n",
|
||||||
|
"sudo systemctl start docker\n",
|
||||||
|
"sudo yum install git -y\n",
|
||||||
|
"git clone -b cloud-deploy https://github.com/Mintplex-Labs/anything-llm.git /home/ec2-user/anything-llm\n",
|
||||||
|
"cd /home/ec2-user/anything-llm/docker\n",
|
||||||
|
"cat >> .env << \"END\"\n",
|
||||||
|
"!SUB::USER::CONTENT!",
|
||||||
|
"UID=\"1000\"\n",
|
||||||
|
"GID=\"1000\"\n",
|
||||||
|
"CLOUD_BUILD=1\n",
|
||||||
|
"END\n",
|
||||||
|
"cd ../frontend\n",
|
||||||
|
"rm -rf .env.production\n",
|
||||||
|
"cat >> .env.production << \"END\"\n",
|
||||||
|
"GENERATE_SOURCEMAP=true\n",
|
||||||
|
"VITE_API_BASE=\"/api\"\n",
|
||||||
|
"END\n",
|
||||||
|
"sudo docker-compose -f /home/ec2-user/anything-llm/docker/docker-compose.yml up -d\n",
|
||||||
|
"\n",
|
||||||
|
"--//--\n"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AnythingLLMInstanceSecurityGroup": {
|
||||||
|
"Type": "AWS::EC2::SecurityGroup",
|
||||||
|
"Properties": {
|
||||||
|
"GroupDescription": "AnythingLLm Instance Security Group",
|
||||||
|
"SecurityGroupIngress": [
|
||||||
|
{
|
||||||
|
"IpProtocol": "tcp",
|
||||||
|
"FromPort": "22",
|
||||||
|
"ToPort": "22",
|
||||||
|
"CidrIp": "0.0.0.0/0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"IpProtocol": "tcp",
|
||||||
|
"FromPort": "3001",
|
||||||
|
"ToPort": "3001",
|
||||||
|
"CidrIp": "0.0.0.0/0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"IpProtocol": "tcp",
|
||||||
|
"FromPort": "3001",
|
||||||
|
"ToPort": "3001",
|
||||||
|
"CidrIpv6": "::/0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Outputs": {
|
||||||
|
"ServerIp": {
|
||||||
|
"Description": "IP address of the AnythingLLM instance",
|
||||||
|
"Value": {
|
||||||
|
"Fn::GetAtt": [
|
||||||
|
"AnythingLLMInstance",
|
||||||
|
"PublicIp"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ServerURL": {
|
||||||
|
"Description": "URL of the AnythingLLM server",
|
||||||
|
"Value": {
|
||||||
|
"Fn::Join": [
|
||||||
|
"",
|
||||||
|
[
|
||||||
|
"http://",
|
||||||
|
{
|
||||||
|
"Fn::GetAtt": [
|
||||||
|
"AnythingLLMInstance",
|
||||||
|
"PublicIp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
":3001"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Mappings": {
|
||||||
|
"Region2AMI": {
|
||||||
|
"ap-south-1": {
|
||||||
|
"AMI": "ami-0e6329e222e662a52",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"eu-north-1": {
|
||||||
|
"AMI": "ami-08c308b1bb265e927",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"eu-west-3": {
|
||||||
|
"AMI": "ami-069d1ea6bc64443f0",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"eu-west-2": {
|
||||||
|
"AMI": "ami-06a566ca43e14780d",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"eu-west-1": {
|
||||||
|
"AMI": "ami-0a8dc52684ee2fee2",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"ap-northeast-3": {
|
||||||
|
"AMI": "ami-0c8a89b455fae8513",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"ap-northeast-2": {
|
||||||
|
"AMI": "ami-0ff56409a6e8ea2a0",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"ap-northeast-1": {
|
||||||
|
"AMI": "ami-0ab0bbbd329f565e6",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"ca-central-1": {
|
||||||
|
"AMI": "ami-033c256a10931f206",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"sa-east-1": {
|
||||||
|
"AMI": "ami-0dabf4dab6b183eef",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"ap-southeast-1": {
|
||||||
|
"AMI": "ami-0dc5785603ad4ff54",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"ap-southeast-2": {
|
||||||
|
"AMI": "ami-0c5d61202c3b9c33e",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"eu-central-1": {
|
||||||
|
"AMI": "ami-004359656ecac6a95",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"us-east-1": {
|
||||||
|
"AMI": "ami-0cff7528ff583bf9a",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"us-east-2": {
|
||||||
|
"AMI": "ami-02238ac43d6385ab3",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"us-west-1": {
|
||||||
|
"AMI": "ami-01163e76c844a2129",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
},
|
||||||
|
"us-west-2": {
|
||||||
|
"AMI": "ami-0ceecbb0f30a902a6",
|
||||||
|
"RootDeviceName": "/dev/xvda"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
aws/cloudformation/generate.mjs
Normal file
56
aws/cloudformation/generate.mjs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Note (tcarambat) This script should be executed from root via the `yarn generate::cloudformation` command only.
|
||||||
|
// This script will copy your current Docker .env settings being used into a slightly custom AWS CloudFormation template
|
||||||
|
// that you can upload and deploy on AWS in a single click!
|
||||||
|
// Recommended settings are already defined in the template but you can modify them as needed.
|
||||||
|
// AnythingLLM can run within the free tier services of AWS (t2.micro w/10GB of storage)
|
||||||
|
//
|
||||||
|
// This will deploy a fully public AnythingLLM so if you do not want anyone to access it please set the AUTH_TOKEN & JWT_SECRET envs
|
||||||
|
// before running this script. You can still run the collector scripts on AWS so no FTP or file uploads are required.
|
||||||
|
// Your documents and data do not leave your AWS instance when you host in the cloud this way.
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import path, { dirname } from 'path';
|
||||||
|
import { exit } from 'process';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const REPLACEMENT_KEY = '!SUB::USER::CONTENT!'
|
||||||
|
|
||||||
|
const envPath = path.resolve(__dirname, `../../docker/.env`)
|
||||||
|
const envFileExists = fs.existsSync(envPath);
|
||||||
|
|
||||||
|
if (!envFileExists) {
|
||||||
|
console.log(chalk.redBright('[ABORT]'), 'You do not have an .env file in your ./docker/ folder. You need to create it first.');
|
||||||
|
console.log('You can start by running', chalk.cyan('cp -n ./docker/.env.example ./docker/.env'))
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove comments
|
||||||
|
// Remove UID,GID,etc
|
||||||
|
// Remove empty strings
|
||||||
|
// Split into array
|
||||||
|
const settings = fs.readFileSync(envPath, "utf8")
|
||||||
|
.replace(/^#.*\n?/gm, '')
|
||||||
|
.replace(/^UID.*\n?/gm, '')
|
||||||
|
.replace(/^GID.*\n?/gm, '')
|
||||||
|
.replace(/^CLOUD_BUILD.*\n?/gm, '')
|
||||||
|
.replace(/^\s*\n/gm, "")
|
||||||
|
.split('\n')
|
||||||
|
.filter((i) => !!i)
|
||||||
|
|
||||||
|
|
||||||
|
const templatePath = path.resolve(__dirname, `cf_template.template`);
|
||||||
|
const templateString = fs.readFileSync(templatePath, "utf8");
|
||||||
|
const template = JSON.parse(templateString);
|
||||||
|
|
||||||
|
const cmdIdx = template.Resources.AnythingLLMInstance.Properties.UserData['Fn::Base64']['Fn::Join'][1].findIndex((cmd) => cmd === REPLACEMENT_KEY)
|
||||||
|
template.Resources.AnythingLLMInstance.Properties.UserData['Fn::Base64']['Fn::Join'][1].splice(cmdIdx, 1, ...settings);
|
||||||
|
|
||||||
|
const output = path.resolve(__dirname, `aws_cf_deploy_anything_llm.json`);
|
||||||
|
fs.writeFileSync(output, JSON.stringify(template, null, 2), "utf8");
|
||||||
|
|
||||||
|
console.log(chalk.greenBright('[SUCCESS]'), 'Deploy AnythingLLM on AWS CloudFormation using your template document.');
|
||||||
|
console.log(chalk.greenBright('File Created:'), 'aws_cf_deploy_anything_llm.json in aws/cloudformation directory.');
|
||||||
|
console.log(chalk.blueBright('[INFO]'), 'Refer to aws/cloudformation/DEPLOY.md for how to use this file.');
|
||||||
|
|
||||||
|
exit();
|
@ -4,6 +4,7 @@ FROM ubuntu:jammy-20230522 AS base
|
|||||||
# Build arguments
|
# Build arguments
|
||||||
ARG ARG_UID
|
ARG ARG_UID
|
||||||
ARG ARG_GID
|
ARG ARG_GID
|
||||||
|
ARG ARG_CLOUD_BUILD=0 # Default to local docker build
|
||||||
ARG ARCH=amd64 # Default to amd64 if not provided
|
ARG ARCH=amd64 # Default to amd64 if not provided
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies
|
||||||
@ -89,6 +90,17 @@ EXPOSE 3001
|
|||||||
HEALTHCHECK --interval=1m --timeout=10s --start-period=1m \
|
HEALTHCHECK --interval=1m --timeout=10s --start-period=1m \
|
||||||
CMD /bin/bash /usr/local/bin/docker-healthcheck.sh || exit 1
|
CMD /bin/bash /usr/local/bin/docker-healthcheck.sh || exit 1
|
||||||
|
|
||||||
|
# Docker will still install deps as root so need to force chown
|
||||||
|
# or else
|
||||||
|
USER root
|
||||||
|
RUN if [ "$ARG_CLOUD_BUILD" = 1 ] ; then \
|
||||||
|
echo "Reowning all files as user!" && \
|
||||||
|
mkdir -p app/server/storage app/server/storage/documents app/server/storage/vector-cache app/server/storage/lancedb && \
|
||||||
|
touch anythingllm.db && \
|
||||||
|
chown -R anythingllm:anythingllm /app/collector /app/server; \
|
||||||
|
fi
|
||||||
|
USER anythingllm
|
||||||
|
|
||||||
# Run the server
|
# Run the server
|
||||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ services:
|
|||||||
args:
|
args:
|
||||||
ARG_UID: ${UID}
|
ARG_UID: ${UID}
|
||||||
ARG_GID: ${GID}
|
ARG_GID: ${GID}
|
||||||
|
ARG_CLOUD_BUILD: ${CLOUD_BUILD}
|
||||||
ARCH: ${ARCH:-amd64}
|
ARCH: ${ARCH:-amd64}
|
||||||
volumes:
|
volumes:
|
||||||
- "../server/storage:/app/server/storage"
|
- "../server/storage:/app/server/storage"
|
||||||
|
BIN
images/screenshots/cf_outputs.png
Normal file
BIN
images/screenshots/cf_outputs.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 103 KiB |
BIN
images/screenshots/create_stack.png
Normal file
BIN
images/screenshots/create_stack.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
BIN
images/screenshots/upload.png
Normal file
BIN
images/screenshots/upload.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 244 KiB |
@ -15,7 +15,11 @@
|
|||||||
"dev:server": "cd server && yarn dev",
|
"dev:server": "cd server && yarn dev",
|
||||||
"dev:frontend": "cd frontend && yarn start",
|
"dev:frontend": "cd frontend && yarn start",
|
||||||
"prod:server": "cd server && yarn start",
|
"prod:server": "cd server && yarn start",
|
||||||
"prod:frontend": "cd frontend && yarn build"
|
"prod:frontend": "cd frontend && yarn build",
|
||||||
|
"generate:cloudformation": "node aws/cloudformation/generate.mjs"
|
||||||
},
|
},
|
||||||
"private": false
|
"private": false,
|
||||||
|
"devDependencies": {
|
||||||
|
"chalk": "^5.2.0"
|
||||||
|
}
|
||||||
}
|
}
|
8
yarn.lock
Normal file
8
yarn.lock
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
chalk@^5.2.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3"
|
||||||
|
integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==
|
Loading…
Reference in New Issue
Block a user