完善主机编辑

pull/21/merge
ouqiang 2017-04-20 09:36:42 +08:00
parent f57561118d
commit 0a9e71cb5a
13 changed files with 268 additions and 81 deletions

View File

@ -99,6 +99,7 @@ func catchSignal() {
}
}
// 作为守护进程运行
func becomeDaemon(ctx *cli.Context) {
// 不支持windows
if app.IsWindows {

View File

@ -1,5 +1,9 @@
package models
import "github.com/ouqiang/gocron/modules/ssh"
// 主机
type Host struct {
Id int16 `xorm:"smallint pk autoincr"`
@ -9,6 +13,8 @@ type Host struct {
Password string `xorm:"varchar(64) notnull default ''"` // ssh 密码
Port int `xorm:"notnull default 22"` // 主机端口
Remark string `xorm:"varchar(512) notnull default '' "` // 备注
AuthType ssh.HostAuthType `xorm:"tinyint notnull default 1"` // 认证方式 1: 密码 2: 公钥
PrivateKey string `xorm:"varchar(4096) notnull default '' "` // 私钥
Page int `xorm:"-"`
PageSize int `xorm:"-"`
}
@ -23,6 +29,11 @@ func (host *Host) Create() (insertId int16, err error) {
return
}
func (host *Host) UpdateBean() (int64, error) {
return Db.Cols("name,alias,username,password,port,remark,auth_type,private_key").Update(host)
}
// 更新
func (host *Host) Update(id int, data CommonMap) (int64, error) {
return Db.Table(host).ID(id).Update(data)
@ -39,9 +50,13 @@ func (host *Host) Find(id int) error {
return err
}
func (host *Host) NameExists(name string) (bool, error) {
count, err := Db.Where("name = ?", name).Count(host);
func (host *Host) NameExists(name string, id int16) (bool, error) {
if id == 0 {
count, err := Db.Where("name = ?", name).Count(host);
return count > 0, err
}
count, err := Db.Where("name = ? AND id != ?", name, id).Count(host);
return count > 0, err
}

View File

@ -2,6 +2,7 @@ package models
import (
"time"
"github.com/ouqiang/gocron/modules/ssh"
)
type TaskProtocol int8
@ -36,6 +37,8 @@ type TaskHost struct {
Username string
Password string
Alias string
AuthType ssh.HostAuthType
PrivateKey string
}
func (TaskHost) TableName() string {
@ -52,7 +55,7 @@ func (task *Task) Create() (insertId int, err error) {
return
}
func (task *Task) UpdateBean(id int) (int64, error) {
func (task *Task) UpdateBean() (int64, error) {
return Db.UseBool("status").Update(task)
}
@ -76,15 +79,26 @@ func (task *Task) Enable(id int) (int64, error) {
return task.Update(id, CommonMap{"status": Enabled})
}
// 获取所有激活任务
func (task *Task) ActiveList() ([]TaskHost, error) {
task.parsePageAndPageSize()
list := make([]TaskHost, 0)
fields := "t.*, host.name,host.username,host.password,host.port"
fields := "t.*, host.alias,host.name,host.username,host.password,host.port,host.auth_type,host.private_key"
err := Db.Alias("t").Join("LEFT", "host", "t.host_id=host.id").Where("t.status = ?", Enabled).Cols(fields).Find(&list)
return list, err
}
// 获取某个主机下的所有激活任务
func (task *Task) ActiveListByHostId(hostId int16) ([]TaskHost, error) {
task.parsePageAndPageSize()
list := make([]TaskHost, 0)
fields := "t.*, host.alias,host.name,host.username,host.password,host.port,host.auth_type,host.private_key"
err := Db.Alias("t").Join("LEFT", "host", "t.host_id=host.id").Where("t.status = ? AND t.host_id = ?", Enabled, hostId).Cols(fields).Find(&list)
return list, err
}
// 判断主机id是否有引用
func (task *Task) HostIdExist(hostId int16) (bool, error) {
count, err := Db.Where("host_id = ?", hostId).Count(task);
@ -105,7 +119,7 @@ func (task *Task) NameExist(name string, id int) (bool, error) {
func(task *Task) Detail(id int) (TaskHost, error) {
taskHost := TaskHost{}
fields := "t.*, host.name,host.username,host.password,host.port"
fields := "t.*, host.name,host.username,host.password,host.port,host.auth_type,host.private_key"
_, err := Db.Alias("t").Join("LEFT", "host", "t.host_id=host.id").Where("t.id=?", id).Cols(fields).Get(&taskHost)
return taskHost, err

View File

@ -8,9 +8,22 @@ import (
"errors"
)
type HostAuthType int8 // 认证方式
const (
HostPassword = 1 // 密码认证
HostPublicKey = 2 // 公钥认证
)
const SSHConnectTimeout = 10
type SSHConfig struct {
AuthType HostAuthType
User string
Password string
PrivateKey string
Host string
Port int
ExecTimeout int// 执行超时时间
@ -21,6 +34,45 @@ type Result struct {
Err error
}
func parseSSHConfig(sshConfig SSHConfig) (config *ssh.ClientConfig, err error) {
timeout := SSHConnectTimeout * time.Second
// 密码认证
if sshConfig.AuthType == HostPassword {
config = &ssh.ClientConfig{
User: sshConfig.User,
Auth: []ssh.AuthMethod{
ssh.Password(sshConfig.Password),
},
Timeout: timeout,
HostKeyCallback:func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
return
}
signer, err := ssh.ParsePrivateKey([]byte(sshConfig.PrivateKey))
if err != nil {
return
}
// 公钥认证
config = &ssh.ClientConfig{
User: sshConfig.User,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
Timeout: timeout,
HostKeyCallback:func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
return
}
// 执行shell命令
func Exec(sshConfig SSHConfig, cmd string) (output string, err error) {
client, err := getClient(sshConfig)
@ -56,15 +108,9 @@ func Exec(sshConfig SSHConfig, cmd string) (output string, err error) {
}
func getClient(sshConfig SSHConfig) (*ssh.Client, error) {
config := &ssh.ClientConfig{
User: sshConfig.User,
Auth: []ssh.AuthMethod{
ssh.Password(sshConfig.Password),
},
Timeout: 10 * time.Second,
HostKeyCallback:func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
config, err := parseSSHConfig(sshConfig)
if err != nil {
return nil, err
}
addr := fmt.Sprintf("%s:%d", sshConfig.Host, sshConfig.Port)
@ -80,3 +126,4 @@ func triggerTimeout(ch chan bool, timeout int){
time.Sleep(time.Duration(timeout) * time.Second)
close(ch)
}

View File

@ -4,36 +4,43 @@
function Util() {
var util = {};
util.post = function(url, params, callback) {
// 用户认证失败
var SUCCESS = 0;
var FAILURE = 1;
var NOT_FOUND = 2;
var AUTH_ERROR = 3;
var SUCCESS = 0; // 操作成功
var FAILURE_MESSAGE = '操作失败';
util.ajaxSuccess = function(response, callback) {
if (response.code === undefined) {
swal(FAILURE_MESSAGE, '服务端返回值无法解析', 'error');
return;
}
if (response.code != SUCCESS) {
swal(FAILURE_MESSAGE, response.message ,'error');
return;
}
callback(response.code, response.message, response.data);
};
util.ajaxFailure = function() {
// todo 错误处理
swal(FAILURE_MESSAGE, '未知错误', 'error');
};
util.get = function(url, callback) {
var SUCCESS = 0; // 操作成功
var FAILURE_MESSAGE = '操作失败';
$.get(
url,
function(response) {
util.ajaxSuccess(response, callback);
},
'json'
).error(util.ajaxFailure);
};
util.post = function(url, params, callback) {
$.post(
url,
params,
function(response) {
if (response.code === undefined) {
swal(FAILURE_MESSAGE, '服务端返回值无法解析', 'error');
}
if (response.code == AUTH_ERROR) {
swal(FAILURE_MESSAGE, response.message, 'error');
return;
}
if (response.code == NOT_FOUND) {
swal(FAILURE_MESSAGE, response.message, 'error');
return;
}
if (response.code == FAILURE) {
swal(FAILURE_MESSAGE, response.message ,'error');
return;
}
callback(response.code, response.message, response.data);
util.ajaxSuccess(response, callback);
},
'json'
)
).error(util.ajaxFailure);
};
util.confirm = function(message, callback) {
swal({

View File

@ -6,6 +6,8 @@ import (
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/modules/logger"
"strconv"
"github.com/ouqiang/gocron/modules/ssh"
"github.com/ouqiang/gocron/service"
)
func Index(ctx *macaron.Context) {
@ -36,36 +38,87 @@ func Edit(ctx *macaron.Context) {
ctx.HTML(200, "host/host_form")
}
func Ping(ctx *macaron.Context) string {
id := ctx.ParamsInt(":id")
hostModel := new(models.Host)
err := hostModel.Find(id)
json := utils.JsonResponse{}
if err != nil || hostModel.Id <= 0{
return json.CommonFailure("主机不存在", err)
}
sshConfig := ssh.SSHConfig{
User: hostModel.Username,
Password: hostModel.Password,
Host: hostModel.Name,
Port: hostModel.Port,
ExecTimeout: 5,
AuthType: hostModel.AuthType,
PrivateKey: hostModel.PrivateKey,
}
_, err = ssh.Exec(sshConfig, "pwd")
if err != nil {
return json.CommonFailure("连接失败-" + err.Error(), err)
}
return json.Success("连接成功", nil)
}
type HostForm struct {
Id int16
Name string `binding:"Required;MaxSize(100)"`
Alias string `binding:"Required;MaxSize(32)"`
Username string `binding:"Required;MaxSize(32)"`
Password string `binding:"Required;MaxSize(64)"`
Password string
Port int `binding:"Required;Range(1-65535)"`
AuthType ssh.HostAuthType `binding:"Required:Range(1,2)"`
PrivateKey string
Remark string
}
func Store(ctx *macaron.Context, form HostForm) string {
json := utils.JsonResponse{}
hostModel := new(models.Host)
nameExist, err := hostModel.NameExists(form.Name)
id := form.Id
nameExist, err := hostModel.NameExists(form.Name, form.Id)
if err != nil {
return json.CommonFailure("操作失败", err)
}
if nameExist {
return json.CommonFailure("主机名已存在")
}
if form.Id > 0 {
hostModel.Id = form.Id
}
hostModel.Name = form.Name
hostModel.Alias = form.Alias
hostModel.Username = form.Username
hostModel.Password = form.Password
hostModel.Port = form.Port
hostModel.Remark = form.Remark
_, err = hostModel.Create()
hostModel.PrivateKey = form.PrivateKey
hostModel.AuthType = form.AuthType
isCreate := false
if id > 0 {
_, err = hostModel.UpdateBean()
} else {
isCreate = true
id, err = hostModel.Create()
}
if err != nil {
return json.CommonFailure("保存失败", err)
}
taskModel := new(models.TaskHost)
tasks, err := taskModel.ActiveListByHostId(id)
if err != nil {
return json.CommonFailure("刷新任务主机信息失败", err)
}
if !isCreate && len(tasks) > 0 {
serviceTask := new(service.Task)
serviceTask.BatchAdd(tasks)
}
return json.Success("保存成功", nil)
}

View File

@ -72,7 +72,8 @@ func Register(m *macaron.Macaron) {
// 主机
m.Group("/host", func() {
m.Get("/create", host.Create)
m.Get("/Edit", host.Edit)
m.Get("/edit/:id", host.Edit)
m.Get("/ping/:id", host.Ping)
m.Post("/store", binding.Bind(host.HostForm{}), host.Store)
m.Get("", host.Index)
m.Post("/remove/:id", host.Remove)

View File

@ -45,7 +45,7 @@ func Edit(ctx *macaron.Context) {
}
taskModel := new(models.Task)
task, err := taskModel.Detail(id)
if err != nil || taskModel.Id != id {
if err != nil || task.Id != id {
logger.Errorf("编辑任务#获取任务详情失败#任务ID-%d#%s", id, err.Error())
ctx.Redirect("/task")
}
@ -92,6 +92,9 @@ func Store(ctx *macaron.Context, form TaskForm) string {
return json.CommonFailure("请选择主机名")
}
if form.Id > 0 {
taskModel.Id = form.Id
}
taskModel.Name = form.Name
taskModel.Protocol = form.Protocol
taskModel.Command = form.Command
@ -106,7 +109,7 @@ func Store(ctx *macaron.Context, form TaskForm) string {
if id == 0 {
id, err = taskModel.Create()
} else {
_, err = taskModel.UpdateBean(id)
_, err = taskModel.UpdateBean()
}
if err != nil {
return json.CommonFailure("保存失败", err)

View File

@ -9,7 +9,6 @@ import (
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/ssh"
"github.com/jakecoffman/cron"
"strings"
"github.com/ouqiang/gocron/modules/utils"
"errors"
)
@ -32,7 +31,12 @@ func (task *Task) Initialize() {
logger.Debug("任务列表为空")
return
}
for _, item := range taskList {
task.BatchAdd(taskList)
}
// 批量添加任务
func (task *Task) BatchAdd(tasks []models.TaskHost) {
for _, item := range tasks {
task.Add(item)
}
}
@ -65,14 +69,10 @@ func (h *LocalCommandHandler) Run(taskModel models.TaskHost) (string, error) {
if taskModel.Command == "" {
return "", errors.New("invalid command")
}
fields := strings.Split(taskModel.Command, " ")
var args []string
if len(fields) > 1 {
args = fields[1:]
} else {
args = []string{}
}
return utils.ExecShellWithTimeout(taskModel.Timeout, fields[0], args...)
args := []string{"-c", taskModel.Command}
return utils.ExecShellWithTimeout(taskModel.Timeout, "/bin/bash", args...)
}
// HTTP任务
@ -83,7 +83,7 @@ func (h *HTTPHandler) Run(taskModel models.TaskHost) (result string, err error)
if taskModel.Timeout > 0 {
client.Timeout = time.Duration(taskModel.Timeout) * time.Second
}
req, err := http.NewRequest("POST", taskModel.Command, nil)
req, err := http.NewRequest("GET", taskModel.Command, nil)
if err != nil {
logger.Error("任务处理#创建HTTP请求错误-", err.Error())
return
@ -119,6 +119,8 @@ func (h *SSHCommandHandler) Run(taskModel models.TaskHost) (string, error) {
Host: taskModel.Name,
Port: taskModel.Port,
ExecTimeout: taskModel.Timeout,
AuthType: taskModel.AuthType,
PrivateKey: taskModel.PrivateKey,
}
return ssh.Exec(sshConfig, taskModel.Command)
}
@ -132,7 +134,7 @@ func createTaskLog(taskModel models.TaskHost) (int64, error) {
taskLogModel.Protocol = taskModel.Protocol
taskLogModel.Command = taskModel.Command
taskLogModel.Timeout = taskModel.Timeout
taskLogModel.Hostname = taskModel.Name
taskLogModel.Hostname = taskModel.Alias + "-" + taskModel.Name
taskLogModel.StartTime = time.Now()
taskLogModel.Status = models.Running
insertId, err := taskLogModel.Create()

View File

@ -10,46 +10,72 @@
<h3 class="ui dividing header">
<i class="large add icon"></i>
<div class="content">
添加主机
{{{.Title}}}
</div>
</h3>
</div>
</div>
<form class="ui form fluid vertical segment">
<div class="two fields">
<input type="hidden" name="id" value="{{{.Host.Id}}}">
<div class="four fields">
<div class="field">
<label>主机名 (域名或IP)</label>
<div class="ui small left icon input">
<input type="text" placeholder="127.0.0.1" name="name">
<input type="text" name="name" value="{{{.Host.Name}}}">
</div>
</div>
<div class="field">
<label>主机别名 (方便记忆和引用)</label>
<div class="ui small left icon input">
<input type="text" placeholder="db" name="alias">
<input type="text" name="alias" value="{{{.Host.Alias}}}">
</div>
</div>
</div>
<div class="two fields">
<div class="four fields">
<div class="field">
<label>SSH用户名</label>
<div class="ui small left icon input">
<input type="text" placeholder="root" name="username">
<input type="text" name="username" value="{{{.Host.Username}}}">
</div>
</div>
<div class="field">
<label>SSH密码</label>
<div class="ui small left icon input">
<input type="text" placeholder="123456" name="password">
</div>
</div>
</div>
<div class="two fields">
<div class="field">
<label>SSH端口</label>
<div class="ui small left icon input">
<input type="text" placeholder="22" name="port" value="22">
<input type="text" name="port" value="{{{.Host.Port}}}">
</div>
</div>
</div>
<div class="four fields">
<div class="field">
<label>认证方式</label>
<div class="ui dropdown selection">
{{{ if .Host }}}
<input type="hidden" name="auth_type" value="{{{if eq .Host.AuthType 1 }}}1{{{else}}}2{{{end}}}">
{{{else}}}
<input type="hidden" name="auth_type" value="2">
{{{end}}}
<div class="default text">公钥</div>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item" data-value="2">公钥</div>
<div class="item" data-value="1">密码</div>
</div>
</div>
</div>
</div>
<div class="two fields">
<div class="field">
<label>私钥 (~/.ssh/id_rsa)</label>
<div class="ui small left icon input">
<textarea rows="7" name="private_key">{{{.Host.PrivateKey}}}</textarea>
</div>
</div>
</div>
<div class="four fields">
<div class="field">
<label>SSH密码</label>
<div class="ui small left icon input">
<input type="text" placeholder="" name="password" value="{{{.Host.Password}}}">
</div>
</div>
</div>
@ -57,7 +83,7 @@
<div class="field">
<label>备注</label>
<div class="ui small left icon input">
<textarea rows="5" name="remark" placeholder="数据库服务器"></textarea>
<textarea rows="7" name="remark" >{{{.Host.Remark}}}</textarea>
</div>
</div>
</div>

View File

@ -20,8 +20,8 @@
<th>主机名</th>
<th>别名</th>
<th>用户名</th>
<th>密码</th>
<th>端口</th>
<th>任务数量</th>
<th>备注</th>
<th>操作</th>
</tr>
@ -32,14 +32,13 @@
<td>{{{.Name}}}</td>
<td>{{{.Alias}}}</td>
<td>{{{.Username}}}</td>
<td>{{{.Password}}}</td>
<td>{{{.Port}}}</td>
<td></td>
<td>{{{.Remark}}}</td>
<td>
<button class="ui pink button" >编辑</button>
<td id="operation">
<a class="ui purple button" href="/host/edit/{{{.Id}}}">编辑</a>
<button class="ui positive button" onclick="util.removeConfirm('/host/remove/{{{.Id}}}')">删除</button>
<button class="ui pink button" >连接测试</button>
<button class="ui pink button" >查看任务</button>
<button class="ui blue button" @click="ping({{{.Id}}})">连接测试</button>
</td>
</tr>
{{{end}}}
@ -48,4 +47,23 @@
</div>
</div>
<script type="text/javascript">
var Vue = new Vue({
el: '#operation',
methods: {
ping: function(id) {
swal({
title: '',
text: "连接中.......",
type: 'info',
closeOnConfirm: true
});
util.get("/host/ping/" + id, function(code, message) {
swal('操作成功', '连接成功', 'success');
})
}
}
});
</script>
{{{ template "common/footer" . }}}

View File

@ -46,7 +46,7 @@
<button class="ui blue button" onclick="changeStatus({{{.Id}}},{{{.Status}}})">激活 </button>
{{{end}}}
<button class="ui positive button" onclick="util.removeConfirm('/task/remove/{{{.Id}}}')">删除</button>
<button class="ui pink button">查看日志</button>
<button class="ui instagram button">查看日志</button>
</td>
</tr>
{{{end}}}

View File

@ -59,7 +59,7 @@
{{{if eq $.Task.HostId .Id}}} checked {{{end}}}
>
{{{else}}}
<input type="radio" name="host_id" tabindex="0" class="hidden">
<input type="radio" name="host_id" tabindex="0" class="hidden" value="{{{.Id}}}">
{{{end}}}
<label>{{{.Alias}}}-{{{.Name}}}</label>
</div>