-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Expand file tree
/
Copy pathviews.py
More file actions
249 lines (230 loc) · 10.7 KB
/
Copy pathviews.py
File metadata and controls
249 lines (230 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.views.generic import View
from django.db.models import F
from django.http.response import HttpResponseBadRequest
from libs import json_response, JsonParser, Argument, AttrDict, auth
from apps.setting.utils import AppSetting
from apps.account.utils import get_host_perms
from apps.host.models import Host, Group
from apps.host.utils import batch_sync_host, _sync_host_extend
from apps.exec.models import ExecTemplate
from apps.app.models import Deploy
from apps.schedule.models import Task
from apps.monitor.models import Detection
from libs.ssh import SSH
from libs.telnet import Telnet, AuthenticationException
from paramiko.ssh_exception import BadAuthenticationType
from openpyxl import load_workbook
from threading import Thread
import socket
import uuid
class HostView(View):
def get(self, request):
hosts = Host.objects.select_related('hostextend')
if not request.user.is_supper:
hosts = hosts.filter(id__in=get_host_perms(request.user))
hosts = {x.id: x.to_view() for x in hosts}
for rel in Group.hosts.through.objects.filter(host_id__in=hosts.keys()):
hosts[rel.host_id]['group_ids'].append(rel.group_id)
return json_response(list(hosts.values()))
@auth('host.host.add|host.host.edit')
def post(self, request):
form, error = JsonParser(
Argument('id', type=int, required=False),
Argument('group_ids', type=list, filter=lambda x: len(x), help='请选择主机分组'),
Argument('name', help='请输主机名称'),
Argument('username', handler=str.strip, help='请输入登录用户名'),
Argument('hostname', handler=str.strip, help='请输入主机名或IP'),
Argument('port', type=int, help='请输入SSH端口'),
Argument('pkey', required=False),
Argument('desc', required=False),
Argument('password', required=False),
).parse(request.body)
if error is None:
if not _do_host_verify(form):
return json_response('auth fail')
group_ids = form.pop('group_ids')
other = Host.objects.filter(name=form.name).first()
if other and (not form.id or other.id != form.id):
return json_response(error=f'已存在的主机名称【{form.name}】')
if form.id:
Host.objects.filter(pk=form.id).update(is_verified=True, **form)
host = Host.objects.get(pk=form.id)
else:
host = Host.objects.create(created_by=request.user, is_verified=True, **form)
host.groups.set(group_ids)
response = host.to_view()
response['group_ids'] = group_ids
return json_response(response)
return json_response(error=error)
@auth('host.host.add|host.host.edit')
def put(self, request):
form, error = JsonParser(
Argument('id', type=int, help='参数错误')
).parse(request.body)
if error is None:
host = Host.objects.get(pk=form.id)
with host.get_ssh() as ssh:
_sync_host_extend(host, ssh=ssh)
return json_response(error=error)
@auth('admin')
def patch(self, request):
form, error = JsonParser(
Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择主机'),
Argument('s_group_id', type=int, help='参数错误'),
Argument('t_group_id', type=int, help='参数错误'),
Argument('is_copy', type=bool, help='参数错误'),
).parse(request.body)
if error is None:
if form.t_group_id == form.s_group_id:
return json_response(error='不能选择本分组的主机')
s_group = Group.objects.get(pk=form.s_group_id)
t_group = Group.objects.get(pk=form.t_group_id)
t_group.hosts.add(*form.host_ids)
if not form.is_copy:
s_group.hosts.remove(*form.host_ids)
return json_response(error=error)
@auth('host.host.del')
def delete(self, request):
form, error = JsonParser(
Argument('id', type=int, required=False),
Argument('group_id', type=int, required=False),
).parse(request.GET)
if error is None:
if form.id:
host_ids = [form.id]
elif form.group_id:
group = Group.objects.get(pk=form.group_id)
host_ids = [x.id for x in group.hosts.all()]
else:
return json_response(error='参数错误')
for host_id in host_ids:
regex = fr'[^0-9]{host_id}[^0-9]'
deploy = Deploy.objects.filter(host_ids__regex=regex) \
.annotate(app_name=F('app__name'), env_name=F('env__name')).first()
if deploy:
return json_response(error=f'应用【{deploy.app_name}】在【{deploy.env_name}】的发布配置关联了该主机,请解除关联后再尝试删除该主机')
task = Task.objects.filter(targets__regex=regex).first()
if task:
return json_response(error=f'任务计划中的任务【{task.name}】关联了该主机,请解除关联后再尝试删除该主机')
detection = Detection.objects.filter(type__in=('3', '4'), targets__regex=regex).first()
if detection:
return json_response(error=f'监控中心的任务【{detection.name}】关联了该主机,请解除关联后再尝试删除该主机')
tpl = ExecTemplate.objects.filter(host_ids__regex=regex).first()
if tpl:
return json_response(error=f'执行模板【{tpl.name}】关联了该主机,请解除关联后再尝试删除该主机')
Host.objects.filter(id__in=host_ids).delete()
return json_response(error=error)
@auth('host.host.add')
def post_import(request):
group_id = request.POST.get('group_id')
file = request.FILES['file']
hosts = []
ws = load_workbook(file, read_only=True)['Sheet1']
summary = {'fail': 0, 'success': 0, 'invalid': [], 'skip': [], 'repeat': []}
for i, row in enumerate(ws.rows, start=1):
if i == 1: # 第1行是表头 略过
continue
if not all([row[x].value for x in range(4)]):
summary['invalid'].append(i)
summary['fail'] += 1
continue
data = AttrDict(
name=row[0].value,
hostname=row[1].value,
port=row[2].value,
username=row[3].value,
desc=row[5].value
)
if Host.objects.filter(hostname=data.hostname, port=data.port, username=data.username).exists():
summary['skip'].append(i)
summary['fail'] += 1
continue
if Host.objects.filter(name=data.name).exists():
summary['repeat'].append(i)
summary['fail'] += 1
continue
host = Host.objects.create(created_by=request.user, **data)
host.groups.add(group_id)
summary['success'] += 1
host.password = row[4].value
hosts.append(host)
token = uuid.uuid4().hex
if hosts:
Thread(target=batch_sync_host, args=(token, hosts)).start()
return json_response({'summary': summary, 'token': token, 'hosts': {x.id: {'name': x.name} for x in hosts}})
@auth('host.host.add')
def post_parse(request):
file = request.FILES['file']
if file:
data = file.read()
return json_response(data.decode())
else:
return HttpResponseBadRequest()
@auth('host.host.add')
def batch_valid(request):
form, error = JsonParser(
Argument('password', required=False),
Argument('range', filter=lambda x: x in ('1', '2'), help='参数错误')
).parse(request.body)
if error is None:
if form.range == '1': # all hosts
hosts = Host.objects.all()
else:
hosts = Host.objects.filter(is_verified=False).all()
token = uuid.uuid4().hex
Thread(target=batch_sync_host, args=(token, hosts, form.password)).start()
return json_response({'token': token, 'hosts': {x.id: {'name': x.name} for x in hosts}})
return json_response(error=error)
def _do_host_verify(form):
password = form.pop('password', None)
connect_type = form.pop('connect_type', 'ssh')
if connect_type == 'telnet':
if not password:
return False
try:
with Telnet(form.hostname, form.port or 23, form.username, password) as tn:
return True
except AuthenticationException:
raise Exception('Telnet认证失败,请检查用户名和密码是否正确')
except socket.timeout:
raise Exception('连接主机超时,请检查网络')
except Exception as e:
raise Exception(f'Telnet连接失败: {str(e)}')
# SSH验证逻辑
if form.pkey:
try:
with SSH(form.hostname, form.port, form.username, form.pkey) as ssh:
ssh.ping()
return True
except BadAuthenticationType:
raise Exception('该主机不支持密钥认证,请参考官方文档,错误代码:E01')
except AuthenticationException:
raise Exception('上传的独立密钥认证失败,请检查该密钥是否能正常连接主机(推荐使用全局密钥)')
except socket.timeout:
raise Exception('连接主机超时,请检查网络')
private_key, public_key = AppSetting.get_ssh_key()
if password:
try:
with SSH(form.hostname, form.port, form.username, password=password) as ssh:
ssh.add_public_key(public_key)
except BadAuthenticationType:
raise Exception('该主机不支持密码认证,请参考官方文档,错误代码:E00')
except AuthenticationException:
raise Exception('密码连接认证失败,请检查密码是否正确')
except socket.timeout:
raise Exception('连接主机超时,请检查网络')
try:
with SSH(form.hostname, form.port, form.username, private_key) as ssh:
ssh.ping()
except BadAuthenticationType:
raise Exception('该主机不支持密钥认证,请参考官方文档,错误代码:E01')
except AuthenticationException:
if password:
raise Exception('密钥认证失败,请参考官方文档,错误代码:E02')
return False
except socket.timeout:
raise Exception('连接主机超时,请检查网络')
return True