├── templates └── rebalance.conf ├── README.md └── sleepwalk.go /templates/rebalance.conf: -------------------------------------------------------------------------------- 1 | 08:00-16:00 2 | { "transient" : { "cluster.routing.allocation.cluster_concurrent_rebalance" : 3 } } 3 | 16:00-08:00 4 | { "transient" : { "cluster.routing.allocation.cluster_concurrent_rebalance" : 15 } } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sleepwalk 2 | 3 | Sleepwalk is a tool to schedule ElasticSearch settings using a simple template system consisting of time range and setting pairs: 4 | 5 | ``` 6 | 08:00-16:00 7 | { "transient": { "cluster.routing.allocation.cluster_concurrent_rebalance": 3 } } 8 | 16:00-08:00 9 | { "transient": { "cluster.routing.allocation.cluster_concurrent_rebalance": 15 } } 10 | ``` 11 | 12 | A single template can hold any number of time and setting pairs, typically with each template representing a related configuration bundle (e.g. only allow 3 shard rebalances during the day, but 15 over night). 13 | 14 | Templates (file formatted accordingly and ending in .conf) are picked up from the specified `-templates` directory on start. Every `-interval` seconds, each template is validated and any settings that are applicable according to the current time are applied in top-down order. 15 | 16 | Time ranges are interpreted to span days. Setting 08:00-16:00 will span 8AM - 4PM of each day, while 16:00-08:00 will span 4PM until 8AM the next day. 17 | 18 | Template files receive basic validation to ensure you've entered syntactically correct time ranges and valid json settings, but doesn't prevent you from setting things like an hour value of 25 or making nonsense API calls to ElasticSearch. 19 | 20 | ``` 21 | Usage of ./sleepwalk: 22 | -address string 23 | ElasticSearch Address (default "http://localhost:9200") 24 | -interval int 25 | Update interval in seconds (default 300) 26 | -templates string 27 | Template path (default "./templates") 28 | ``` 29 | 30 | ``` 31 | 2015/10/28 10:42:31 Sleepwalk Running 32 | 2015/10/28 10:42:31 Reading template: rebalance.conf 33 | 2015/10/28 10:42:31 Pushing setting from template: rebalance.conf. Current settings: {"persistent":{},"transient":{"cluster":{"routing":{"allocation":{"cluster_concurrent_rebalance":"0"}}}}} 34 | 2015/10/28 10:42:31 New settings: {"persistent":{},"transient":{"cluster":{"routing":{"allocation":{"cluster_concurrent_rebalance":"3"}}}}} 35 | ``` 36 | -------------------------------------------------------------------------------- /sleepwalk.go: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Jamie Alquiza 4 | // 5 | // http://knowyourmeme.com/memes/deal-with-it. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | package main 25 | 26 | import ( 27 | "bufio" 28 | "encoding/json" 29 | "flag" 30 | "fmt" 31 | "io/ioutil" 32 | "log" 33 | "net/http" 34 | "os" 35 | "regexp" 36 | "strings" 37 | "time" 38 | ) 39 | 40 | var SleepwalkSettings struct { 41 | address string 42 | interval int 43 | templates string 44 | } 45 | 46 | var ( 47 | // Template naming scheme. 48 | templateFileName *regexp.Regexp = regexp.MustCompile(".conf$") 49 | templateDateRange *regexp.Regexp = regexp.MustCompile("[0-9]{2}") 50 | ) 51 | 52 | // An ElasticSearch cluster setting and timestamp describing 53 | // a start time for the setting to go into effect. 54 | type Setting struct { 55 | StartHH, StartMM, EndHH, EndMM string 56 | Value string 57 | } 58 | 59 | func init() { 60 | flag.StringVar(&SleepwalkSettings.address, "address", "http://localhost:9200", "ElasticSearch Address") 61 | flag.IntVar(&SleepwalkSettings.interval, "interval", 300, "Update interval in seconds") 62 | flag.StringVar(&SleepwalkSettings.templates, "templates", "./templates", "Template path") 63 | flag.Parse() 64 | } 65 | 66 | // getSettings fetches the current ElasticSearch cluster settings. 67 | func getSettings() (string, error) { 68 | resp, err := http.Get(SleepwalkSettings.address + "/_cluster/settings") 69 | if err != nil { 70 | return "", fmt.Errorf("Error getting settings: %s", err) 71 | } 72 | 73 | contents, _ := ioutil.ReadAll(resp.Body) 74 | resp.Body.Close() 75 | 76 | return string(contents), nil 77 | } 78 | 79 | // putSettings pushes a cluster setting to ElasticSearch. 80 | func putSettings(setting string) (string, error) { 81 | client := &http.Client{} 82 | 83 | req, err := http.NewRequest("PUT", SleepwalkSettings.address+"/_cluster/settings", strings.NewReader(setting)) 84 | if err != nil { 85 | return "", fmt.Errorf("Request error: %s", err) 86 | } 87 | 88 | r, err := client.Do(req) 89 | if err != nil { 90 | return "", fmt.Errorf("Error pushing settings: %s", err) 91 | } 92 | 93 | resp, _ := ioutil.ReadAll(r.Body) 94 | r.Body.Close() 95 | 96 | return string(resp), nil 97 | } 98 | 99 | // parseTsRange takes a 09:30-15:30 format start / end time range 100 | // and reteurns the start HH, start MM, end HH, end MM elements. 101 | func parseTsRange(tsrange string) (string, string, string, string) { 102 | // Break start / stop times. 103 | r := strings.Split(tsrange, "-") 104 | // Get start elements. 105 | start := strings.Split(r[0], ":") 106 | // Get end elements. 107 | end := strings.Split(r[1], ":") 108 | 109 | return start[0], start[1], end[0], end[1] 110 | } 111 | 112 | // validateSettings does a basic validation of each time range and 113 | // setting pair from a template. It ensures that 00:00 times were received 114 | // and that the setting string is at least valid json. 115 | func validateSetting(setting Setting, i int) (int, bool) { 116 | // Validate start/end HH/MM. 117 | // Needs to do something smarter than just a /[0-9]{2}/ match. 118 | switch { 119 | case !templateDateRange.MatchString(setting.StartHH): 120 | return i + 1, false 121 | case !templateDateRange.MatchString(setting.StartMM): 122 | return i + 1, false 123 | case !templateDateRange.MatchString(setting.EndHH): 124 | return i + 1, false 125 | case !templateDateRange.MatchString(setting.EndMM): 126 | return i + 1, false 127 | } 128 | 129 | null := make(map[string]interface{}) 130 | if err := json.Unmarshal([]byte(setting.Value), &null); err != nil { 131 | return i + 2, false 132 | } 133 | 134 | return i, true 135 | } 136 | 137 | // parseTemplate reads a Sleepwalk settings template and returns an array of 138 | // Setting structs. 139 | func parseTemplate(template string) ([]Setting, error) { 140 | settings := []Setting{} 141 | 142 | f, err := os.Open(SleepwalkSettings.templates + "/" + template) 143 | if err != nil { 144 | return settings, fmt.Errorf("Template error: %s", err) 145 | } 146 | defer f.Close() 147 | 148 | lines := []string{} 149 | scanner := bufio.NewScanner(f) 150 | 151 | for scanner.Scan() { 152 | lines = append(lines, scanner.Text()) 153 | } 154 | 155 | // No safeties yet. Assumes that the template is a perfectly formatted 156 | // time range and associated setting in alternating lines. 157 | for i := 0; i < len(lines); i = i + 2 { 158 | s := Setting{} 159 | // Get value (the setting) from the template. 160 | s.Value = lines[i+1] 161 | // Get time range. 162 | s.StartHH, s.StartMM, s.EndHH, s.EndMM = parseTsRange(lines[i]) 163 | 164 | // Validate setting. We have to pass the index i we are on to 165 | // determine the line the failed validation. 166 | if line, valid := validateSetting(s, i); valid { 167 | settings = append(settings, s) 168 | } else { 169 | return settings, fmt.Errorf("Template parsing error from %s:%d", 170 | template, line) 171 | } 172 | } 173 | 174 | return settings, nil 175 | } 176 | 177 | // getTs takes HH:MM pairs and a reference timestamp (for current date-time and zone) 178 | // and returns a formatted time.Time stamp. 179 | func getTs(hh, mm string, ref time.Time) (time.Time, error) { 180 | tz, _ := time.Now().Zone() 181 | tsString := fmt.Sprintf("%d-%02d-%02d %s:%s %s", 182 | ref.Year(), ref.Month(), ref.Day(), hh, mm, tz) 183 | 184 | ts, err := time.Parse("2006-01-02 15:04 MST", tsString) 185 | if err != nil { 186 | return ts, err 187 | } 188 | 189 | return ts, nil 190 | } 191 | 192 | // applyTemplate parses a template file and applies each setting. 193 | func applyTemplate(template string) { 194 | log.Printf("Reading template: %s\n", template) 195 | 196 | settings, err := parseTemplate(template) 197 | if err != nil { 198 | log.Println(err) 199 | os.Exit(1) 200 | } 201 | 202 | now := time.Now() 203 | // Count of how many settings 204 | // were applied from this template. 205 | applied := 0 206 | 207 | // We have a bunch of Setting structs from our template. 208 | for i := range settings { 209 | // Should probably move this into parseTemplate() and 210 | // just have start / end fields in the setting struct. 211 | start, _ := getTs(settings[i].StartHH, settings[i].StartMM, now) 212 | end, _ := getTs(settings[i].EndHH, settings[i].EndMM, now) 213 | 214 | // Check if the time range is intended to span overnight and 215 | // reference more than one day. 216 | // Is end an earlier time than start? If so, we span day boundaries. 217 | if start.After(end) { 218 | switch { 219 | // If now is before end, start needs to reference yesterday. 220 | case now.Before(end): 221 | start = start.AddDate(0, 0, -1) 222 | // Otherwise if now is after end, end needs to reference tomorrow. 223 | default: 224 | end = end.AddDate(0, 0, 1) 225 | } 226 | } 227 | 228 | if now.After(start) && now.Before(end) { 229 | cSettings, err := getSettings() 230 | if err != nil { 231 | log.Println(err) 232 | } 233 | 234 | log.Printf("Pushing setting from template: %s\n", template) 235 | 236 | _, err = putSettings(settings[i].Value) 237 | if err != nil { 238 | log.Println(err) 239 | } 240 | applied++ 241 | 242 | nSettings, err := getSettings() 243 | if err != nil { 244 | log.Println(err) 245 | } 246 | 247 | if cSettings != nSettings { 248 | log.Printf("Settings changed from %s to %s\n", cSettings, nSettings) 249 | } else { 250 | log.Printf("No settings changed") 251 | } 252 | } 253 | } 254 | 255 | if applied < 1 { 256 | log.Printf("No settings to apply from: %s\n", template) 257 | } 258 | } 259 | 260 | // getTemplates returns a list of template files 261 | // from the template path. 262 | func getTemplates(path string) []string { 263 | templates := []string{} 264 | fs, _ := ioutil.ReadDir(path) 265 | 266 | for _, f := range fs { 267 | if templateFileName.MatchString(f.Name()) { 268 | templates = append(templates, f.Name()) 269 | } 270 | } 271 | 272 | return templates 273 | } 274 | 275 | func main() { 276 | log.Println("Sleepwalk Running") 277 | 278 | templates := getTemplates(SleepwalkSettings.templates) 279 | for _, t := range templates { 280 | applyTemplate(t) 281 | } 282 | 283 | run := time.Tick(time.Duration(SleepwalkSettings.interval) * time.Second) 284 | for _ = range run { 285 | for _, t := range templates { 286 | applyTemplate(t) 287 | } 288 | } 289 | } 290 | --------------------------------------------------------------------------------