-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add Backup view * add backup page and api * prettier * form * backups working quite well * touch up
- Loading branch information
1 parent
976e87f
commit fd16ae4
Showing
10 changed files
with
329 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID | ||
AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY | ||
AWS_DEFAULT_REGION=YOUR_AWS_DEFAULT_REGION |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
import React, { useEffect, useState } from 'react' | ||
import { usePollingEffect } from '../../utils/usePollingEffect' | ||
import { ColumnType } from 'antd/es/table' | ||
import { Table, Button, Form, Input, Modal, Tag, Col, Progress, Row, Tooltip, notification } from 'antd' | ||
|
||
interface BackupRow { | ||
id: string | ||
name: string | ||
status: string | ||
error: string | ||
start_time: string | ||
end_time: string | ||
num_files: number | ||
total_size: number | ||
num_entries: number | ||
uncompressed_size: number | ||
compressed_size: number | ||
files_read: number | ||
bytes_read: number | ||
} | ||
|
||
interface Backups { | ||
backups: BackupRow[] | ||
} | ||
|
||
type FieldType = { | ||
database?: string | ||
table?: string | ||
bucket?: string | ||
path?: string | ||
} | ||
|
||
export default function Backups() { | ||
const [backups, setBackups] = useState<Backups>({ | ||
backups: [], | ||
}) | ||
const [loadingBackups, setLoadingBackups] = useState(false) | ||
const [open, setOpen] = useState(false) | ||
const [confirmLoading, setConfirmLoading] = useState(false) | ||
|
||
const [form] = Form.useForm() // Hook to get form API | ||
|
||
const handleSubmit = async () => { | ||
try { | ||
// Validate and get form values | ||
const values = await form.validateFields() | ||
setConfirmLoading(true) | ||
const res = await fetch(`/api/backups`, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify(values), | ||
}) | ||
setOpen(false) | ||
setConfirmLoading(false) | ||
loadData() | ||
return await res.json() | ||
} catch (error) { | ||
notification.error({ | ||
message: 'Creating backup failed', | ||
}) | ||
} | ||
} | ||
|
||
const showModal = () => { | ||
setOpen(true) | ||
} | ||
const handleCancel = () => { | ||
console.log('Clicked cancel button') | ||
setOpen(false) | ||
} | ||
|
||
const loadData = async () => { | ||
try { | ||
const res = await fetch('/api/backups') | ||
const resJson = await res.json() | ||
const backups = { backups: resJson } | ||
console.log(backups) | ||
setBackups(backups) | ||
} catch (err) { | ||
notification.error({ message: 'Failed to load data' }) | ||
} | ||
} | ||
|
||
useEffect(() => { | ||
loadData() | ||
}, []) | ||
|
||
const columns: ColumnType<BackupRow>[] = [ | ||
{ title: 'UUID', dataIndex: 'id' }, | ||
{ title: 'Name', dataIndex: 'name' }, | ||
{ | ||
title: 'Status', | ||
dataIndex: 'status', | ||
render: (_, { status }) => { | ||
var color = 'volcano' | ||
switch (status) { | ||
case 'CREATING_BACKUP' || 'RESTORING': | ||
color = 'black' | ||
break | ||
case 'BACKUP_CREATED' || 'RESTORED': | ||
color = 'green' | ||
break | ||
case 'BACKUP_FAILED' || 'RESTORE_FAILED': | ||
color = 'volcano' | ||
break | ||
} | ||
return ( | ||
<Tag color={color} key={status}> | ||
{status.toUpperCase()} | ||
</Tag> | ||
) | ||
}, | ||
}, | ||
{ title: 'Error', dataIndex: 'error' }, | ||
{ title: 'Start', dataIndex: 'start_time' }, | ||
{ title: 'End', dataIndex: 'end_time' }, | ||
{ title: 'Size', dataIndex: 'total_size' }, | ||
] | ||
|
||
usePollingEffect( | ||
async () => { | ||
loadData() | ||
}, | ||
[], | ||
{ interval: 5000 } | ||
) | ||
|
||
return ( | ||
<div> | ||
<h1 style={{ textAlign: 'left' }}>Backups</h1> | ||
<Button onClick={showModal}>Create Backup</Button> | ||
<br /> | ||
<Modal | ||
title="Create Backup" | ||
open={open} | ||
onOk={handleSubmit} | ||
confirmLoading={confirmLoading} | ||
onCancel={handleCancel} | ||
> | ||
<Form | ||
name="basic" | ||
form={form} | ||
labelCol={{ span: 8 }} | ||
wrapperCol={{ span: 16 }} | ||
style={{ maxWidth: 600 }} | ||
initialValues={{ remember: true }} | ||
autoComplete="on" | ||
> | ||
<Form.Item<FieldType> | ||
label="Database" | ||
name="database" | ||
initialValue="default" | ||
rules={[{ required: true, message: 'Please select a database to back up from' }]} | ||
> | ||
<Input /> | ||
</Form.Item> | ||
|
||
<Form.Item<FieldType> | ||
label="Table" | ||
name="table" | ||
initialValue="test_backup" | ||
rules={[{ required: true, message: 'Please select a table to back up' }]} | ||
> | ||
<Input /> | ||
</Form.Item> | ||
|
||
<Form.Item<FieldType> | ||
label="S3 Bucket" | ||
name="bucket" | ||
initialValue="posthog-clickhouse" | ||
rules={[{ required: true, message: 'What S3 bucket to backup into' }]} | ||
> | ||
<Input /> | ||
</Form.Item> | ||
|
||
<Form.Item<FieldType> | ||
label="S3 Path" | ||
name="path" | ||
initialValue="testing/test_backup/7" | ||
rules={[{ required: true, message: 'What is the path in the bucket to backup to' }]} | ||
> | ||
<Input /> | ||
</Form.Item> | ||
</Form> | ||
</Modal> | ||
<Row gutter={8} style={{ paddingBottom: 8 }}> | ||
<ul> | ||
<Table columns={columns} dataSource={backups.backups} loading={loadingBackups} /> | ||
</ul> | ||
</Row> | ||
<br /> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import structlog | ||
from rest_framework.decorators import action | ||
from rest_framework.request import Request | ||
from rest_framework.response import Response | ||
from rest_framework.viewsets import GenericViewSet | ||
from housewatch.clickhouse import backups | ||
|
||
|
||
logger = structlog.get_logger(__name__) | ||
|
||
|
||
class BackupViewset(GenericViewSet): | ||
def list(self, request: Request) -> Response: | ||
cluster = request.query_params.get("cluster") | ||
return Response(backups.get_backups(cluster=cluster)) | ||
|
||
def retrieve(self, request: Request, pk: str) -> Response: | ||
cluster = request.query_params.get("cluster") | ||
return Response(backups.get_backup(pk, cluster=cluster)) | ||
|
||
@action(detail=True, methods=["post"]) | ||
def restore(self, request: Request, pk: str) -> Response: | ||
backups.restore_backup(pk) | ||
return Response() | ||
|
||
def create(self, request: Request) -> Response: | ||
database = request.data.get("database") | ||
table = request.data.get("table") | ||
bucket = request.data.get("bucket") | ||
path = request.data.get("path") | ||
if table: | ||
res = backups.create_table_backup(database, table, bucket, path) | ||
else: | ||
res = backups.create_database_backup(database, bucket, path) | ||
return Response(res) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
from collections import defaultdict | ||
from housewatch.clickhouse.client import run_query | ||
|
||
from django.conf import settings | ||
|
||
|
||
def get_backups(cluster=None): | ||
if cluster: | ||
QUERY = """SELECT * FROM clusterAllReplicas(%(cluster)s, system.backups) ORDER BY start_time DESC""" | ||
else: | ||
QUERY = """SELECT * FROM system.backups ORDER BY start_time DESC""" | ||
res = run_query(QUERY, {"cluster": cluster}, use_cache=False) | ||
return res | ||
|
||
|
||
def get_backup(backup, cluster=None): | ||
if cluster: | ||
QUERY = """Select * FROM clusterAllReplicas(%(cluster)s, system.backups) WHERE id = '%(uuid)s' """ | ||
return run_query(QUERY, {"cluster": cluster, "uuid": backup}, use_cache=False) | ||
else: | ||
QUERY = """Select * FROM system.backups WHERE id = '%(uuid)s' """ | ||
return run_query(QUERY, {"uuid": backup}, use_cache=False) | ||
|
||
|
||
def create_table_backup(database, table, bucket, path, aws_key=None, aws_secret=None): | ||
if aws_key is None or aws_secret is None: | ||
aws_key = settings.AWS_ACCESS_KEY_ID | ||
aws_secret = settings.AWS_SECRET_ACCESS_KEY | ||
QUERY = """BACKUP TABLE %(database)s.%(table)s | ||
TO S3('https://%(bucket)s.s3.amazonaws.com/%(path)s', '%(aws_key)s', '%(aws_secret)s') | ||
ASYNC""" | ||
return run_query( | ||
QUERY, | ||
{ | ||
"database": database, | ||
"table": table, | ||
"bucket": bucket, | ||
"path": path, | ||
"aws_key": aws_key, | ||
"aws_secret": aws_secret, | ||
}, | ||
use_cache=False, | ||
) | ||
|
||
|
||
def create_database_backup(database, bucket, path, aws_key=None, aws_secret=None): | ||
if aws_key is None or aws_secret is None: | ||
aws_key = settings.AWS_ACCESS_KEY_ID | ||
aws_secret = settings.AWS_SECRET_ACCESS_KEY | ||
QUERY = """BACKUP DATABASE %(database)s | ||
TO S3('https://%(bucket)s.s3.amazonaws.com/%(path)s', '%(aws_key)s', '%(aws_secret)s') | ||
ASYNC""" | ||
return run_query( | ||
QUERY, | ||
{ | ||
"database": database, | ||
"bucket": bucket, | ||
"path": path, | ||
"aws_key": aws_key, | ||
"aws_secret": aws_secret, | ||
}, | ||
use_cache=False, | ||
) | ||
|
||
|
||
def restore_backup(backup): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
CREATE TABLE test_backup ( | ||
id UUID DEFAULT generateUUIDv4(), | ||
name String, | ||
timestamp DateTime DEFAULT now() | ||
) ENGINE = MergeTree() | ||
ORDER BY id; | ||
INSERT INTO test_backup (name) | ||
SELECT substring(toString(rand() * 1000000000), 1, 5) AS random_string | ||
FROM numbers(100); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters