├── .gitattributes ├── .gitignore ├── README.md ├── configure ├── account.go └── configure.go ├── database └── database.go ├── go.mod ├── go.sum ├── handlers ├── host.go ├── hostCrontab.go ├── hostCrontabJob.go ├── hostgroup.go ├── hostgroupHost.go ├── html.go ├── operationRecord.go └── utils.go ├── logger └── logger.go ├── main.go ├── models └── models.go ├── router └── router.go ├── scripts ├── create_host_crontab_job.py ├── delete_host_crontab_job.py ├── get_host_crontab.py └── update_host_crontab_job.py ├── static ├── axios-0.18.0 │ └── axios.min.js ├── bootstrap-3.3.7 │ ├── css │ │ └── bootstrap.min.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ └── bootstrap.min.js ├── css │ └── tree.css ├── font-awesome-4.7.0 │ ├── css │ │ ├── font-awesome.css │ │ └── font-awesome.min.css │ └── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 ├── img │ ├── favicon.ico │ ├── login-logo.ico │ └── title-logo.png ├── jquery-2.1.1 │ └── jquery.min.js ├── jquery-jsonview-1.2.3 │ ├── jquery.jsonview.min.css │ └── jquery.jsonview.min.js ├── js │ └── crontab.js ├── toastr-2.1.3 │ ├── toastr.css │ └── toastr.min.js └── vue-2.2.2 │ └── vue.min.js ├── table.sql └── templates ├── addHostgroupHostsModal.html ├── createCrontabJobModal.html ├── createHostgroupModal.html ├── crontab.html ├── footer.html ├── head.html ├── host.html ├── nav.html ├── operation_record.html ├── removeHostgroupHostsModal.html ├── updateCrontabJobModal.html ├── updateHostModal.html └── updateHostgoupModal.html /.gitattributes: -------------------------------------------------------------------------------- 1 | templates/* linguist-vendored 2 | scripts/* linguist-vendored -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .idea 3 | */.DS_Store 4 | */.idea 5 | cronnest 6 | dump.rdb 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cronnest 2 | 一个真正的Crontab管理平台。 3 | *** 4 | ## Introduction 5 | Cronnest 是一个对Linux Crontab任务进行统一管理的平台工具,通过 SSH实现对远程设备Crontab的全面管理。 6 | 7 | **特点:** 没有多余概念,也没有特有名词,更没有学习成本,致力于提供清晰、简单的使用功能。 8 | 9 | ### DEMO 10 | 地址: 11 | 12 | 用户:guest 13 | 14 | 密码:guest123 15 | 16 | ### Web UI 17 | ![image](https://github.com/olajowon/exhibitions/blob/master/cronnest/crontab.png?raw=true) 18 | 19 | ### 组件 20 | GO 1.3 21 | 22 | Gin 1.4 23 | 24 | PgSQL 11 (没错不是MySQL、也不是NoSQL) 25 | 26 | VUE 2.2 (但这不是一个前后端分离项目) 27 | 28 | ## Installation & Configuration 29 | ### 下载 30 | git clone http://git@github.com:olajowon/cronnest.git 31 | 32 | ### 修改配置 configure/configure.go 33 | 34 | // PgSQL 连接信息 35 | PgSQL = "host=localhost user=zhouwang dbname=cronnest sslmode=disable password=123456" 36 | // SSH 配置 37 | SSH = map[string]string { 38 | "user": "root", 39 | "password": "", 40 | "privateKeyPath": "/Users/zhouwang/.ssh/id_rsa", // ras 私钥绝对路径 (优先) 41 | "port": "22", // 端口,注意是字符串 42 | } 43 | 44 | ### 用户配置 configure/account.go 45 | Accounts = map[string]string { 46 | "zhangsan": "123456", // 用户名: 密码, 47 | } 48 | 49 | ### 建表Sql 50 | table.sql // 烦请手动建表 51 | 52 | ## Start Up 53 | 54 | ### 编译 55 | go build -o cronnest . 56 | 57 | ### 启动 58 | ./cronnest 59 | 60 | ### 访问 61 | http://localhost:9900/html/crontab/ 62 | 63 | -------------------------------------------------------------------------------- /configure/account.go: -------------------------------------------------------------------------------- 1 | package configure 2 | 3 | var Accounts map[string]string 4 | 5 | func init() { 6 | Accounts = map[string]string { 7 | "admin": "admin", // 用户名: 密码 8 | "guest": "guest123", 9 | } 10 | } -------------------------------------------------------------------------------- /configure/configure.go: -------------------------------------------------------------------------------- 1 | package configure 2 | 3 | var PgSQL string 4 | var Log map[string]string 5 | var SystemCrontabFile string 6 | var UserCrontabDir string 7 | var SSH map[string]string 8 | 9 | func init() { 10 | PgSQL = "host=localhost user=root dbname=cronnest sslmode=disable password=123456" // pgsql 11 | Log = map[string]string { 12 | "request": "/var/log/cronnest/request.log", 13 | "cronnest": "/var/log/cronnest/cronnest.log", 14 | } 15 | SSH = map[string]string { 16 | "user": "zhouwang", 17 | "password": "", 18 | "privateKeyPath": "/Users/zhouwang/.ssh/id_rsa", // ras 私钥绝对路径 (优先) 19 | "port": "22", // 端口,注意是字符串 20 | } 21 | SystemCrontabFile = "/etc/crontab" // 系统crontab文件 22 | UserCrontabDir = "/var/spool/cron" // 用户crontab目录 23 | } 24 | -------------------------------------------------------------------------------- /database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "cronnest/configure" 6 | "log" 7 | ) 8 | 9 | var DB *gorm.DB 10 | var err error 11 | 12 | func init() { 13 | DB, err = gorm.Open("postgres", configure.PgSQL) 14 | if err != nil { 15 | log.Fatalf("数据库连接失败, %v", err) 16 | } 17 | 18 | if DB.Error != nil { 19 | log.Fatalf("数据库错误, %v", DB.Error) 20 | } 21 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module cronnest 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 7 | github.com/fatih/color v1.7.0 // indirect 8 | github.com/gin-contrib/sse v0.1.0 // indirect 9 | github.com/gin-gonic/gin v1.4.0 10 | github.com/go-siris/siris v7.4.0+incompatible // indirect 11 | github.com/golang/protobuf v1.3.2 // indirect 12 | github.com/howeyc/fsnotify v0.9.0 // indirect 13 | github.com/jinzhu/gorm v1.9.11 14 | github.com/json-iterator/go v1.1.8 // indirect 15 | github.com/labstack/echo v3.3.10+incompatible // indirect 16 | github.com/labstack/gommon v0.3.0 // indirect 17 | github.com/mattn/go-colorable v0.1.4 // indirect 18 | github.com/mattn/go-isatty v0.0.10 // indirect 19 | github.com/oxequa/interact v0.0.0-20171114182912-f8fb5795b5d7 // indirect 20 | github.com/oxequa/realize v2.0.2+incompatible // indirect 21 | github.com/satori/go.uuid v1.2.0 // indirect 22 | github.com/silenceper/gowatch v0.0.0-20191122025114-d9a4b93d0ed4 // indirect 23 | github.com/silenceper/log v0.0.0-20171204144354-e5ac7fa8a76a // indirect 24 | github.com/sirupsen/logrus v1.4.2 25 | github.com/ugorji/go v1.1.7 // indirect 26 | github.com/valyala/fasttemplate v1.1.0 // indirect 27 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c 28 | golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4 // indirect 29 | gopkg.in/gin-gonic/gin.v1 v1.3.0 // indirect 30 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect 31 | gopkg.in/yaml.v2 v2.2.5 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 6 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 7 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 8 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 9 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 10 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= 15 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 16 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 17 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 18 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 19 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 20 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 21 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 22 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 23 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 24 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 25 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 26 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 27 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 28 | github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= 29 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 30 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 31 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 32 | github.com/go-siris/siris v7.4.0+incompatible h1:dZb+3EeuhRveTeeQ9sLXVbLMeadiQme32/JaCtZKrqo= 33 | github.com/go-siris/siris v7.4.0+incompatible/go.mod h1:bw/JZxpCF3U5eUlNOjsAzCFbIzRRly9Aa+jvvlO4UKI= 34 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 35 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 36 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 37 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 38 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 39 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 40 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 41 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 42 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 43 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 44 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 45 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 46 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 47 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 48 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 49 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 50 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 51 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 52 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 53 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 54 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 55 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 56 | github.com/howeyc/fsnotify v0.9.0 h1:0gtV5JmOKH4A8SsFxG2BczSeXWWPvcMT0euZt5gDAxY= 57 | github.com/howeyc/fsnotify v0.9.0/go.mod h1:41HzSPxBGeFRQKEEwgh49TRw/nKBsYZ2cF1OzPjSJsA= 58 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 59 | github.com/jinzhu/gorm v1.9.11 h1:gaHGvE+UnWGlbWG4Y3FUwY1EcZ5n6S9WtqBA/uySMLE= 60 | github.com/jinzhu/gorm v1.9.11/go.mod h1:bu/pK8szGZ2puuErfU0RwyeNdsf3e6nCX/noXaVxkfw= 61 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 62 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 63 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 64 | github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= 65 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 66 | github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= 67 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 68 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 69 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 70 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 71 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 72 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 73 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 74 | github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= 75 | github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= 76 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= 77 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 78 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= 79 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 80 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 81 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 82 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 83 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 84 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 85 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 86 | github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= 87 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 88 | github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 89 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 90 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 91 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 92 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 93 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 94 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 95 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 96 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 97 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 98 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 99 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 100 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 101 | github.com/oxequa/interact v0.0.0-20171114182912-f8fb5795b5d7 h1:VhMyYEArWL80OqmBloufn/ABe355btZ3Md+EFFrv+zE= 102 | github.com/oxequa/interact v0.0.0-20171114182912-f8fb5795b5d7/go.mod h1:lYzYp3DJ1SPLrp8ZX8ODprgEoxmNelVN+TKaWvum0cg= 103 | github.com/oxequa/realize v2.0.2+incompatible h1:R+Rg8R+gyuWP8oqvFpaJMzdcFF0vy15zjEAGAiuc8pQ= 104 | github.com/oxequa/realize v2.0.2+incompatible/go.mod h1:Bqw5jC78Eh70s7/rryEaVPwps/kYdPxBWTZDE+6x0/8= 105 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 106 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 107 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 108 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 109 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 110 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 111 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 112 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 113 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 114 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 115 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 116 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 117 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 118 | github.com/silenceper/gowatch v0.0.0-20191122025114-d9a4b93d0ed4 h1:n47u2PCscLO08HhdR8PqHpHwjL/uNscfNl8eFAdnT00= 119 | github.com/silenceper/gowatch v0.0.0-20191122025114-d9a4b93d0ed4/go.mod h1:oWdkMQHrP7svJjD5aV4/dKePJ2vkLG0F1diRWxjVbHM= 120 | github.com/silenceper/log v0.0.0-20171204144354-e5ac7fa8a76a h1:COf2KvPmardI1M8p2fhHsXlFS2EXSQygbGgcDYBI9Wc= 121 | github.com/silenceper/log v0.0.0-20171204144354-e5ac7fa8a76a/go.mod h1:nyN/YUSK3CgJjtNzm6dVTkcou+RYXNMP+XLSlzQu0m0= 122 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 123 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 124 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 125 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 126 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 127 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 128 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 129 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 130 | github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= 131 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 132 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 133 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 134 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 135 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 136 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 137 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 138 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 139 | github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= 140 | github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 141 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 142 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 143 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 144 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 145 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= 146 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 147 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 148 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 149 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 150 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 151 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 152 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 153 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 154 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 155 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 156 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 157 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 158 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 159 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= 160 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 161 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 162 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 163 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 164 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 165 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 166 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 167 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 168 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 169 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 170 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 171 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 172 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 173 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 174 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU= 177 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 178 | golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4 h1:Hynbrlo6LbYI3H1IqXpkVDOcX/3HiPdhVEuyj5a59RM= 179 | golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 180 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 181 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 182 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 183 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 184 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 185 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 186 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 187 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 188 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 189 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 190 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 191 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 192 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 193 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 194 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 195 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 196 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 197 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 198 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 199 | gopkg.in/gin-gonic/gin.v1 v1.3.0 h1:DjAu49rN1YttQsOkVCPlAO3INcZNFT0IKsNVMk5MRT4= 200 | gopkg.in/gin-gonic/gin.v1 v1.3.0/go.mod h1:Eljh74A/zAvUOQ835v6ySeZ+5gQG6tKjbZTaZ9iWU3A= 201 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 202 | gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= 203 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 204 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 205 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= 206 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 207 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 208 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 209 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 210 | gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= 211 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 212 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 213 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 214 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 215 | -------------------------------------------------------------------------------- /handlers/host.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "strconv" 5 | "cronnest/models" 6 | "net/http" 7 | "github.com/gin-gonic/gin" 8 | "fmt" 9 | db "cronnest/database" 10 | lg "cronnest/logger" 11 | "time" 12 | "encoding/json" 13 | ) 14 | 15 | type hostReqData struct { 16 | Address string `json:"address"` 17 | } 18 | 19 | 20 | func GetHosts(c *gin.Context) { 21 | search := c.DefaultQuery("search", "") 22 | page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) 23 | pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) 24 | limit := pageSize 25 | offset := (page - 1) * pageSize 26 | 27 | var hosts []models.Host 28 | var count int 29 | if search != "" { 30 | search = fmt.Sprintf("%%%v%%", search) 31 | db.DB.Table("host").Where("address LIKE ?", search).Count(&count).Limit(limit).Offset(offset). 32 | Order("address").Find(&hosts) 33 | } else { 34 | db.DB.Table("host").Count(&count).Limit(limit).Offset(offset).Order("address").Find(&hosts) 35 | } 36 | 37 | var data []map[string]interface{} 38 | data = []map[string]interface{} {} 39 | for _, host := range hosts { 40 | data = append(data, MakeHostKv(host)) 41 | } 42 | 43 | c.JSON(http.StatusOK, gin.H{"data": data, "total": count}) 44 | } 45 | 46 | 47 | func UpdateHost(c *gin.Context) { 48 | user, _, _ := c.Request.BasicAuth() 49 | hId := c.Param("hId") 50 | var hostMdl models.Host 51 | db.DB.Table("host").Where("id=?", hId).First(&hostMdl) 52 | if hostMdl.Id == 0 { 53 | c.JSON(http.StatusNotFound, gin.H{"msg": "主机不存在"}) 54 | } 55 | 56 | var reqData hostReqData 57 | if err := c.ShouldBindJSON(&reqData); err != nil { 58 | c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()}) 59 | return 60 | } 61 | 62 | var addressCount int64 63 | db.DB.Table("host").Where( 64 | "address=? AND id!=?", reqData.Address, hId).Count(&addressCount) 65 | if addressCount > 0 { 66 | c.JSON(http.StatusBadRequest, gin.H{"msg": fmt.Sprintf("主机地址[%s]已存在", reqData.Address)}) 67 | return 68 | } 69 | 70 | transaction := db.DB.Begin() 71 | hostMdl.Address = reqData.Address 72 | hostMdl.UpdatedAt = time.Now() 73 | result := transaction.Table("host").Save(&hostMdl) 74 | if result.Error != nil { 75 | transaction.Rollback() 76 | msg := fmt.Sprintf("修改主机[%s]失败, %v", hostMdl.Address, result.Error) 77 | lg.Logger.Error(msg) 78 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 79 | return 80 | } 81 | 82 | hostData := MakeHostKv(hostMdl) 83 | 84 | jsonData, _ := json.Marshal(map[string]interface{}{"updated_host": hostData}) 85 | operRecord := models.OperationRecord{SourceType:"host", SourceId:hostMdl.Id, SourceLabel: hostMdl.Address, 86 | OperationType: "update", Data:jsonData, User: user, CreatedAt: time.Now()} 87 | if result = transaction.Table("operation_record").Create(&operRecord); result.Error != nil { 88 | transaction.Rollback() 89 | msg := fmt.Sprintf("记录操作失败, %v", result.Error) 90 | lg.Logger.Error(msg) 91 | c.JSON(http.StatusInternalServerError, 92 | gin.H{"code": 500, "msg": msg}) 93 | return 94 | } 95 | transaction.Commit() 96 | 97 | c.JSON(http.StatusOK, gin.H{"data": hostData}) 98 | } 99 | 100 | 101 | func DeleteHost(c *gin.Context) { 102 | user, _, _ := c.Request.BasicAuth() 103 | 104 | hId := c.Param("hId") 105 | var hostMdl models.Host 106 | db.DB.Table("host").Where("id=?", hId).First(&hostMdl) 107 | if hostMdl.Id == 0 { 108 | c.JSON(http.StatusNotFound, gin.H{"msg": "主机不存在"}) 109 | } 110 | 111 | transaction := db.DB.Begin() 112 | result := transaction.Table("hostgroup_host").Where("host_id=?", hId).Delete(models.HostgroupHost{}) 113 | if result.Error != nil { 114 | transaction.Rollback() 115 | msg := fmt.Sprintf("删除主机[%s]与组关系失败,%v", hostMdl.Address, result.Error) 116 | lg.Logger.Error(msg) 117 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 118 | return 119 | } 120 | result = transaction.Table("host_crontab").Where("host_id=?", hId).Delete(models.HostCrontab{}) 121 | if result.Error != nil { 122 | transaction.Rollback() 123 | msg := fmt.Sprintf("删除主机[%s]Crontab失败,%v", hostMdl.Address, result.Error) 124 | lg.Logger.Error(msg) 125 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 126 | return 127 | } 128 | 129 | result = transaction.Table("host").Where("id=?", hId).Delete(models.Host{}) 130 | if result.Error != nil { 131 | transaction.Rollback() 132 | msg :=fmt.Sprintf("删除主机[%s]失败,%v", hostMdl.Address, result.Error) 133 | lg.Logger.Error(msg) 134 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 135 | return 136 | } 137 | 138 | hostData := MakeHostKv(hostMdl) 139 | 140 | recordData, _ := json.Marshal(map[string]interface{}{"deleted_host": hostData}) 141 | operRecord := models.OperationRecord{SourceType:"host", SourceId:hostMdl.Id, SourceLabel: hostMdl.Address, 142 | OperationType: "delete", Data:recordData, User: user, CreatedAt: time.Now()} 143 | if result = transaction.Table("operation_record").Create(&operRecord); result.Error != nil { 144 | transaction.Rollback() 145 | msg := fmt.Sprintf("记录操作失败, %v", result.Error) 146 | lg.Logger.Error(msg) 147 | c.JSON(http.StatusInternalServerError, 148 | gin.H{"code": 500, "msg": msg}) 149 | return 150 | } 151 | transaction.Commit() 152 | 153 | c.JSON(http.StatusOK, gin.H{"data": hostData}) 154 | } -------------------------------------------------------------------------------- /handlers/hostCrontab.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "cronnest/models" 5 | "net/http" 6 | "github.com/gin-gonic/gin" 7 | "fmt" 8 | db "cronnest/database" 9 | ) 10 | 11 | 12 | func GetHostCrontab(c *gin.Context) { 13 | hId := c.Param("hId") 14 | data := map[string]interface{} {} 15 | host := models.Host{} 16 | db.DB.Table("host").Where(fmt.Sprintf("id=%v", hId)).Find(&host) 17 | if host.Id > 0 { 18 | data = UpdateHostCrontabRecord(host) 19 | } 20 | 21 | c.JSON(http.StatusOK, gin.H{"data": data}) 22 | } -------------------------------------------------------------------------------- /handlers/hostCrontabJob.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "github.com/gin-gonic/gin" 6 | "cronnest/models" 7 | db "cronnest/database" 8 | lg "cronnest/logger" 9 | "fmt" 10 | "encoding/base64" 11 | "cronnest/configure" 12 | "golang.org/x/crypto/ssh" 13 | "encoding/json" 14 | "time" 15 | ) 16 | 17 | func CreateHostCrontabJob(c *gin.Context) { 18 | user, _, _ := c.Request.BasicAuth() 19 | 20 | hId := c.Param("hId") 21 | hostMdl := models.Host{} 22 | db.DB.Table("host").Where("id = ?", hId).First(&hostMdl) 23 | if hostMdl.Id == 0{ 24 | c.JSON(http.StatusNotFound, gin.H{"msg": "Host not found"}) 25 | return 26 | } 27 | 28 | rawData, _ := c.GetRawData() 29 | byteJob := []byte(rawData) 30 | base64Job := base64.StdEncoding.EncodeToString(byteJob) 31 | 32 | cmd := fmt.Sprintf("echo %s | base64 -d > /tmp/cronnest_create_job && chmod +x /tmp/cronnest_create_job && /tmp/cronnest_create_job %s %s %s", 33 | CreateHostCronJobJobScriptBase64Content, configure.SystemCrontabFile, configure.UserCrontabDir, base64Job) 34 | 35 | addr := fmt.Sprintf("%s:%s", hostMdl.Address, configure.SSH["port"]) 36 | 37 | status, output := cmdStatusOutput(addr, cmd) 38 | if status == 400 { 39 | c.JSON(http.StatusBadRequest, gin.H{"msg": output}) 40 | } else if status == 500 { 41 | msg := fmt.Sprintf("SSH创建主机Crontab job失败, %s", output) 42 | lg.Logger.Error(msg) 43 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 44 | } else { 45 | created := map[string]interface{}{} 46 | json.Unmarshal([]byte(output), &created) 47 | 48 | recordData, _ := json.Marshal(map[string]interface{}{"created_job": created}) 49 | operRecord := models.OperationRecord{SourceType:"host_crontab_job", SourceId:hostMdl.Id, 50 | SourceLabel: hostMdl.Address, OperationType: "create", Data:recordData, User: user, CreatedAt: time.Now()} 51 | if result := db.DB.Table("operation_record").Create(&operRecord); result.Error != nil { 52 | msg := fmt.Sprintf("记录操作失败, %v", result.Error) 53 | lg.Logger.Error(msg) 54 | c.JSON(http.StatusInternalServerError, 55 | gin.H{"code": 500, "msg": msg}) 56 | return 57 | } 58 | 59 | crontab := UpdateHostCrontabRecord(hostMdl) 60 | c.JSON(http.StatusOK, gin.H{"data": gin.H{"created": created, "tab": crontab["tab"]}}) 61 | } 62 | } 63 | 64 | func UpdateHostCrontabJob(c *gin.Context) { 65 | user, _, _ := c.Request.BasicAuth() 66 | hId := c.Param("hId") 67 | hostMdl := models.Host{} 68 | db.DB.Table("host").Where("id = ?", hId).First(&hostMdl) 69 | if hostMdl.Id == 0{ 70 | c.JSON(http.StatusNotFound, gin.H{"msg": "Host not found"}) 71 | return 72 | } 73 | rawData, _ := c.GetRawData() 74 | byteJob := []byte(rawData) 75 | base64Job := base64.StdEncoding.EncodeToString(byteJob) 76 | 77 | cmd := fmt.Sprintf("echo %s | base64 -d > /tmp/cronnest_update_job && chmod +x /tmp/cronnest_update_job && /tmp/cronnest_update_job %s %s %s", 78 | UpdateHostCronJobJobScriptBase64Content, configure.SystemCrontabFile, configure.UserCrontabDir, base64Job) 79 | 80 | addr := fmt.Sprintf("%s:%s", hostMdl.Address, configure.SSH["port"]) 81 | 82 | status, output := cmdStatusOutput(addr, cmd) 83 | 84 | if status == 400 { 85 | c.JSON(http.StatusBadRequest, gin.H{"msg": output}) 86 | } else if status == 500 { 87 | c.JSON(http.StatusInternalServerError, gin.H{ "msg": output}) 88 | } else { 89 | updated := map[string]interface{}{} 90 | json.Unmarshal([]byte(output), &updated) 91 | 92 | recordData, _ := json.Marshal(map[string]interface{}{"updated_job": updated}) 93 | operRecord := models.OperationRecord{SourceType:"host_crontab_job", SourceId:hostMdl.Id, 94 | SourceLabel: hostMdl.Address, OperationType: "update", Data:recordData, User: user, CreatedAt: time.Now()} 95 | if result := db.DB.Table("operation_record").Create(&operRecord); result.Error != nil { 96 | msg := fmt.Sprintf("记录操作失败, %v", result.Error) 97 | lg.Logger.Error(msg) 98 | c.JSON(http.StatusInternalServerError, 99 | gin.H{"code": 500, "msg": msg}) 100 | return 101 | } 102 | 103 | crontab := UpdateHostCrontabRecord(hostMdl) 104 | c.JSON(http.StatusOK, gin.H{"data": gin.H{"updated": updated, "tab": crontab["tab"]}}) 105 | } 106 | } 107 | 108 | 109 | func DeleteHostCrontabJob(c *gin.Context) { 110 | user, _, _ := c.Request.BasicAuth() 111 | hId := c.Param("hId") 112 | hostMdl := models.Host{} 113 | db.DB.Table("host").Where("id = ?", hId).First(&hostMdl) 114 | if hostMdl.Id == 0{ 115 | c.JSON(http.StatusNotFound, gin.H{"msg": "Host not found"}) 116 | return 117 | } 118 | rawData, _ := c.GetRawData() 119 | byteJob := []byte(rawData) 120 | base64Job := base64.StdEncoding.EncodeToString(byteJob) 121 | 122 | cmd := fmt.Sprintf("echo %s | base64 -d > /tmp/cronnest_delete_job && " + 123 | "chmod +x /tmp/cronnest_delete_job && /tmp/cronnest_delete_job %s %s %s", 124 | DeleteHostCronJobJobScriptBase64Content, configure.SystemCrontabFile, configure.UserCrontabDir, base64Job) 125 | 126 | addr := fmt.Sprintf("%s:%s", hostMdl.Address, configure.SSH["port"]) 127 | 128 | status, output := cmdStatusOutput(addr, cmd) 129 | 130 | if status == 400 { 131 | c.JSON(http.StatusBadRequest, gin.H{"msg": output}) 132 | } else if status == 500 { 133 | c.JSON(http.StatusInternalServerError, gin.H{"msg": output}) 134 | } else { 135 | removed := map[string]interface{}{} 136 | json.Unmarshal([]byte(output), &removed) 137 | fmt.Println(output) 138 | recordData, _ := json.Marshal(map[string]interface{}{"deleted_job": removed}) 139 | operRecord := models.OperationRecord{SourceType:"host_crontab_job", SourceId:hostMdl.Id, 140 | SourceLabel: hostMdl.Address, OperationType: "delete", 141 | Data:recordData, User: user, CreatedAt: time.Now()} 142 | if result := db.DB.Table("operation_record").Create(&operRecord); result.Error != nil { 143 | msg := fmt.Sprintf("记录操作失败, %v", result.Error) 144 | lg.Logger.Error(msg) 145 | c.JSON(http.StatusInternalServerError, 146 | gin.H{"code": 500, "msg": msg}) 147 | return 148 | } 149 | 150 | crontab := UpdateHostCrontabRecord(hostMdl) 151 | c.JSON(http.StatusOK, gin.H{"data": gin.H{"removed": removed, "tab": crontab["tab"]}}) 152 | } 153 | } 154 | 155 | 156 | func cmdStatusOutput(addr string, cmd string) (int64, string){ 157 | sshClient, err := ssh.Dial("tcp", addr, &SshCltConfig) 158 | if err != nil { 159 | errMsg := fmt.Sprintf("SSH连接%v失败, %v", addr, err) 160 | return 500, errMsg 161 | } 162 | defer sshClient.Close() 163 | 164 | sshSession, err := sshClient.NewSession() 165 | if err != nil { 166 | errMsg := fmt.Sprintf("SSH连接创建Session失败, %v", err) 167 | return 500, errMsg 168 | } 169 | defer sshSession.Close() 170 | 171 | output, err := sshSession.CombinedOutput(cmd) 172 | if err != nil { 173 | if err.Error() == "Process exited with status 40" { 174 | return 400, string(output) 175 | } else { 176 | return 500, string(output) 177 | } 178 | } 179 | return 200, string(output) 180 | } -------------------------------------------------------------------------------- /handlers/hostgroup.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "strconv" 5 | "cronnest/models" 6 | "net/http" 7 | "github.com/gin-gonic/gin" 8 | "fmt" 9 | db "cronnest/database" 10 | lg "cronnest/logger" 11 | "time" 12 | "encoding/json" 13 | ) 14 | 15 | type reqData struct { 16 | Name string `json:"name"` 17 | } 18 | 19 | func GetHostgroup(c *gin.Context) { 20 | search := c.DefaultQuery("search", "") 21 | page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) 22 | pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) 23 | limit := pageSize 24 | offset := (page - 1) * pageSize 25 | 26 | var hostgroupMdls []models.Hostgroup 27 | var count int 28 | if search != "" { 29 | search = fmt.Sprintf("%%%v%%", search) 30 | db.DB.Table("hostgroup").Where("name LIKE ?", search).Count(&count).Limit(limit).Offset(offset). 31 | Order("name").Find(&hostgroupMdls) 32 | } else { 33 | db.DB.Table("hostgroup").Count(&count).Limit(limit).Offset(offset).Order( 34 | "name").Find(&hostgroupMdls) 35 | } 36 | 37 | data := []map[string]interface{}{} 38 | for _, hg := range hostgroupMdls { 39 | data = append(data, MakeHostgroupKv(hg)) 40 | } 41 | 42 | c.JSON(http.StatusOK, gin.H{"data": data, "total": count}) 43 | } 44 | 45 | func CreateHostgroup(c *gin.Context) { 46 | user, _, _ := c.Request.BasicAuth() 47 | 48 | var reqData reqData 49 | if err := c.ShouldBindJSON(&reqData); err != nil { 50 | c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()}) 51 | return 52 | } 53 | 54 | var hostgroupMdl models.Hostgroup 55 | db.DB.Table("hostgroup").Where("name=?", reqData.Name).Find(&hostgroupMdl) 56 | if hostgroupMdl.Id > 0 { 57 | c.JSON(http.StatusBadRequest, gin.H{"msg": "主机组已存在"}) 58 | return 59 | } 60 | 61 | transaction := db.DB.Begin() 62 | 63 | hostgroupMdl = models.Hostgroup{Name: reqData.Name, CreatedAt: time.Now(), UpdatedAt: time.Now()} 64 | result := transaction.Table("hostgroup").Create(&hostgroupMdl) 65 | if result.Error != nil { 66 | transaction.Rollback() 67 | msg := fmt.Sprintf("创建主机组[%s]失败, %v", hostgroupMdl.Name, result.Error) 68 | lg.Logger.Error(msg) 69 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 70 | return 71 | } 72 | 73 | data := MakeHostgroupKv(hostgroupMdl) 74 | 75 | recordData, _ := json.Marshal(map[string]interface{}{"created_hostgroup": data}) 76 | operRecord := models.OperationRecord{SourceType: "hostgroup", SourceId: hostgroupMdl.Id, 77 | SourceLabel: hostgroupMdl.Name, OperationType: "create", Data: recordData, User: user, CreatedAt: time.Now()} 78 | if result = transaction.Table("operation_record").Create(&operRecord); result.Error != nil { 79 | transaction.Rollback() 80 | msg := fmt.Sprintf("记录操作失败, %v", result.Error) 81 | lg.Logger.Error(msg) 82 | c.JSON(http.StatusInternalServerError, 83 | gin.H{"code": 500, "msg": msg}) 84 | return 85 | } 86 | transaction.Commit() 87 | c.JSON(http.StatusCreated, gin.H{"data": data}) 88 | } 89 | 90 | func UpdateHostgroup(c *gin.Context) { 91 | user, _, _ := c.Request.BasicAuth() 92 | hgId := c.Param("hgId") 93 | var hostgroupMdl models.Hostgroup 94 | db.DB.Table("hostgroup").Where("id=?", hgId).Find(&hostgroupMdl) 95 | if hostgroupMdl.Id == 0 { 96 | c.JSON(http.StatusNotFound, gin.H{"msg": "主机组不存在"}) 97 | return 98 | } 99 | 100 | var reqData reqData 101 | if err := c.ShouldBindJSON(&reqData); err != nil { 102 | c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()}) 103 | return 104 | } 105 | 106 | var hgNameCount int64 107 | db.DB.Table("hostgroup").Where( 108 | "name=? AND id!=?", reqData.Name, hgId).Count(&hgNameCount) 109 | if hgNameCount > 0 { 110 | c.JSON(http.StatusBadRequest, gin.H{"msg": fmt.Sprintf("主机组名称[%s]已存在", reqData.Name)}) 111 | return 112 | } 113 | 114 | transaction := db.DB.Begin() 115 | hostgroupMdl.Name = reqData.Name 116 | hostgroupMdl.UpdatedAt = time.Now() 117 | result := transaction.Table("hostgroup").Save(&hostgroupMdl) 118 | if result.Error != nil { 119 | transaction.Rollback() 120 | msg := fmt.Sprintf("修改主机组[%s]失败, %v", hostgroupMdl.Name, result.Error) 121 | lg.Logger.Error(msg) 122 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 123 | return 124 | } 125 | 126 | data := MakeHostgroupKv(hostgroupMdl) 127 | 128 | recordData, _ := json.Marshal(map[string]interface{}{"updated_hostgroup": data}) 129 | operRecord := models.OperationRecord{SourceType: "hostgroup", SourceId: hostgroupMdl.Id, 130 | SourceLabel: hostgroupMdl.Name, OperationType: "update", Data: recordData, User: user, CreatedAt: time.Now()} 131 | if result := transaction.Table("operation_record").Create(&operRecord); result.Error != nil { 132 | transaction.Rollback() 133 | msg := fmt.Sprintf("记录操作失败, %v", result.Error) 134 | lg.Logger.Error(msg) 135 | c.JSON(http.StatusInternalServerError, 136 | gin.H{"code": 500, "msg": msg}) 137 | return 138 | } 139 | transaction.Commit() 140 | c.JSON(http.StatusOK, gin.H{"data": data}) 141 | } 142 | 143 | func DeleteHostgroup(c *gin.Context) { 144 | user, _, _ := c.Request.BasicAuth() 145 | hgId := c.Param("hgId") 146 | var hostgroupMdl models.Hostgroup 147 | db.DB.Table("hostgroup").Where("id=?", hgId).Find(&hostgroupMdl) 148 | if hostgroupMdl.Id == 0 { 149 | c.JSON(http.StatusNotFound, gin.H{"msg": "主机组不存在"}) 150 | return 151 | } 152 | 153 | transaction := db.DB.Begin() 154 | var hostMdls []models.Host 155 | var removedHostMdls []models.Host 156 | var deletedHostMdls []models.Host 157 | transaction.Table("host").Joins( 158 | "join hostgroup_host ON hostgroup_host.host_id=host.id AND hostgroup_host.hostgroup_id=?", 159 | hgId).Find(&hostMdls) 160 | for _, hostMdl := range hostMdls { 161 | var hostOtherHgCount int64 162 | transaction.Table("hostgroup").Joins( 163 | "join hostgroup_host ON hostgroup_host.host_id=? AND "+ 164 | "hostgroup_host.hostgroup_id=hostgroup.id AND hostgroup_host.hostgroup_id!=?", 165 | hostMdl.Id, hgId).Count(&hostOtherHgCount) 166 | 167 | if hostOtherHgCount > 0 { 168 | removedHostMdls = append(removedHostMdls, hostMdl) 169 | continue 170 | } 171 | deletedHostMdls = append(deletedHostMdls, hostMdl) 172 | 173 | result := transaction.Table("host_crontab").Where( 174 | "host_id=?", hostMdl.Id).Delete(models.HostCrontab{}) 175 | if result.Error != nil { 176 | transaction.Rollback() 177 | msg := fmt.Sprintf( 178 | "删除主机组[%s]的主机[%s]Crontab失败, %v", hostgroupMdl.Name, hostMdl.Address, result.Error) 179 | lg.Logger.Error(msg) 180 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 181 | return 182 | } 183 | 184 | result = transaction.Table("host").Where("id=?", hostMdl.Id).Delete(models.Host{}) 185 | if result.Error != nil { 186 | transaction.Rollback() 187 | msg := fmt.Sprintf( 188 | "删除主机组[%s]的主机[%s]失败, %v", hostgroupMdl.Name, hostMdl.Address, result.Error) 189 | lg.Logger.Error(msg) 190 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 191 | return 192 | } 193 | } 194 | 195 | result := transaction.Table("hostgroup").Delete(&hostgroupMdl) 196 | if result.Error != nil { 197 | transaction.Rollback() 198 | msg := fmt.Sprintf("删除主机组[%s]失败, %v", hostgroupMdl.Name, result.Error) 199 | lg.Logger.Error(msg) 200 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 201 | return 202 | } 203 | 204 | result = transaction.Table("hostgroup_host").Where( 205 | "hostgroup_id=?", hostgroupMdl.Id).Delete(&models.HostgroupHost{}) 206 | if result.Error != nil { 207 | transaction.Rollback() 208 | msg := fmt.Sprintf( 209 | "删除主机组[%s]与主机关系失败, %v", hostgroupMdl.Name, result.Error) 210 | lg.Logger.Error(msg) 211 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 212 | return 213 | } 214 | 215 | removedHostData := []map[string]interface{}{} 216 | for _, hMdl := range removedHostMdls { 217 | removedHostData = append(removedHostData, MakeHostKv(hMdl)) 218 | } 219 | 220 | deletedHostData := []map[string]interface{}{} 221 | for _, hMdl := range deletedHostMdls { 222 | deletedHostData = append(deletedHostData, MakeHostKv(hMdl)) 223 | } 224 | 225 | hgData := MakeHostgroupKv(hostgroupMdl) 226 | 227 | recordData, _ := json.Marshal(map[string]interface{}{ 228 | "deleted_hostgroup": hgData, "removed_hosts": removedHostData, "deleted_hosts": deletedHostData}) 229 | operRecord := models.OperationRecord{SourceType: "hostgroup", SourceId: hostgroupMdl.Id, 230 | SourceLabel: hostgroupMdl.Name, OperationType: "delete", Data: recordData, User: user, CreatedAt: time.Now()} 231 | if result := transaction.Table("operation_record").Create(&operRecord); result.Error != nil { 232 | transaction.Rollback() 233 | msg := fmt.Sprintf("记录操作失败, %v", result.Error) 234 | lg.Logger.Error(msg) 235 | c.JSON(http.StatusInternalServerError, 236 | gin.H{"code": 500, "msg": msg}) 237 | return 238 | } 239 | 240 | transaction.Commit() 241 | 242 | c.JSON(http.StatusOK, gin.H{"data": hgData}) 243 | } 244 | -------------------------------------------------------------------------------- /handlers/hostgroupHost.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "strconv" 5 | "cronnest/models" 6 | "net/http" 7 | "github.com/gin-gonic/gin" 8 | "fmt" 9 | db "cronnest/database" 10 | lg "cronnest/logger" 11 | "time" 12 | "encoding/json" 13 | ) 14 | 15 | type hostgroupHostReqData struct { 16 | Hosts []string `json:"hosts"` 17 | } 18 | 19 | func GetHostgroupHosts(c *gin.Context) { 20 | hgId := c.Param("hgId") 21 | search := c.DefaultQuery("search", "") 22 | page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) 23 | pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) 24 | limit := pageSize 25 | offset := (page - 1) * pageSize 26 | 27 | hosts := []models.Host{} 28 | count := 0 29 | q := db.DB.Table("host").Joins( 30 | "JOIN hostgroup_host ON hostgroup_host.hostgroup_id=? AND host.id=hostgroup_host.host_id", hgId) 31 | if search != "" { 32 | search = fmt.Sprintf("%%%v%%", search) 33 | q.Where("address LIKE ?", search).Count(&count).Limit(limit). 34 | Offset(offset).Order("address").Find(&hosts) 35 | } else { 36 | q.Count(&count).Limit(limit).Offset(offset).Order("address").Find(&hosts) 37 | } 38 | 39 | data := []map[string]interface{}{} 40 | for _, h := range hosts { 41 | data = append(data, MakeHostKv(h)) 42 | } 43 | 44 | c.JSON(http.StatusOK, gin.H{"data": data, "total": count}) 45 | } 46 | 47 | func AddHostgroupHosts(c *gin.Context) { 48 | user, _, _ := c.Request.BasicAuth() 49 | hgId := c.Param("hgId") 50 | var hostgroupMdl models.Hostgroup 51 | db.DB.Table("hostgroup").Where("id=?", hgId).Find(&hostgroupMdl) 52 | if hostgroupMdl.Id == 0 { 53 | c.JSON(http.StatusNotFound, gin.H{"msg": "主机组不存在"}) 54 | return 55 | } 56 | 57 | var reqData hostgroupHostReqData 58 | if err := c.ShouldBindJSON(&reqData); err != nil { 59 | c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()}) 60 | return 61 | } 62 | 63 | transaction := db.DB.Begin() 64 | 65 | var addedHostMdls []models.Host 66 | var createdHostMdls []models.Host 67 | for _, host := range reqData.Hosts { 68 | if host != "" { 69 | var hostMdl models.Host 70 | transaction.Table("host").Where("address=?", host).First(&hostMdl) 71 | if hostMdl.Id == 0 { 72 | hostMdl.Address = host 73 | hostMdl.CreatedAt = time.Now() 74 | hostMdl.UpdatedAt = time.Now() 75 | hostMdl.Status = "enabled" 76 | result := transaction.Table("host").Save(&hostMdl) 77 | if result.Error != nil { 78 | transaction.Rollback() 79 | msg := fmt.Sprintf("创建主机[%s]失败, %v", hostgroupMdl.Name, host, result.Error) 80 | lg.Logger.Error(msg) 81 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 82 | return 83 | } 84 | createdHostMdls = append(createdHostMdls, hostMdl) 85 | } 86 | addedHostMdls = append(addedHostMdls, hostMdl) 87 | 88 | var hostgroupHostMdl models.HostgroupHost 89 | transaction.Table("hostgroup_host").Where( 90 | "hostgroup_id=? AND host_id=?", hgId, hostMdl.Id).Find(&hostgroupHostMdl) 91 | if hostgroupHostMdl.HostgroupId == 0 { 92 | hostgroupHostMdl.HostgroupId = hostgroupMdl.Id 93 | hostgroupHostMdl.HostId = hostMdl.Id 94 | result := transaction.Table("hostgroup_host").Save(&hostgroupHostMdl) 95 | if result.Error != nil { 96 | transaction.Rollback() 97 | msg := fmt.Sprintf("创建主机组[%s]主机[%s]关系失败, host, %v", hostgroupMdl.Name, result.Error) 98 | lg.Logger.Error(msg) 99 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 100 | return 101 | } 102 | } 103 | } 104 | } 105 | 106 | addedHostData := []map[string]interface{}{} 107 | createdHostData := []map[string]interface{}{} 108 | for _, h := range addedHostMdls { 109 | addedHostData = append(addedHostData, MakeHostKv(h)) 110 | } 111 | for _, h := range createdHostMdls { 112 | createdHostData = append(createdHostData, MakeHostKv(h)) 113 | } 114 | 115 | recordData, _ := json.Marshal(map[string]interface{}{ 116 | "add_hosts": addedHostData, "createdHostData": createdHostData}) 117 | operRecord := models.OperationRecord{SourceType: "hostgroup_host", SourceId: hostgroupMdl.Id, 118 | SourceLabel: hostgroupMdl.Name, OperationType: "add", Data: recordData, 119 | User: user, CreatedAt: time.Now()} 120 | if result := transaction.Table("operation_record").Create(&operRecord); result.Error != nil { 121 | transaction.Rollback() 122 | msg := fmt.Sprintf("记录操作失败, %v", result.Error) 123 | lg.Logger.Error(msg) 124 | c.JSON(http.StatusInternalServerError, 125 | gin.H{"code": 500, "msg": msg}) 126 | return 127 | } 128 | 129 | transaction.Commit() 130 | c.JSON(http.StatusOK, gin.H{"data": addedHostData}) 131 | } 132 | 133 | 134 | func RemoveHostgroupHosts(c *gin.Context) { 135 | user, _, _ := c.Request.BasicAuth() 136 | hgId := c.Param("hgId") 137 | var hostgroupMdl models.Hostgroup 138 | db.DB.Table("hostgroup").Where("id=?", hgId).Find(&hostgroupMdl) 139 | if hostgroupMdl.Id == 0 { 140 | c.JSON(http.StatusNotFound, gin.H{"msg": "主机组不存在"}) 141 | return 142 | } 143 | 144 | var reqData hostgroupHostReqData 145 | if err := c.ShouldBindJSON(&reqData); err != nil { 146 | c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()}) 147 | return 148 | } 149 | 150 | 151 | var removedHostMdls []models.Host 152 | var deletedHostMdls []models.Host 153 | transaction := db.DB.Begin() 154 | for _, host := range reqData.Hosts { 155 | var hostMdl models.Host 156 | transaction.Table("host").Where("address=?", host).Joins( 157 | "join hostgroup_host ON hostgroup_host.hostgroup_id=? AND hostgroup_host.host_id=host.id", 158 | hgId).First(&hostMdl) 159 | if hostMdl.Id == 0 { 160 | transaction.Rollback() 161 | c.JSON(http.StatusBadRequest, gin.H{"msg": fmt.Sprintf("主机[%s]不存在或者不属于该组", host)}) 162 | return 163 | } 164 | 165 | removedHostMdls = append(removedHostMdls, hostMdl) 166 | 167 | result := transaction.Table("hostgroup_host").Where( 168 | "hostgroup_id=? AND host_id=?", hgId, hostMdl.Id).Delete(models.HostgroupHost{}) 169 | if result.Error != nil { 170 | transaction.Rollback() 171 | c.JSON(http.StatusBadRequest, gin.H{"msg": fmt.Sprintf( 172 | "移除主机组与主机[%s]关系失败,%v", host, result.Error)}) 173 | return 174 | } 175 | 176 | var hostOtherHgCount int64 177 | transaction.Table("hostgroup").Joins( 178 | "join hostgroup_host ON hostgroup_host.hostgroup_id=hostgroup.id AND hostgroup_host.host_id=?", 179 | hostMdl.Id).Count(&hostOtherHgCount) 180 | 181 | if hostOtherHgCount > 0 { 182 | continue 183 | } 184 | deletedHostMdls = append(deletedHostMdls, hostMdl) 185 | 186 | result = transaction.Table("host_crontab").Where( 187 | "host_id=?", hostMdl.Id).Delete(models.HostCrontab{}) 188 | if result.Error != nil { 189 | transaction.Rollback() 190 | msg := fmt.Sprintf("删除主机组[%s]的主机[%s]Crontab失败, %v", hostgroupMdl.Name, host, result.Error) 191 | lg.Logger.Error(msg) 192 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 193 | return 194 | } 195 | 196 | result = transaction.Table("host").Where("id=?", hostMdl.Id).Delete(models.Host{}) 197 | if result.Error != nil { 198 | transaction.Rollback() 199 | msg := fmt.Sprintf("删除主机组[%s]的主机[%s]失败, %v", hostgroupMdl.Name, host, result.Error) 200 | lg.Logger.Error(msg) 201 | c.JSON(http.StatusInternalServerError, gin.H{"msg": msg}) 202 | return 203 | } 204 | } 205 | 206 | removedHostData := []map[string]interface{}{} 207 | deletedHostData := []map[string]interface{}{} 208 | for _, h := range removedHostMdls { 209 | removedHostData = append(removedHostData, MakeHostKv(h)) 210 | } 211 | for _, h := range deletedHostMdls { 212 | deletedHostData = append(deletedHostData, MakeHostKv(h)) 213 | } 214 | 215 | recordData, _ := json.Marshal(map[string]interface{}{ 216 | "removed_hosts": removedHostData, "deleted_hosts": deletedHostData}) 217 | operRecord := models.OperationRecord{SourceType: "hostgroup_host", SourceId: hostgroupMdl.Id, 218 | SourceLabel: hostgroupMdl.Name, OperationType: "remove", Data: recordData, 219 | User: user, CreatedAt: time.Now()} 220 | if result := transaction.Table("operation_record").Create(&operRecord); result.Error != nil { 221 | transaction.Rollback() 222 | msg := fmt.Sprintf("记录操作失败, %v", result.Error) 223 | lg.Logger.Error(msg) 224 | c.JSON(http.StatusInternalServerError, 225 | gin.H{"code": 500, "msg": msg}) 226 | return 227 | } 228 | 229 | transaction.Commit() 230 | c.JSON(http.StatusOK, gin.H{"data": removedHostData}) 231 | } -------------------------------------------------------------------------------- /handlers/html.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | func Html(c *gin.Context){ 9 | tpl := c.Param("tpl") 10 | user, _, _ := c.Request.BasicAuth() 11 | var html string 12 | if tpl == "hosts" { 13 | html = "host.html" 14 | } else if tpl == "operation_records" { 15 | html = "operation_record.html" 16 | } else if tpl == "crontab" { 17 | html = "crontab.html" 18 | } else { 19 | c.JSON(http.StatusOK, gin.H{"msg": "页面不存在!"}) 20 | } 21 | c.HTML(http.StatusOK, html, gin.H{"user": user}) 22 | } -------------------------------------------------------------------------------- /handlers/operationRecord.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "strconv" 5 | "github.com/gin-gonic/gin" 6 | "cronnest/models" 7 | "net/http" 8 | "fmt" 9 | db "cronnest/database" 10 | ) 11 | 12 | 13 | func GetOperationRecords(c *gin.Context) { 14 | search := c.DefaultQuery("search", "") 15 | page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) 16 | pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) 17 | limit := pageSize 18 | offset := (page - 1) * pageSize 19 | 20 | var operationRecords []models.OperationRecord 21 | var count int64 22 | if search != "" { 23 | search = fmt.Sprintf("%%%v%%", search) 24 | db.DB.Table("operation_record").Where("resource_label LIKE ?", search).Count(&count) 25 | db.DB.Table("operation_record").Where("resource_label LIKE ?", search). 26 | Limit(limit).Offset(offset).Order("-id").Find(&operationRecords) 27 | } else { 28 | db.DB.Table("operation_record").Count(&count) 29 | db.DB.Table("operation_record"). 30 | Limit(limit).Offset(offset).Order("-id").Find(&operationRecords) 31 | } 32 | 33 | data := []map[string]interface{}{} 34 | for _, record := range operationRecords { 35 | data = append(data, MakeOperationRecordKv(record)) 36 | } 37 | c.JSON(http.StatusOK, gin.H{"code": 200, "data": data, "total": count}) 38 | } 39 | -------------------------------------------------------------------------------- /handlers/utils.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "cronnest/models" 5 | "cronnest/configure" 6 | "golang.org/x/crypto/ssh" 7 | "time" 8 | "io/ioutil" 9 | "log" 10 | "encoding/base64" 11 | "fmt" 12 | "encoding/json" 13 | db "cronnest/database" 14 | 15 | ) 16 | 17 | var SshCltConfig ssh.ClientConfig 18 | var GetHostCrontabScriptBase64Content string 19 | var CreateHostCronJobJobScriptBase64Content string 20 | var UpdateHostCronJobJobScriptBase64Content string 21 | var DeleteHostCronJobJobScriptBase64Content string 22 | 23 | 24 | func MakeHostKv(host models.Host) map[string]interface{} { 25 | row := make(map[string]interface{}) 26 | row["id"] = host.Id 27 | row["address"] = host.Address 28 | row["status"] = host.Status 29 | row["created_at"] = host.CreatedAt.Format("2006-01-02 15:04:05") 30 | row["updated_at"] = host.UpdatedAt.Format("2006-01-02 15:04:05") 31 | return row 32 | } 33 | 34 | func MakeHostgroupKv(hg models.Hostgroup) map[string]interface{} { 35 | row := make(map[string]interface{}) 36 | row["id"] = hg.Id 37 | row["name"] = hg.Name 38 | row["created_at"] = hg.CreatedAt.Format("2006-01-02 15:04:05") 39 | row["updated_at"] = hg.UpdatedAt.Format("2006-01-02 15:04:05") 40 | return row 41 | } 42 | 43 | func MakeHostCrontabKv(hc models.HostCrontab) map[string]interface{} { 44 | row := make(map[string]interface{}) 45 | row["id"] = hc.Id 46 | row["host"] = hc.HostId 47 | row["status"] = hc.Status 48 | row["msg"] = hc.Msg 49 | row["tab"] = hc.Tab 50 | row["created_at"] = hc.CreatedAt.Format("2006-01-02 15:04:05") 51 | row["updated_at"] = hc.UpdatedAt.Format("2006-01-02 15:04:05") 52 | if hc.LastSucceed != nil { 53 | row["last_succeed"] = (*hc.LastSucceed).Format("2006-01-02 15:04:05") 54 | } else { 55 | row["last_succeed"] = nil 56 | } 57 | 58 | return row 59 | } 60 | 61 | func UpdateHostCrontabRecord(host models.Host) map[string]interface{} { 62 | crontabMdl := models.HostCrontab{} 63 | db.DB.Table("host_crontab").Where("host_id=?", host.Id).Find(&crontabMdl) 64 | 65 | cmd := fmt.Sprintf("echo %s | base64 -d > /tmp/get_host_crontab && chmod +x /tmp/get_host_crontab && /tmp/get_host_crontab %s %s", 66 | GetHostCrontabScriptBase64Content, configure.SystemCrontabFile, configure.UserCrontabDir) 67 | succ, output := Command(host.Address, configure.SSH["port"], cmd) 68 | currTime := time.Now() 69 | if succ == true { 70 | tab := map[string]interface{}{} 71 | json.Unmarshal([]byte(output), &tab) 72 | jsonData, _ := json.Marshal(tab) 73 | 74 | crontabMdl.Status = "successful" 75 | crontabMdl.Tab = jsonData 76 | crontabMdl.Msg = "done" 77 | crontabMdl.LastSucceed = &currTime 78 | } else { 79 | crontabMdl.Status = "failed" 80 | crontabMdl.Msg = output 81 | if crontabMdl.Id == 0 { 82 | tab := map[string]interface{}{} 83 | jsonData, _ := json.Marshal(tab) 84 | crontabMdl.Tab = jsonData 85 | } 86 | } 87 | crontabMdl.UpdatedAt = currTime 88 | 89 | if crontabMdl.Id > 0{ 90 | db.DB.Table("host_crontab").Save(&crontabMdl) 91 | } else { 92 | crontabMdl.HostId = host.Id 93 | crontabMdl.CreatedAt = currTime 94 | db.DB.Table("host_crontab").Create(&crontabMdl) 95 | } 96 | crontabData := MakeHostCrontabKv(crontabMdl) 97 | return crontabData 98 | } 99 | 100 | func MakeOperationRecordKv(record models.OperationRecord) map[string]interface{} { 101 | row := make(map[string]interface{}) 102 | row["id"] = record.Id 103 | row["resource_type"] = record.SourceType 104 | row["resource_id"] = record.SourceId 105 | row["resource_label"] = record.SourceLabel 106 | row["operation_type"] = record.OperationType 107 | row["data"] = record.Data 108 | row["user"] = record.User 109 | row["created_at"] = record.CreatedAt.Format("2006-01-02 15:04:05") 110 | return row 111 | } 112 | 113 | 114 | func Command(host string, port string, cmd string) (bool, string) { 115 | addr := fmt.Sprintf("%s:%s", host, port) 116 | 117 | sshClient, err := ssh.Dial("tcp", addr, &SshCltConfig) 118 | if err != nil { 119 | errMsg := fmt.Sprintf("SSH连接%s Client失败, %v", host, err) 120 | return false, errMsg 121 | } 122 | defer sshClient.Close() 123 | 124 | sshSession, err := sshClient.NewSession() 125 | if err != nil { 126 | errMsg := fmt.Sprintf("SSH连接创建Session失败, %v", err) 127 | return false, errMsg 128 | } 129 | defer sshSession.Close() 130 | 131 | output, err := sshSession.CombinedOutput(cmd) 132 | if err != nil { 133 | errMsg := fmt.Sprintf("执行出错, %v, \n%s", err, output) 134 | return false, errMsg 135 | } 136 | 137 | return true, string(output) 138 | } 139 | 140 | func makeScriptBase64Content(path string) string { 141 | b, err := ioutil.ReadFile(path) 142 | if err != nil { 143 | log.Fatalf("makeScriptBase64Content error: read file failed, %v", err) 144 | } 145 | content := string(b) 146 | byteContent := []byte(content) 147 | return base64.StdEncoding.EncodeToString(byteContent) 148 | } 149 | 150 | func makeSshAuthMethod(keyPath string) ssh.AuthMethod { 151 | key, err := ioutil.ReadFile(keyPath) 152 | if err != nil { 153 | log.Fatalf("makeSshAuthMethod error: read keyfile failed, %v", err) 154 | } 155 | signer, err := ssh.ParsePrivateKey(key) 156 | if err != nil { 157 | log.Fatalf("makeSshAuthMethod error: parse private key failed, %v", err) 158 | } 159 | return ssh.PublicKeys(signer) 160 | } 161 | 162 | 163 | func init() { 164 | sshConf := configure.SSH 165 | SshCltConfig = ssh.ClientConfig{} 166 | SshCltConfig.User = sshConf["user"] 167 | SshCltConfig.HostKeyCallback = ssh.InsecureIgnoreHostKey() 168 | if sshConf["privateKeyPath"] != "" { 169 | SshCltConfig.Auth = []ssh.AuthMethod{makeSshAuthMethod(sshConf["privateKeyPath"])} 170 | } else { 171 | SshCltConfig.Auth = []ssh.AuthMethod{ssh.Password(sshConf["password"])} 172 | } 173 | 174 | SshCltConfig.Timeout = time.Duration(5) * time.Second 175 | 176 | GetHostCrontabScriptBase64Content = makeScriptBase64Content("./scripts/get_host_crontab.py") 177 | CreateHostCronJobJobScriptBase64Content = makeScriptBase64Content("./scripts/create_host_crontab_job.py") 178 | UpdateHostCronJobJobScriptBase64Content = makeScriptBase64Content("./scripts/update_host_crontab_job.py") 179 | DeleteHostCronJobJobScriptBase64Content = makeScriptBase64Content("./scripts/delete_host_crontab_job.py") 180 | } -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "os" 6 | "cronnest/configure" 7 | ) 8 | 9 | var Logger = logrus.New() 10 | 11 | func init() { 12 | Logger.Out = os.Stdout 13 | file, err := os.OpenFile(configure.Log["cronnest"], os.O_CREATE|os.O_WRONLY, 0666) 14 | if err == nil { 15 | Logger.Out = file 16 | } 17 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | _ "github.com/jinzhu/gorm/dialects/postgres" 7 | "cronnest/router" 8 | db "cronnest/database" 9 | ) 10 | 11 | func main() { 12 | defer db.DB.Close() 13 | 14 | router := router.InitRouter() 15 | 16 | s := &http.Server{ 17 | Addr: ":9900", 18 | Handler: router, 19 | ReadTimeout: 20 * time.Second, 20 | WriteTimeout: 20 * time.Second, 21 | MaxHeaderBytes: 1 << 20, 22 | } 23 | s.ListenAndServe() 24 | } 25 | -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type Hostgroup struct { 9 | Id int64 10 | Name string 11 | CreatedAt time.Time 12 | UpdatedAt time.Time 13 | } 14 | 15 | type HostCrontab struct { 16 | Id int64 17 | HostId int64 18 | Status string 19 | Msg string 20 | Tab json.RawMessage `sql:"type:jsonb"` 21 | CreatedAt time.Time 22 | UpdatedAt time.Time 23 | LastSucceed *time.Time `gorm:"default:NULL"` 24 | } 25 | 26 | type Host struct { 27 | Id int64 28 | Address string 29 | Status string 30 | CreatedAt time.Time 31 | UpdatedAt time.Time 32 | } 33 | 34 | type HostgroupHost struct { 35 | HostgroupId int64 36 | HostId int64 37 | } 38 | 39 | type OperationRecord struct { 40 | Id int64 41 | SourceType string 42 | SourceId int64 43 | SourceLabel string 44 | OperationType string 45 | Data json.RawMessage `sql:"type:jsonb"` 46 | User string 47 | CreatedAt time.Time 48 | } -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "cronnest/handlers" 5 | "github.com/gin-gonic/gin" 6 | "cronnest/configure" 7 | "os" 8 | "io" 9 | ) 10 | 11 | 12 | func InitRouter() *gin.Engine { 13 | f, _ := os.Create(configure.Log["request"]) 14 | gin.DefaultWriter = io.MultiWriter(f) 15 | 16 | 17 | router := gin.Default() 18 | 19 | router.LoadHTMLGlob("templates/*") 20 | 21 | router.Static("/static", "./static") 22 | 23 | authorized := router.Group("/", gin.BasicAuth(configure.Accounts)) 24 | 25 | authorized.GET("/html/:tpl/", handlers.Html) 26 | 27 | apiGroup := authorized.Group("/api") 28 | { 29 | apiGroup.GET("/hostgroups/:hgId/hosts/", handlers.GetHostgroupHosts) 30 | apiGroup.POST("/hostgroups/:hgId/hosts/", handlers.AddHostgroupHosts) 31 | apiGroup.DELETE("/hostgroups/:hgId/hosts/", handlers.RemoveHostgroupHosts) 32 | 33 | apiGroup.GET("/hostgroups/", handlers.GetHostgroup) 34 | apiGroup.POST("/hostgroups/", handlers.CreateHostgroup) 35 | apiGroup.PUT("/hostgroups/:hgId/", handlers.UpdateHostgroup) 36 | apiGroup.DELETE("/hostgroups/:hgId/", handlers.DeleteHostgroup) 37 | 38 | 39 | apiGroup.GET("/hosts/:hId/crontab/", handlers.GetHostCrontab) 40 | apiGroup.POST("/hosts/:hId/crontab/job/", handlers.CreateHostCrontabJob) 41 | apiGroup.PUT("/hosts/:hId/crontab/job/", handlers.UpdateHostCrontabJob) 42 | apiGroup.DELETE("/hosts/:hId/crontab/job/", handlers.DeleteHostCrontabJob) 43 | 44 | apiGroup.GET("/hosts/", handlers.GetHosts) 45 | apiGroup.PUT("/hosts/:hId/", handlers.UpdateHost) 46 | apiGroup.DELETE("/hosts/:hId/", handlers.DeleteHost) 47 | 48 | 49 | apiGroup.GET("/operation_records/", handlers.GetOperationRecords) 50 | } 51 | 52 | return router 53 | } -------------------------------------------------------------------------------- /static/axios-0.18.0/axios.min.js: -------------------------------------------------------------------------------- 1 | /* axios v0.18.0 | (c) 2018 by Matt Zabriskie */ 2 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";function r(e){var t=new s(e),n=i(s.prototype.request,t);return o.extend(n,s.prototype,t),o.extend(n,t),n}var o=n(2),i=n(3),s=n(5),u=n(6),a=r(u);a.Axios=s,a.create=function(e){return r(o.merge(u,e))},a.Cancel=n(23),a.CancelToken=n(24),a.isCancel=n(20),a.all=function(e){return Promise.all(e)},a.spread=n(25),e.exports=a,e.exports.default=a},function(e,t,n){"use strict";function r(e){return"[object Array]"===R.call(e)}function o(e){return"[object ArrayBuffer]"===R.call(e)}function i(e){return"undefined"!=typeof FormData&&e instanceof FormData}function s(e){var t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer}function u(e){return"string"==typeof e}function a(e){return"number"==typeof e}function c(e){return"undefined"==typeof e}function f(e){return null!==e&&"object"==typeof e}function p(e){return"[object Date]"===R.call(e)}function d(e){return"[object File]"===R.call(e)}function l(e){return"[object Blob]"===R.call(e)}function h(e){return"[object Function]"===R.call(e)}function m(e){return f(e)&&h(e.pipe)}function y(e){return"undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams}function w(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function g(){return("undefined"==typeof navigator||"ReactNative"!==navigator.product)&&("undefined"!=typeof window&&"undefined"!=typeof document)}function v(e,t){if(null!==e&&"undefined"!=typeof e)if("object"!=typeof e&&(e=[e]),r(e))for(var n=0,o=e.length;n 6 | * @license MIT 7 | */ 8 | e.exports=function(e){return null!=e&&(n(e)||r(e)||!!e._isBuffer)}},function(e,t,n){"use strict";function r(e){this.defaults=e,this.interceptors={request:new s,response:new s}}var o=n(6),i=n(2),s=n(17),u=n(18);r.prototype.request=function(e){"string"==typeof e&&(e=i.merge({url:arguments[0]},arguments[1])),e=i.merge(o,{method:"get"},this.defaults,e),e.method=e.method.toLowerCase();var t=[u,void 0],n=Promise.resolve(e);for(this.interceptors.request.forEach(function(e){t.unshift(e.fulfilled,e.rejected)}),this.interceptors.response.forEach(function(e){t.push(e.fulfilled,e.rejected)});t.length;)n=n.then(t.shift(),t.shift());return n},i.forEach(["delete","get","head","options"],function(e){r.prototype[e]=function(t,n){return this.request(i.merge(n||{},{method:e,url:t}))}}),i.forEach(["post","put","patch"],function(e){r.prototype[e]=function(t,n,r){return this.request(i.merge(r||{},{method:e,url:t,data:n}))}}),e.exports=r},function(e,t,n){"use strict";function r(e,t){!i.isUndefined(e)&&i.isUndefined(e["Content-Type"])&&(e["Content-Type"]=t)}function o(){var e;return"undefined"!=typeof XMLHttpRequest?e=n(8):"undefined"!=typeof process&&(e=n(8)),e}var i=n(2),s=n(7),u={"Content-Type":"application/x-www-form-urlencoded"},a={adapter:o(),transformRequest:[function(e,t){return s(t,"Content-Type"),i.isFormData(e)||i.isArrayBuffer(e)||i.isBuffer(e)||i.isStream(e)||i.isFile(e)||i.isBlob(e)?e:i.isArrayBufferView(e)?e.buffer:i.isURLSearchParams(e)?(r(t,"application/x-www-form-urlencoded;charset=utf-8"),e.toString()):i.isObject(e)?(r(t,"application/json;charset=utf-8"),JSON.stringify(e)):e}],transformResponse:[function(e){if("string"==typeof e)try{e=JSON.parse(e)}catch(e){}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,validateStatus:function(e){return e>=200&&e<300}};a.headers={common:{Accept:"application/json, text/plain, */*"}},i.forEach(["delete","get","head"],function(e){a.headers[e]={}}),i.forEach(["post","put","patch"],function(e){a.headers[e]=i.merge(u)}),e.exports=a},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){r.forEach(e,function(n,r){r!==t&&r.toUpperCase()===t.toUpperCase()&&(e[t]=n,delete e[r])})}},function(e,t,n){"use strict";var r=n(2),o=n(9),i=n(12),s=n(13),u=n(14),a=n(10),c="undefined"!=typeof window&&window.btoa&&window.btoa.bind(window)||n(15);e.exports=function(e){return new Promise(function(t,f){var p=e.data,d=e.headers;r.isFormData(p)&&delete d["Content-Type"];var l=new XMLHttpRequest,h="onreadystatechange",m=!1;if("undefined"==typeof window||!window.XDomainRequest||"withCredentials"in l||u(e.url)||(l=new window.XDomainRequest,h="onload",m=!0,l.onprogress=function(){},l.ontimeout=function(){}),e.auth){var y=e.auth.username||"",w=e.auth.password||"";d.Authorization="Basic "+c(y+":"+w)}if(l.open(e.method.toUpperCase(),i(e.url,e.params,e.paramsSerializer),!0),l.timeout=e.timeout,l[h]=function(){if(l&&(4===l.readyState||m)&&(0!==l.status||l.responseURL&&0===l.responseURL.indexOf("file:"))){var n="getAllResponseHeaders"in l?s(l.getAllResponseHeaders()):null,r=e.responseType&&"text"!==e.responseType?l.response:l.responseText,i={data:r,status:1223===l.status?204:l.status,statusText:1223===l.status?"No Content":l.statusText,headers:n,config:e,request:l};o(t,f,i),l=null}},l.onerror=function(){f(a("Network Error",e,null,l)),l=null},l.ontimeout=function(){f(a("timeout of "+e.timeout+"ms exceeded",e,"ECONNABORTED",l)),l=null},r.isStandardBrowserEnv()){var g=n(16),v=(e.withCredentials||u(e.url))&&e.xsrfCookieName?g.read(e.xsrfCookieName):void 0;v&&(d[e.xsrfHeaderName]=v)}if("setRequestHeader"in l&&r.forEach(d,function(e,t){"undefined"==typeof p&&"content-type"===t.toLowerCase()?delete d[t]:l.setRequestHeader(t,e)}),e.withCredentials&&(l.withCredentials=!0),e.responseType)try{l.responseType=e.responseType}catch(t){if("json"!==e.responseType)throw t}"function"==typeof e.onDownloadProgress&&l.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&l.upload&&l.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then(function(e){l&&(l.abort(),f(e),l=null)}),void 0===p&&(p=null),l.send(p)})}},function(e,t,n){"use strict";var r=n(10);e.exports=function(e,t,n){var o=n.config.validateStatus;n.status&&o&&!o(n.status)?t(r("Request failed with status code "+n.status,n.config,null,n.request,n)):e(n)}},function(e,t,n){"use strict";var r=n(11);e.exports=function(e,t,n,o,i){var s=new Error(e);return r(s,t,n,o,i)}},function(e,t){"use strict";e.exports=function(e,t,n,r,o){return e.config=t,n&&(e.code=n),e.request=r,e.response=o,e}},function(e,t,n){"use strict";function r(e){return encodeURIComponent(e).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}var o=n(2);e.exports=function(e,t,n){if(!t)return e;var i;if(n)i=n(t);else if(o.isURLSearchParams(t))i=t.toString();else{var s=[];o.forEach(t,function(e,t){null!==e&&"undefined"!=typeof e&&(o.isArray(e)?t+="[]":e=[e],o.forEach(e,function(e){o.isDate(e)?e=e.toISOString():o.isObject(e)&&(e=JSON.stringify(e)),s.push(r(t)+"="+r(e))}))}),i=s.join("&")}return i&&(e+=(e.indexOf("?")===-1?"?":"&")+i),e}},function(e,t,n){"use strict";var r=n(2),o=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var t,n,i,s={};return e?(r.forEach(e.split("\n"),function(e){if(i=e.indexOf(":"),t=r.trim(e.substr(0,i)).toLowerCase(),n=r.trim(e.substr(i+1)),t){if(s[t]&&o.indexOf(t)>=0)return;"set-cookie"===t?s[t]=(s[t]?s[t]:[]).concat([n]):s[t]=s[t]?s[t]+", "+n:n}}),s):s}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){function e(e){var t=e;return n&&(o.setAttribute("href",t),t=o.href),o.setAttribute("href",t),{href:o.href,protocol:o.protocol?o.protocol.replace(/:$/,""):"",host:o.host,search:o.search?o.search.replace(/^\?/,""):"",hash:o.hash?o.hash.replace(/^#/,""):"",hostname:o.hostname,port:o.port,pathname:"/"===o.pathname.charAt(0)?o.pathname:"/"+o.pathname}}var t,n=/(msie|trident)/i.test(navigator.userAgent),o=document.createElement("a");return t=e(window.location.href),function(n){var o=r.isString(n)?e(n):n;return o.protocol===t.protocol&&o.host===t.host}}():function(){return function(){return!0}}()},function(e,t){"use strict";function n(){this.message="String contains an invalid character"}function r(e){for(var t,r,i=String(e),s="",u=0,a=o;i.charAt(0|u)||(a="=",u%1);s+=a.charAt(63&t>>8-u%1*8)){if(r=i.charCodeAt(u+=.75),r>255)throw new n;t=t<<8|r}return s}var o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";n.prototype=new Error,n.prototype.code=5,n.prototype.name="InvalidCharacterError",e.exports=r},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){return{write:function(e,t,n,o,i,s){var u=[];u.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&u.push("expires="+new Date(n).toGMTString()),r.isString(o)&&u.push("path="+o),r.isString(i)&&u.push("domain="+i),s===!0&&u.push("secure"),document.cookie=u.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},function(e,t,n){"use strict";function r(){this.handlers=[]}var o=n(2);r.prototype.use=function(e,t){return this.handlers.push({fulfilled:e,rejected:t}),this.handlers.length-1},r.prototype.eject=function(e){this.handlers[e]&&(this.handlers[e]=null)},r.prototype.forEach=function(e){o.forEach(this.handlers,function(t){null!==t&&e(t)})},e.exports=r},function(e,t,n){"use strict";function r(e){e.cancelToken&&e.cancelToken.throwIfRequested()}var o=n(2),i=n(19),s=n(20),u=n(6),a=n(21),c=n(22);e.exports=function(e){r(e),e.baseURL&&!a(e.url)&&(e.url=c(e.baseURL,e.url)),e.headers=e.headers||{},e.data=i(e.data,e.headers,e.transformRequest),e.headers=o.merge(e.headers.common||{},e.headers[e.method]||{},e.headers||{}),o.forEach(["delete","get","head","post","put","patch","common"],function(t){delete e.headers[t]});var t=e.adapter||u.adapter;return t(e).then(function(t){return r(e),t.data=i(t.data,t.headers,e.transformResponse),t},function(t){return s(t)||(r(e),t&&t.response&&(t.response.data=i(t.response.data,t.response.headers,e.transformResponse))),Promise.reject(t)})}},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t,n){return r.forEach(n,function(n){e=n(e,t)}),e}},function(e,t){"use strict";e.exports=function(e){return!(!e||!e.__CANCEL__)}},function(e,t){"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},function(e,t){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},function(e,t){"use strict";function n(e){this.message=e}n.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},n.prototype.__CANCEL__=!0,e.exports=n},function(e,t,n){"use strict";function r(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise(function(e){t=e});var n=this;e(function(e){n.reason||(n.reason=new o(e),t(n.reason))})}var o=n(23);r.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},r.source=function(){var e,t=new r(function(t){e=t});return{token:t,cancel:e}},e.exports=r},function(e,t){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}}])}); 9 | //# sourceMappingURL=axios.min.map -------------------------------------------------------------------------------- /static/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olajowon/cronnest/3d8263872fb800c098dcfa99c41fae972fdf6ad3/static/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olajowon/cronnest/3d8263872fb800c098dcfa99c41fae972fdf6ad3/static/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olajowon/cronnest/3d8263872fb800c098dcfa99c41fae972fdf6ad3/static/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olajowon/cronnest/3d8263872fb800c098dcfa99c41fae972fdf6ad3/static/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /static/css/tree.css: -------------------------------------------------------------------------------- 1 | .tree ul { 2 | padding-left: 21px; 3 | } 4 | 5 | .tree>ul { 6 | padding-left: 0px; 7 | } 8 | 9 | .tree li { 10 | list-style-type:none; 11 | margin:0; 12 | padding:1px 0px 0 10px; 13 | position:relative 14 | } 15 | .tree li::before, .tree li::after { 16 | content:''; 17 | left:-15px; 18 | position:absolute; 19 | right:auto 20 | } 21 | .tree li::before { 22 | border-left:1px dotted #B0B0B0; 23 | bottom:50px; 24 | height: calc(100% - 1px); 25 | top:-6px; 26 | width:1px 27 | } 28 | .tree li::after { 29 | border-top:1px dotted #B0B0B0; 30 | height:20px; 31 | top:12px; 32 | width:25px 33 | } 34 | .tree li>span { 35 | color:#585858 ; 36 | display:inline-block; 37 | padding:2px 10px 2px 0px; 38 | text-decoration:none; 39 | width: 100%; 40 | } 41 | .tree li>span { 42 | cursor:pointer 43 | } 44 | .tree>ul>li::before, .tree>ul>li::after { 45 | border:0 46 | } 47 | .tree li:last-child::before { 48 | height:19px 49 | } 50 | .tree li>span:hover { 51 | background:#eee; 52 | color:#585858 53 | } 54 | 55 | .tree i { 56 | color: #505050; 57 | font-size: 13px; 58 | } 59 | 60 | .tree li>span>span { 61 | font-size: 14px; 62 | } 63 | 64 | .tree .selected { 65 | background:#eee; 66 | } 67 | 68 | .tree li>span>span.option { 69 | float: right; 70 | font-size: 8px; 71 | padding-top: 2px; 72 | } 73 | 74 | .tree li>span>span.option>i { 75 | cursor: pointer; 76 | margin-left: 5px; 77 | } 78 | 79 | .tree li>span>span.option>i:hover { 80 | color: #202020; 81 | } 82 | -------------------------------------------------------------------------------- /static/font-awesome-4.7.0/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} 5 | -------------------------------------------------------------------------------- /static/font-awesome-4.7.0/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olajowon/cronnest/3d8263872fb800c098dcfa99c41fae972fdf6ad3/static/font-awesome-4.7.0/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /static/font-awesome-4.7.0/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olajowon/cronnest/3d8263872fb800c098dcfa99c41fae972fdf6ad3/static/font-awesome-4.7.0/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /static/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olajowon/cronnest/3d8263872fb800c098dcfa99c41fae972fdf6ad3/static/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /static/font-awesome-4.7.0/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olajowon/cronnest/3d8263872fb800c098dcfa99c41fae972fdf6ad3/static/font-awesome-4.7.0/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /static/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olajowon/cronnest/3d8263872fb800c098dcfa99c41fae972fdf6ad3/static/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olajowon/cronnest/3d8263872fb800c098dcfa99c41fae972fdf6ad3/static/img/favicon.ico -------------------------------------------------------------------------------- /static/img/login-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olajowon/cronnest/3d8263872fb800c098dcfa99c41fae972fdf6ad3/static/img/login-logo.ico -------------------------------------------------------------------------------- /static/img/title-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olajowon/cronnest/3d8263872fb800c098dcfa99c41fae972fdf6ad3/static/img/title-logo.png -------------------------------------------------------------------------------- /static/jquery-jsonview-1.2.3/jquery.jsonview.min.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";.jsonview{font-family:monospace;font-size:1.1em;white-space:pre-wrap}.jsonview .prop{font-weight:700}.jsonview .null{color:red}.jsonview .bool,.jsonview .num{color:#00f}.jsonview .string{color:green;white-space:pre-wrap}.jsonview .string.multiline{display:inline-block;vertical-align:text-top}.jsonview .collapser{position:absolute;left:-1em;cursor:pointer}.jsonview .collapsible{transition:height 1.2s;transition:width 1.2s}.jsonview .collapsible.collapsed{height:.8em;width:1em;display:inline-block;overflow:hidden;margin:0}.jsonview .collapsible.collapsed:before{content:"…";width:1em;margin-left:.2em}.jsonview .collapser.collapsed{transform:rotate(0)}.jsonview .q{display:inline-block;width:0;color:transparent}.jsonview li{position:relative}.jsonview ul{list-style:none;margin:0 0 0 2em;padding:0}.jsonview h1{font-size:1.2em} -------------------------------------------------------------------------------- /static/jquery-jsonview-1.2.3/jquery.jsonview.min.js: -------------------------------------------------------------------------------- 1 | !function(e){var n,t,l,r;return l=function(){function e(e){null==e&&(e={}),this.options=e}return e.prototype.htmlEncode=function(e){return null!==e?e.toString().replace(/&/g,"&").replace(/"/g,""").replace(//g,">"):""},e.prototype.jsString=function(e){return e=JSON.stringify(e).slice(1,-1),this.htmlEncode(e)},e.prototype.decorateWithSpan=function(e,n){return''+this.htmlEncode(e)+""},e.prototype.valueToHTML=function(e,n){var t;return null==n&&(n=0),t=Object.prototype.toString.call(e).match(/\s(.+)]/)[1].toLowerCase(),this[""+t+"ToHTML"].call(this,e,n)},e.prototype.nullToHTML=function(e){return this.decorateWithSpan("null","null")},e.prototype.numberToHTML=function(e){return this.decorateWithSpan(e,"num")},e.prototype.stringToHTML=function(e){var n,t;return/^(http|https|file):\/\/[^\s]+$/i.test(e)?'"'+this.jsString(e)+'"':(n="",e=this.jsString(e),this.options.nl2br&&(t=/([^>\\r\\n]?)(\\r\\n|\\n\\r|\\r|\\n)/g,t.test(e)&&(n=" multiline",e=(e+"").replace(t,"$1
"))),'"'+e+'"')},e.prototype.booleanToHTML=function(e){return this.decorateWithSpan(e,"bool")},e.prototype.arrayToHTML=function(e,n){var t,l,r,o,s,i,a,p;for(null==n&&(n=0),l=!1,s="",o=e.length,r=a=0,p=e.length;p>a;r=++a)i=e[r],l=!0,s+="
  • "+this.valueToHTML(i,n+1),o>1&&(s+=","),s+="
  • ",o--;return l?(t=0===n?"":" collapsible",'[]"):"[ ]"},e.prototype.objectToHTML=function(e,n){var t,l,r,o,s,i,a;null==n&&(n=0),l=!1,s="",o=0;for(i in e)o++;for(i in e)a=e[i],l=!0,r=this.options.escape?this.jsString(i):i,s+='
  • "'+r+'": '+this.valueToHTML(a,n+1),o>1&&(s+=","),s+="
  • ",o--;return l?(t=0===n?"":" collapsible",'{}"):"{ }"},e.prototype.jsonToHTML=function(e){return'
    '+this.valueToHTML(e)+"
    "},e}(),"undefined"!=typeof module&&null!==module&&(module.exports=l),t=function(){function e(){}return e.bindEvent=function(e,n){var t;return t=document.createElement("div"),t.className="collapser",t.innerHTML=n.collapsed?"+":"-",t.addEventListener("click",function(e){return function(t){return e.toggle(t.target,n)}}(this)),e.insertBefore(t,e.firstChild),n.collapsed?this.collapse(t):void 0},e.expand=function(e){var n,t;return t=this.collapseTarget(e),""!==t.style.display?(n=t.parentNode.getElementsByClassName("ellipsis")[0],t.parentNode.removeChild(n),t.style.display="",e.innerHTML="-"):void 0},e.collapse=function(e){var n,t;return t=this.collapseTarget(e),"none"!==t.style.display?(t.style.display="none",n=document.createElement("span"),n.className="ellipsis",n.innerHTML=" … ",t.parentNode.insertBefore(n,t),e.innerHTML="+"):void 0},e.toggle=function(e,n){var t,l,r,o,s,i;if(null==n&&(n={}),r=this.collapseTarget(e),t="none"===r.style.display?"expand":"collapse",n.recursive_collapser){for(l=e.parentNode.getElementsByClassName("collapser"),i=[],o=0,s=l.length;s>o;o++)e=l[o],i.push(this[t](e));return i}return this[t](e)},e.collapseTarget=function(e){var n,t;return t=e.parentNode.getElementsByClassName("collapsible"),t.length?n=t[0]:void 0},e}(),n=e,r={collapse:function(e){return"-"===e.innerHTML?t.collapse(e):void 0},expand:function(e){return"+"===e.innerHTML?t.expand(e):void 0},toggle:function(e){return t.toggle(e)}},n.fn.JSONView=function(){var e,o,s,i,a,p,c;return e=arguments,null!=r[e[0]]?(a=e[0],this.each(function(){var t,l;return t=n(this),null!=e[1]?(l=e[1],t.find(".jsonview .collapsible.level"+l).siblings(".collapser").each(function(){return r[a](this)})):t.find(".jsonview > ul > li .collapsible").siblings(".collapser").each(function(){return r[a](this)})})):(i=e[0],p=e[1]||{},o={collapsed:!1,nl2br:!1,recursive_collapser:!1,escape:!0},p=n.extend(o,p),s=new l({nl2br:p.nl2br,escape:p.escape}),"[object String]"===Object.prototype.toString.call(i)&&(i=JSON.parse(i)),c=s.jsonToHTML(i),this.each(function(){var e,l,r,o,s,i;for(e=n(this),e.html(c),r=e[0].getElementsByClassName("collapsible"),i=[],o=0,s=r.length;s>o;o++)l=r[o],"LI"===l.parentNode.nodeName?i.push(t.bindEvent(l.parentNode,p)):i.push(void 0);return i}))}}(jQuery); -------------------------------------------------------------------------------- /static/js/crontab.js: -------------------------------------------------------------------------------- 1 | new Vue({ 2 | el: '#app', 3 | delimiters: ['${', '}'], 4 | data: { 5 | mouseOver: null, 6 | pageName: "crontab", 7 | locking: null, 8 | tree: { 9 | name: 'Crontab', 10 | open: true, 11 | hostgroups: [] 12 | }, 13 | queryParams: {hostgroup: null, host: null, tab: "", row: null}, 14 | currHost: {}, 15 | openUpdateTabJobModal: true, 16 | updateJobData: {host_id: null, tab: "", old_job: {}, new_job: {}}, 17 | createJobData: {host_id: null, tab: "", job: {}}, 18 | createHostgroupData: {name: ""}, 19 | updateHostgroupData: {hostgroup: null, data: {name: ""}}, 20 | addHostgroupHostsData: {hostgroup: null, data: {hosts: ""}}, 21 | removeHostgroupHostsData: {hostgroup: null, data: {hosts: []}}, 22 | updateHostData: {host: null, data: {address: ""}}, 23 | }, 24 | watch: { 25 | "currHost.id": function(n, o) { 26 | if (n > 0) { 27 | this.getHostCrontab(this.currHost) 28 | } 29 | }, 30 | locking: function (n, o) { 31 | if (n != null && n != "") { 32 | $("#lockingModal").modal('show') 33 | } else { 34 | $("#lockingModal").modal('hide') 35 | } 36 | } 37 | }, 38 | mounted: function () { 39 | this.initQueryParams(); 40 | this.init(); 41 | }, 42 | methods: { 43 | init() { 44 | let _this = this; 45 | axios.get("/api/hostgroups/") 46 | .then(function(resp){ 47 | let hostgroups = resp.data.data; 48 | hostgroups.forEach(function (hg, _) { 49 | if (hg.id == _this.queryParams.hostgroup) { 50 | hg.open = true; 51 | hg.hosts = []; 52 | axios.get("/api/hostgroups/"+ hg.id +"/hosts/") 53 | .then(function(resp){ 54 | let hosts = resp.data.data; 55 | hg.hosts = hosts; 56 | hg.hosts.forEach(function (h, _) { 57 | if (h.id == _this.queryParams.host) { 58 | _this.currHost = $.extend({crontab: {}, hostgroup: hg.id}, h); 59 | } 60 | }) 61 | }) 62 | } else { 63 | hg.open = false; 64 | hg.hosts = []; 65 | } 66 | }) 67 | _this.tree.hostgroups = hostgroups; 68 | }) 69 | }, 70 | 71 | initQueryParams() { 72 | for (let k in this.queryParams) { 73 | let v = this.getQueryVariable(k); 74 | if (v) { 75 | this.queryParams[k] = decodeURI(v); 76 | } else { 77 | this.queryParams[k] = null; 78 | } 79 | } 80 | }, 81 | 82 | getHostCrontab(h) { 83 | let _this = this 84 | this.locking = "正在获取Crontab..." 85 | axios.get("/api/hosts/"+ h.id +"/crontab/") 86 | .then(function(resp){ 87 | let crontab = resp.data.data; 88 | 89 | if (crontab.tab) { 90 | crontab.tab = _this.makeNewTab(crontab.tab) 91 | } 92 | h.crontab = crontab; 93 | _this.locking = null; 94 | }) 95 | }, 96 | 97 | makeNewTab(tab) { 98 | let newtab = {}; 99 | if (tab.root) { 100 | newtab.root = tab.root; 101 | } 102 | for (let k in tab) { 103 | if (k!='system' && k!='root') { 104 | newtab[k] = tab[k]; 105 | } 106 | } 107 | if (tab.system) { 108 | newtab.system = tab.system; 109 | } 110 | return newtab 111 | }, 112 | 113 | getHostgroupHost: function(hg) { 114 | let _this = this; 115 | axios.get("/api/hostgroups/"+ hg.id +"/hosts/") 116 | .then(function(resp){ 117 | let hosts = resp.data.data; 118 | hg.hosts = hosts; 119 | }) 120 | }, 121 | 122 | handleHostgroupNodeClick(hg) { 123 | hg.open = !hg.open; 124 | if (hg.open == true && hg.hosts.length == 0) { 125 | this.getHostgroupHost(hg) 126 | } 127 | }, 128 | 129 | handleHostClick(h, hg) { 130 | if (h.id != this.currHost.id) { 131 | this.currHost = $.extend({crontab: {}, hostgroup: hg.id}, h); 132 | let href = window.location.pathname + "?hostgroup=" + hg.id + "&host=" + h.id; 133 | window.history.pushState({}, null, href); 134 | this.queryParams = { 135 | hostgoups: hg.id, 136 | host: h.id, 137 | crontab: null, 138 | row: null 139 | } 140 | } 141 | }, 142 | 143 | getQueryVariable: function (variable) { 144 | let query = window.location.search.substring(1); 145 | let vars = query.split("&"); 146 | for (let i=0;i")) 190 | } else { 191 | toastr.error(error) 192 | } 193 | }); 194 | }, 195 | 196 | updateJob(hostId, tab, job) { 197 | this.updateJobData.host_id = hostId; 198 | this.updateJobData.tab = tab; 199 | this.updateJobData.old_job = job; 200 | this.updateJobData.new_job = { 201 | enabled: job.enabled, 202 | slices: job.slices, 203 | command: job.command, 204 | comment: job.comment, 205 | }; 206 | if (tab == "system") { 207 | this.updateJobData.new_job.user = job.user; 208 | } 209 | 210 | $("#updateJobModal").modal("show"); 211 | }, 212 | 213 | submitUpdateJob(updateJobData) { 214 | let hostId = updateJobData.host_id; 215 | let data = { 216 | tab: updateJobData.tab, 217 | old_job: updateJobData.old_job, 218 | new_job: updateJobData.new_job 219 | } 220 | let _this = this; 221 | _this.locking = "正在修改主机Crontab Job,请稍等..."; 222 | axios.put("/api/hosts/"+ hostId +"/crontab/job/", data) 223 | .then(function(resp){ 224 | updateJobData.old_job.enabled = updateJobData.new_job.enabled; 225 | updateJobData.old_job.slices = updateJobData.new_job.slices; 226 | updateJobData.old_job.command = updateJobData.new_job.command; 227 | updateJobData.old_job.comment = updateJobData.new_job.comment; 228 | if (updateJobData.tab == "system") { 229 | updateJobData.old_job.user = updateJobData.new_job.user; 230 | } 231 | _this.locking = null; 232 | toastr.success("修改成功") 233 | $("#updateJobModal").modal("hide"); 234 | }) 235 | .catch(function(error){ 236 | _this.locking = null; 237 | if (error.response && error.response.data && error.response.data.msg) { 238 | toastr.error(error.response.data.msg.replace(/\n/g,"
    ")) 239 | } else { 240 | toastr.error(error) 241 | } 242 | }); 243 | }, 244 | 245 | deleteJob(hostId, tab, job) { 246 | let r = confirm("删除后不可恢复,确定删除此Job?"); 247 | if(r == true) { 248 | let data = { 249 | tab: tab, 250 | job: job, 251 | } 252 | let _this = this; 253 | _this.locking = "正在删除主机Crontab Job,请稍等..."; 254 | axios.delete("/api/hosts/"+ hostId +"/crontab/job/", {data: data}) 255 | .then(function(resp){ 256 | if (_this.currHost.id = hostId) { 257 | _this.currHost.crontab.tab = _this.makeNewTab(resp.data.data.tab) 258 | } 259 | _this.locking = null; 260 | toastr.success("删除成功") 261 | }) 262 | .catch(function(error){ 263 | _this.locking = null; 264 | if (error.response && error.response.data && error.response.data.msg) { 265 | toastr.error(error.response.data.msg.replace(/\n/g,"
    ")) 266 | } else { 267 | toastr.error(error) 268 | } 269 | }); 270 | } 271 | }, 272 | 273 | createHostgroup() { 274 | this.createHostgroupData.name = ""; 275 | $("#createHostgroupModal").modal("show"); 276 | }, 277 | 278 | submitCreateHostgroup(createHgData) { 279 | let _this = this; 280 | _this.locking = "正在创建主机组,请稍等..."; 281 | axios.post("/api/hostgroups/", createHgData) 282 | .then(function(resp){ 283 | let hg = resp.data.data; 284 | hg.open = false; 285 | hg.hosts = []; 286 | _this.tree.hostgroups.push(hg) 287 | _this.locking = null; 288 | toastr.success("创建成功") 289 | $("#createHostgroupModal").modal("hide"); 290 | }) 291 | .catch(function(error){ 292 | _this.locking = null; 293 | if (error.response && error.response.data && error.response.data.msg) { 294 | toastr.error(error.response.data.msg.replace(/\n/g,"
    ")) 295 | } else { 296 | toastr.error(error) 297 | } 298 | }); 299 | }, 300 | 301 | updateHostgroup(hg) { 302 | this.updateHostgroupData.hostgroup = hg; 303 | this.updateHostgroupData.data.name = hg.name; 304 | $("#updateHostgroupModal").modal("show"); 305 | }, 306 | 307 | submitUpdateHostgroup(updateHgData) { 308 | let _this = this; 309 | _this.locking = "正在修改主机组,请稍等..."; 310 | axios.put("/api/hostgroups/" + updateHgData.hostgroup.id + "/", updateHgData.data) 311 | .then(function(resp){ 312 | updateHgData.hostgroup.name = updateHgData.data.name; 313 | 314 | _this.locking = null; 315 | toastr.success("修改成功") 316 | $("#updateHostgroupModal").modal("hide"); 317 | }) 318 | .catch(function(error){ 319 | _this.locking = null; 320 | if (error.response && error.response.data && error.response.data.msg) { 321 | toastr.error(error.response.data.msg.replace(/\n/g,"
    ")) 322 | } else { 323 | toastr.error(error) 324 | } 325 | }); 326 | }, 327 | 328 | deleteHostgroup(hg, idx) { 329 | let r = confirm("删除后不可恢复,确定删除此主机组?\n注:仅属于该主机组的主机将会被一并删除!"); 330 | if(r == true) { 331 | let _this = this; 332 | _this.locking = "正在删除主机组,请稍等..."; 333 | axios.delete("/api/hostgroups/"+ hg.id +"/") 334 | .then(function(resp){ 335 | _this.tree.hostgroups.splice(idx, 1); 336 | _this.locking = null; 337 | toastr.success("删除成功") 338 | }) 339 | .catch(function(error){ 340 | _this.locking = null; 341 | if (error.response && error.response.data && error.response.data.msg) { 342 | toastr.error(error.response.data.msg.replace(/\n/g,"
    ")) 343 | } else { 344 | toastr.error(error) 345 | } 346 | }); 347 | } 348 | }, 349 | 350 | addHostgroupHosts(hg) { 351 | this.addHostgroupHostsData.hostgroup = hg; 352 | this.addHostgroupHostsData.data.hosts = ""; 353 | $("#addHostgroupHostsModal").modal("show") 354 | }, 355 | 356 | submitAddHostgroupHosts(addHgHostData) { 357 | let _this = this; 358 | _this.locking = "正在为主机组添加主机,请稍等..."; 359 | console.log(addHgHostData.data.hosts.split('\n')) 360 | axios.post("/api/hostgroups/"+ addHgHostData.hostgroup.id +"/hosts/", 361 | {hosts: addHgHostData.data.hosts.split('\n')}) 362 | .then(function(resp){ 363 | _this.locking = null; 364 | toastr.success("添加成功") 365 | $("#addHostgroupHostsModal").modal("hide"); 366 | addHgHostData.hostgroup.open = true; 367 | _this.getHostgroupHost(addHgHostData.hostgroup); 368 | }) 369 | .catch(function(error){ 370 | _this.locking = null; 371 | if (error.response && error.response.data && error.response.data.msg) { 372 | toastr.error(error.response.data.msg.replace(/\n/g,"
    ")) 373 | } else { 374 | toastr.error(error) 375 | } 376 | }); 377 | }, 378 | 379 | deleteHost(h) { 380 | let r = confirm("删除后不可恢复,确定删除此主机?\n注:其他组内的该主机也会被一并删除!"); 381 | if(r == true) { 382 | let _this = this; 383 | _this.locking = "正在删除主机,请稍等..."; 384 | axios.delete("/api/hosts/"+ h.id +"/") 385 | .then(function(resp){ 386 | for (let i=0; i<_this.tree.hostgroups.length; i++) { 387 | for (let j=_this.tree.hostgroups[i].hosts.length-1; j>=0; j--) { 388 | if (_this.tree.hostgroups[i].hosts[j].id == h.id) { 389 | _this.tree.hostgroups[i].hosts.splice(j, 1); 390 | } 391 | } 392 | } 393 | 394 | _this.locking = null; 395 | toastr.success("删除成功") 396 | }) 397 | .catch(function(error){ 398 | _this.locking = null; 399 | if (error.response && error.response.data && error.response.data.msg) { 400 | toastr.error(error.response.data.msg.replace(/\n/g,"
    ")) 401 | } else { 402 | toastr.error(error) 403 | } 404 | }); 405 | } 406 | }, 407 | 408 | updateHost(h) { 409 | this.updateHostData.host = h; 410 | this.updateHostData.data.address = h.address; 411 | $("#updateHostModal").modal("show"); 412 | }, 413 | 414 | submitUpdateHost(updateData) { 415 | let _this = this; 416 | _this.locking = "正在修改主机,请稍等..."; 417 | axios.put("/api/hosts/" + updateData.host.id + "/", updateData.data) 418 | .then(function(resp){ 419 | for (let i=0; i<_this.tree.hostgroups.length; i++) { 420 | for (let j=0; j<_this.tree.hostgroups[i].hosts.length; j++) { 421 | if (_this.tree.hostgroups[i].hosts[j].id == updateData.host.id) { 422 | _this.tree.hostgroups[i].hosts[j].address = updateData.data.address; 423 | } 424 | } 425 | } 426 | _this.locking = null; 427 | toastr.success("修改成功") 428 | $("#updateHostModal").modal("hide"); 429 | }) 430 | .catch(function(error){ 431 | _this.locking = null; 432 | if (error.response && error.response.data && error.response.data.msg) { 433 | toastr.error(error.response.data.msg.replace(/\n/g,"
    ")) 434 | } else { 435 | toastr.error(error) 436 | } 437 | }); 438 | }, 439 | 440 | removeHostgroupHosts(hg) { 441 | this.removeHostgroupHostsData.hostgroup = hg; 442 | this.removeHostgroupHostsData.data.hosts = []; 443 | let _this = this; 444 | axios.get("/api/hostgroups/"+ hg.id +"/hosts/") 445 | .then(function(resp){ 446 | let hosts = resp.data.data; 447 | for (let i=0; i=0; i--) { 468 | if (removeHosts.indexOf(removeHgHostData.hostgroup.hosts[i].address) >= 0) { 469 | removeHgHostData.hostgroup.hosts.splice(i, 1); 470 | } 471 | } 472 | 473 | _this.locking = null; 474 | toastr.success("移除成功") 475 | $("#removeHostgroupHostsModal").modal("hide"); 476 | }) 477 | .catch(function(error){ 478 | _this.locking = null; 479 | if (error.response && error.response.data && error.response.data.msg) { 480 | toastr.error(error.response.data.msg.replace(/\n/g,"
    ")) 481 | } else { 482 | toastr.error(error) 483 | } 484 | }); 485 | } 486 | } 487 | }) 488 | -------------------------------------------------------------------------------- /static/toastr-2.1.3/toastr.css: -------------------------------------------------------------------------------- 1 | .toast-title { 2 | font-weight: bold; 3 | } 4 | .toast-message { 5 | -ms-word-wrap: break-word; 6 | word-wrap: break-word; 7 | } 8 | .toast-message a, 9 | .toast-message label { 10 | color: #FFFFFF; 11 | } 12 | .toast-message a:hover { 13 | color: #CCCCCC; 14 | text-decoration: none; 15 | } 16 | .toast-close-button { 17 | position: relative; 18 | right: -0.3em; 19 | top: -0.3em; 20 | float: right; 21 | font-size: 20px; 22 | font-weight: bold; 23 | color: #FFFFFF; 24 | -webkit-text-shadow: 0 1px 0 #ffffff; 25 | text-shadow: 0 1px 0 #ffffff; 26 | opacity: 0.8; 27 | -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80); 28 | filter: alpha(opacity=80); 29 | line-height: 1; 30 | } 31 | .toast-close-button:hover, 32 | .toast-close-button:focus { 33 | color: #000000; 34 | text-decoration: none; 35 | cursor: pointer; 36 | opacity: 0.4; 37 | -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40); 38 | filter: alpha(opacity=40); 39 | } 40 | .rtl .toast-close-button { 41 | left: -0.3em; 42 | float: left; 43 | right: 0.3em; 44 | } 45 | /*Additional properties for button version 46 | iOS requires the button element instead of an anchor tag. 47 | If you want the anchor version, it requires `href="#"`.*/ 48 | button.toast-close-button { 49 | padding: 0; 50 | cursor: pointer; 51 | background: transparent; 52 | border: 0; 53 | -webkit-appearance: none; 54 | } 55 | .toast-top-center { 56 | top: 0; 57 | right: 0; 58 | width: 100%; 59 | } 60 | .toast-bottom-center { 61 | bottom: 0; 62 | right: 0; 63 | width: 100%; 64 | } 65 | .toast-top-full-width { 66 | top: 0; 67 | right: 0; 68 | width: 100%; 69 | } 70 | .toast-bottom-full-width { 71 | bottom: 0; 72 | right: 0; 73 | width: 100%; 74 | } 75 | .toast-top-left { 76 | top: 12px; 77 | left: 12px; 78 | } 79 | .toast-top-right { 80 | top: 12px; 81 | right: 12px; 82 | } 83 | .toast-bottom-right { 84 | right: 12px; 85 | bottom: 12px; 86 | } 87 | .toast-bottom-left { 88 | bottom: 12px; 89 | left: 12px; 90 | } 91 | #toast-container { 92 | position: fixed; 93 | z-index: 999999; 94 | pointer-events: none; 95 | /*overrides*/ 96 | } 97 | #toast-container * { 98 | -moz-box-sizing: border-box; 99 | -webkit-box-sizing: border-box; 100 | box-sizing: border-box; 101 | } 102 | #toast-container > div { 103 | position: relative; 104 | pointer-events: auto; 105 | overflow: hidden; 106 | margin: 0 0 6px; 107 | padding: 15px 15px 15px 50px; 108 | width: 300px; 109 | -moz-border-radius: 3px 3px 3px 3px; 110 | -webkit-border-radius: 3px 3px 3px 3px; 111 | border-radius: 3px 3px 3px 3px; 112 | background-position: 15px center; 113 | background-repeat: no-repeat; 114 | -moz-box-shadow: 0 0 12px #999999; 115 | -webkit-box-shadow: 0 0 12px #999999; 116 | box-shadow: 0 0 12px #999999; 117 | color: #FFFFFF; 118 | opacity: 0.8; 119 | -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80); 120 | filter: alpha(opacity=80); 121 | } 122 | #toast-container > div.rtl { 123 | direction: rtl; 124 | padding: 15px 50px 15px 15px; 125 | background-position: right 15px center; 126 | } 127 | #toast-container > div:hover { 128 | -moz-box-shadow: 0 0 12px #000000; 129 | -webkit-box-shadow: 0 0 12px #000000; 130 | box-shadow: 0 0 12px #000000; 131 | opacity: 1; 132 | -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); 133 | filter: alpha(opacity=100); 134 | cursor: pointer; 135 | } 136 | #toast-container > .toast-info { 137 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important; 138 | } 139 | #toast-container > .toast-error { 140 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important; 141 | } 142 | #toast-container > .toast-success { 143 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important; 144 | } 145 | #toast-container > .toast-warning { 146 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important; 147 | } 148 | #toast-container.toast-top-center > div, 149 | #toast-container.toast-bottom-center > div { 150 | width: 300px; 151 | margin-left: auto; 152 | margin-right: auto; 153 | } 154 | #toast-container.toast-top-full-width > div, 155 | #toast-container.toast-bottom-full-width > div { 156 | width: 96%; 157 | margin-left: auto; 158 | margin-right: auto; 159 | } 160 | .toast { 161 | background-color: #030303; 162 | } 163 | .toast-success { 164 | background-color: #51A351; 165 | } 166 | .toast-error { 167 | background-color: #BD362F; 168 | } 169 | .toast-info { 170 | background-color: #2F96B4; 171 | } 172 | .toast-warning { 173 | background-color: #F89406; 174 | } 175 | .toast-progress { 176 | position: absolute; 177 | left: 0; 178 | bottom: 0; 179 | height: 4px; 180 | background-color: #000000; 181 | opacity: 0.4; 182 | -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40); 183 | filter: alpha(opacity=40); 184 | } 185 | /*Responsive Design*/ 186 | @media all and (max-width: 240px) { 187 | #toast-container > div { 188 | padding: 8px 8px 8px 50px; 189 | width: 11em; 190 | } 191 | #toast-container > div.rtl { 192 | padding: 8px 50px 8px 8px; 193 | } 194 | #toast-container .toast-close-button { 195 | right: -0.2em; 196 | top: -0.2em; 197 | } 198 | #toast-container .rtl .toast-close-button { 199 | left: -0.2em; 200 | right: 0.2em; 201 | } 202 | } 203 | @media all and (min-width: 241px) and (max-width: 480px) { 204 | #toast-container > div { 205 | padding: 8px 8px 8px 50px; 206 | width: 18em; 207 | } 208 | #toast-container > div.rtl { 209 | padding: 8px 50px 8px 8px; 210 | } 211 | #toast-container .toast-close-button { 212 | right: -0.2em; 213 | top: -0.2em; 214 | } 215 | #toast-container .rtl .toast-close-button { 216 | left: -0.2em; 217 | right: 0.2em; 218 | } 219 | } 220 | @media all and (min-width: 481px) and (max-width: 768px) { 221 | #toast-container > div { 222 | padding: 15px 15px 15px 50px; 223 | width: 25em; 224 | } 225 | #toast-container > div.rtl { 226 | padding: 15px 50px 15px 15px; 227 | } 228 | } -------------------------------------------------------------------------------- /static/toastr-2.1.3/toastr.min.js: -------------------------------------------------------------------------------- 1 | !function(e){e(["jquery"],function(e){return function(){function t(e,t,n){return g({type:O.error,iconClass:m().iconClasses.error,message:e,optionsOverride:n,title:t})}function n(t,n){return t||(t=m()),v=e("#"+t.containerId),v.length?v:(n&&(v=d(t)),v)}function o(e,t,n){return g({type:O.info,iconClass:m().iconClasses.info,message:e,optionsOverride:n,title:t})}function s(e){C=e}function i(e,t,n){return g({type:O.success,iconClass:m().iconClasses.success,message:e,optionsOverride:n,title:t})}function a(e,t,n){return g({type:O.warning,iconClass:m().iconClasses.warning,message:e,optionsOverride:n,title:t})}function r(e,t){var o=m();v||n(o),u(e,o,t)||l(o)}function c(t){var o=m();return v||n(o),t&&0===e(":focus",t).length?void h(t):void(v.children().length&&v.remove())}function l(t){for(var n=v.children(),o=n.length-1;o>=0;o--)u(e(n[o]),t)}function u(t,n,o){var s=!(!o||!o.force)&&o.force;return!(!t||!s&&0!==e(":focus",t).length)&&(t[n.hideMethod]({duration:n.hideDuration,easing:n.hideEasing,complete:function(){h(t)}}),!0)}function d(t){return v=e("
    ").attr("id",t.containerId).addClass(t.positionClass),v.appendTo(e(t.target)),v}function p(){return{tapToDismiss:!0,toastClass:"toast",containerId:"toast-container",debug:!1,showMethod:"fadeIn",showDuration:300,showEasing:"swing",onShown:void 0,hideMethod:"fadeOut",hideDuration:1e3,hideEasing:"swing",onHidden:void 0,closeMethod:!1,closeDuration:!1,closeEasing:!1,closeOnHover:!0,extendedTimeOut:1e3,iconClasses:{error:"toast-error",info:"toast-info",success:"toast-success",warning:"toast-warning"},iconClass:"toast-info",positionClass:"toast-top-right",timeOut:5e3,titleClass:"toast-title",messageClass:"toast-message",escapeHtml:!1,target:"body",closeHtml:'',closeClass:"toast-close-button",newestOnTop:!0,preventDuplicates:!1,progressBar:!1,progressClass:"toast-progress",rtl:!1}}function f(e){C&&C(e)}function g(t){function o(e){return null==e&&(e=""),e.replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function s(){c(),u(),d(),p(),g(),C(),l(),i()}function i(){var e="";switch(t.iconClass){case"toast-success":case"toast-info":e="polite";break;default:e="assertive"}I.attr("aria-live",e)}function a(){E.closeOnHover&&I.hover(H,D),!E.onclick&&E.tapToDismiss&&I.click(b),E.closeButton&&j&&j.click(function(e){e.stopPropagation?e.stopPropagation():void 0!==e.cancelBubble&&e.cancelBubble!==!0&&(e.cancelBubble=!0),E.onCloseClick&&E.onCloseClick(e),b(!0)}),E.onclick&&I.click(function(e){E.onclick(e),b()})}function r(){I.hide(),I[E.showMethod]({duration:E.showDuration,easing:E.showEasing,complete:E.onShown}),E.timeOut>0&&(k=setTimeout(b,E.timeOut),F.maxHideTime=parseFloat(E.timeOut),F.hideEta=(new Date).getTime()+F.maxHideTime,E.progressBar&&(F.intervalId=setInterval(x,10)))}function c(){t.iconClass&&I.addClass(E.toastClass).addClass(y)}function l(){E.newestOnTop?v.prepend(I):v.append(I)}function u(){if(t.title){var e=t.title;E.escapeHtml&&(e=o(t.title)),M.append(e).addClass(E.titleClass),I.append(M)}}function d(){if(t.message){var e=t.message;E.escapeHtml&&(e=o(t.message)),B.append(e).addClass(E.messageClass),I.append(B)}}function p(){E.closeButton&&(j.addClass(E.closeClass).attr("role","button"),I.prepend(j))}function g(){E.progressBar&&(q.addClass(E.progressClass),I.prepend(q))}function C(){E.rtl&&I.addClass("rtl")}function O(e,t){if(e.preventDuplicates){if(t.message===w)return!0;w=t.message}return!1}function b(t){var n=t&&E.closeMethod!==!1?E.closeMethod:E.hideMethod,o=t&&E.closeDuration!==!1?E.closeDuration:E.hideDuration,s=t&&E.closeEasing!==!1?E.closeEasing:E.hideEasing;if(!e(":focus",I).length||t)return clearTimeout(F.intervalId),I[n]({duration:o,easing:s,complete:function(){h(I),clearTimeout(k),E.onHidden&&"hidden"!==P.state&&E.onHidden(),P.state="hidden",P.endTime=new Date,f(P)}})}function D(){(E.timeOut>0||E.extendedTimeOut>0)&&(k=setTimeout(b,E.extendedTimeOut),F.maxHideTime=parseFloat(E.extendedTimeOut),F.hideEta=(new Date).getTime()+F.maxHideTime)}function H(){clearTimeout(k),F.hideEta=0,I.stop(!0,!0)[E.showMethod]({duration:E.showDuration,easing:E.showEasing})}function x(){var e=(F.hideEta-(new Date).getTime())/F.maxHideTime*100;q.width(e+"%")}var E=m(),y=t.iconClass||E.iconClass;if("undefined"!=typeof t.optionsOverride&&(E=e.extend(E,t.optionsOverride),y=t.optionsOverride.iconClass||y),!O(E,t)){T++,v=n(E,!0);var k=null,I=e("
    "),M=e("
    "),B=e("
    "),q=e("
    "),j=e(E.closeHtml),F={intervalId:null,hideEta:null,maxHideTime:null},P={toastId:T,state:"visible",startTime:new Date,options:E,map:t};return s(),r(),a(),f(P),E.debug&&console&&console.log(P),I}}function m(){return e.extend({},p(),b.options)}function h(e){v||(v=n()),e.is(":visible")||(e.remove(),e=null,0===v.children().length&&(v.remove(),w=void 0))}var v,C,w,T=0,O={error:"error",info:"info",success:"success",warning:"warning"},b={clear:r,remove:c,error:t,getContainer:n,info:o,options:{},subscribe:s,success:i,version:"2.1.3",warning:a};return b}()})}("function"==typeof define&&define.amd?define:function(e,t){"undefined"!=typeof module&&module.exports?module.exports=t(require("jquery")):window.toastr=t(window.jQuery)}); 2 | //# sourceMappingURL=toastr.js.map -------------------------------------------------------------------------------- /table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE hostgroup ( 2 | id SERIAL PRIMARY KEY, 3 | name text, 4 | created_at timestamp with time zone, 5 | updated_at timestamp with time zone 6 | ); 7 | 8 | 9 | CREATE TABLE host ( 10 | id SERIAL PRIMARY KEY, 11 | address text NOT NULL UNIQUE, 12 | created_at timestamp with time zone, 13 | updated_at timestamp with time zone, 14 | status text NOT NULL DEFAULT 'enabled'::text 15 | ); 16 | 17 | 18 | CREATE TABLE hostgroup_host ( 19 | hostgroup_id integer, 20 | host_id integer 21 | ); 22 | 23 | CREATE TABLE host_crontab ( 24 | id SERIAL PRIMARY KEY, 25 | host_id integer, 26 | tab jsonb DEFAULT '{}'::jsonb, 27 | created_at timestamp with time zone, 28 | updated_at timestamp with time zone, 29 | status text, 30 | msg text, 31 | last_succeed timestamp with time zone 32 | ); 33 | 34 | 35 | CREATE TABLE operation_record ( 36 | id SERIAL PRIMARY KEY, 37 | source_type text, 38 | source_id integer, 39 | operation_type text, 40 | data jsonb DEFAULT '{}'::jsonb, 41 | "user" text, 42 | created_at timestamp with time zone NOT NULL, 43 | source_label text 44 | ); 45 | 46 | -------------------------------------------------------------------------------- /templates/addHostgroupHostsModal.html: -------------------------------------------------------------------------------- 1 | {{define "addHostgroupHostsModal"}} 2 | 24 | {{end}} -------------------------------------------------------------------------------- /templates/createCrontabJobModal.html: -------------------------------------------------------------------------------- 1 | {{define "createCrontabJobModal"}} 2 | 47 | {{end}} -------------------------------------------------------------------------------- /templates/createHostgroupModal.html: -------------------------------------------------------------------------------- 1 | {{define "createHostgroupModal"}} 2 | 24 | {{end}} -------------------------------------------------------------------------------- /templates/crontab.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{template "head"}} 5 | 6 | 7 |
    8 | {{template "nav" .}} 9 |
    10 |
    11 |
    12 |
      13 |
    • 14 | ${tree.name} 15 | 16 | 17 |
        18 |
      • 19 | 20 | 21 | ${hg.name} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
          30 |
        • 31 | 32 | 33 | ${h.address} 34 | 35 | 36 | 37 | 38 | 39 |
        • 40 |
        41 |
      • 42 |
      43 |
    • 44 |
    45 |
    46 |
    47 |
    48 |
    49 | 地址:${currHost.address} 50 | 获取状态:${currHost.crontab.status=='successful' ? '成功' : (currHost.crontab.status=='failed' ? '失败' : '')} 51 | 获取时间:${currHost.crontab.updated_at} 52 | 最后一次成功时间:${currHost.crontab.last_succeed} 53 |
    54 | 55 |
    56 | 57 | 64 | 65 |
    66 |
    67 | 本次获取最新crontab信息失败!当前展示的crontab 信息为 ${currHost.crontab.last_succeed} 时获取,可能与主机真实Crontab信息有差异!
    ${currHost.crontab.msg} 68 |
    69 | 70 |
    71 |
    72 |
    73 |
    74 |
    75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 95 | 98 | 101 | 104 | 107 | 111 | 112 | 113 |
    #状态调度用户命令备注操作
    ${jidx + 1} 92 | 启用 93 | 停用 94 | 96 | ${job.slices} 97 | 99 | ${job.user} 100 | 102 | ${job.command} 103 | 105 | ${job.comment} 106 | 108 | 109 | 110 |
    114 |
    115 |
    116 |
    117 |
    118 |
    119 |
    120 |
    121 |
    122 |
    123 | 128 | 129 | 130 | {{template "updateHostModal"}} 131 | {{template "createHostgroupModal"}} 132 | {{template "updateHostgroupModal"}} 133 | {{template "createCrontabJobModal"}} 134 | {{template "updateCrontabJobModal"}} 135 | {{template "addHostgroupHostsModal"}} 136 | {{template "removeHostgroupHostsModal"}} 137 | {{template "footer"}} 138 |
    139 | 140 | 141 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /templates/footer.html: -------------------------------------------------------------------------------- 1 | {{define "footer"}} 2 | 5 | {{end}} -------------------------------------------------------------------------------- /templates/head.html: -------------------------------------------------------------------------------- 1 | {{define "head"}} 2 | 3 | 一个真正的Crontab管理系统 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 44 | {{end}} -------------------------------------------------------------------------------- /templates/host.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{template "head"}} 5 | 6 | 7 |
    8 | {{template "nav"}} 9 | 10 |
    11 |
    12 |
    13 |
    14 | 15 | 16 | 18 | 19 |
    20 |
    21 |
    22 |
    23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | 39 | 40 | 41 | 42 |
    #地址状态
    ${(Number(hostsTable.params.page)-1) * Number(hostsTable.params.pageSize) + idx + 1}${row.address}${row.status}
    43 |
    44 |
    45 |
    46 |
    47 | 当前第${hostsTable.params.page}页,${hostsTable.data.length}条;共${Math.floor(hostsTable.total/Number(hostsTable.params.pageSize))+1}页,${hostsTable.total}条 48 |
    49 |
    50 | 53 | 64 |
    65 |
    66 |
    67 |
    68 | 73 | 74 |
    75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
    基本信息
    地址${currHost.address}
    状态${currHost.status}
    创建时间${currHost.created_at}
    最近更新时间${currHost.updated_at}
    86 | 87 | 88 | 89 | 90 | 91 | 92 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
    计划任务(${currHostJobsTable.total})
    #名称状态描述调度周期创建时间
    ${idx + 1}${row.name}${row.status}${row.description}${row.spec}${row.created_at}
    104 |
    105 |
    106 |
    107 | 108 | {{template "footer"}} 109 |
    110 | 111 | 200 | 270 | -------------------------------------------------------------------------------- /templates/nav.html: -------------------------------------------------------------------------------- 1 | {{define "nav"}} 2 | 36 | {{end}} -------------------------------------------------------------------------------- /templates/operation_record.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{template "head"}} 5 | 6 | 7 |
    8 | {{template "nav" .}} 9 |
    10 |
    11 |
    12 |
    13 | 14 | 15 | 17 | 18 |
    19 |
    20 |
    21 |
    22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
    #来源标识用户类型时间
    ${(Number(recordsTable.params.page)-1) * Number(recordsTable.params.pageSize) + idx + 1}${resourceTypeMp[row.resource_type]}${row.resource_label}${row.user}${operationTypeMp[row.operation_type]}${row.created_at}
    48 |
    49 |
    50 |
    51 |
    52 | 当前第${recordsTable.params.page}页,${recordsTable.data.length}条;共${Math.floor(recordsTable.total/Number(recordsTable.params.pageSize))+1}页,${recordsTable.total}条 53 |
    54 |
    55 | 58 | 69 |
    70 |
    71 |
    72 |
    73 | 78 | 79 |
    80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
    用户${currRecord.user}
    时间${currRecord.created_at}
    来源${resourceTypeMp[currRecord.resource_type]}
    标识${currRecord.resource_label}
    类型${operationTypeMp[currRecord.operation_type]}
    数据
    90 |
    91 |
    92 |
    93 | 94 | {{template "footer"}} 95 |
    96 | 97 | 189 | 268 | -------------------------------------------------------------------------------- /templates/removeHostgroupHostsModal.html: -------------------------------------------------------------------------------- 1 | {{define "removeHostgroupHostsModal"}} 2 | 26 | {{end}} -------------------------------------------------------------------------------- /templates/updateCrontabJobModal.html: -------------------------------------------------------------------------------- 1 | {{define "updateCrontabJobModal"}} 2 | 47 | {{end}} -------------------------------------------------------------------------------- /templates/updateHostModal.html: -------------------------------------------------------------------------------- 1 | {{define "updateHostModal"}} 2 | 24 | {{end}} -------------------------------------------------------------------------------- /templates/updateHostgoupModal.html: -------------------------------------------------------------------------------- 1 | {{define "updateHostgroupModal"}} 2 | 24 | {{end}} --------------------------------------------------------------------------------