// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package dashboards import ( "archive/zip" "errors" "fmt" "io" "io/ioutil" "net/http" "os" "path" "path/filepath" "strconv" "strings" errw "github.com/pkg/errors" "github.com/elastic/beats/libbeat/common" ) // ErrNotFound returned when we cannot find any dashboard to import. type ErrNotFound struct { ErrorString string } // Error returns the human readable error. func (e *ErrNotFound) Error() string { return e.ErrorString } func newErrNotFound(s string, a ...interface{}) *ErrNotFound { return &ErrNotFound{fmt.Sprintf(s, a...)} } // MessageOutputter is a function type for injecting status logging // into this module. type MessageOutputter func(msg string, a ...interface{}) type Importer struct { cfg *Config version common.Version loader Loader } type Loader interface { ImportIndex(file string) error ImportDashboard(file string) error statusMsg(msg string, a ...interface{}) Close() error } func NewImporter(version common.Version, cfg *Config, loader Loader) (*Importer, error) { // Current max version is 6 if version.Major > 6 { version.Major = 6 } return &Importer{ cfg: cfg, version: version, loader: loader, }, nil } // Import imports the Kibana dashboards according to the configuration options. func (imp Importer) Import() error { if imp.cfg.URL != "" || imp.cfg.File != "" { err := imp.ImportArchive() if err != nil { return errw.Wrap(err, "Error importing URL/file") } } else { err := imp.ImportKibanaDir(imp.cfg.Dir) if err != nil { return errw.Wrapf(err, "Error importing directory %s", imp.cfg.Dir) } } return nil } func (imp Importer) ImportDashboard(file string) error { imp.loader.statusMsg("Import dashboard %s", file) return imp.loader.ImportDashboard(file) } func (imp Importer) ImportFile(fileType string, file string) error { imp.loader.statusMsg("Import %s from %s", fileType, file) if fileType == "dashboard" { return imp.loader.ImportDashboard(file) } else if fileType == "index-pattern" { return imp.loader.ImportIndex(file) } return fmt.Errorf("Unexpected file type %s", fileType) } func (imp Importer) ImportDir(dirType string, dir string) error { imp.loader.statusMsg("Import directory %s", dir) dir = path.Join(dir, dirType) var errors []string files, err := filepath.Glob(path.Join(dir, "*.json")) if err != nil { return fmt.Errorf("Failed to read directory %s. Error: %s", dir, err) } if len(files) == 0 { return fmt.Errorf("The directory %s is empty, nothing to import", dir) } for _, file := range files { err = imp.ImportFile(dirType, file) if err != nil { errors = append(errors, fmt.Sprintf(" error loading %s: %s", file, err)) } } if len(errors) > 0 { return fmt.Errorf("Failed to load directory %s:\n%s", dir, strings.Join(errors, "\n")) } return nil } func (imp Importer) unzip(archive, target string) error { imp.loader.statusMsg("Unzip archive %s", target) reader, err := zip.OpenReader(archive) if err != nil { return err } defer reader.Close() // Closure to close the files on each iteration unzipFile := func(file *zip.File) error { filePath := filepath.Join(target, file.Name) // check that the resulting file path is indeed under target // Note that Rel calls Clean. relPath, err := filepath.Rel(target, filePath) if err != nil { return err } if strings.HasPrefix(filepath.ToSlash(relPath), "../") { return fmt.Errorf("Zip file contains files outside of the target directory: %s", relPath) } if file.FileInfo().IsDir() { return os.MkdirAll(filePath, file.Mode()) } if err = os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { return fmt.Errorf("failed making directory for file %v: %v", filePath, err) } fileReader, err := file.Open() if err != nil { return err } defer fileReader.Close() targetFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) if err != nil { return err } defer targetFile.Close() if _, err := io.Copy(targetFile, fileReader); err != nil { return err } return nil } for _, file := range reader.File { err := unzipFile(file) if err != nil { return err } } return nil } func (imp Importer) ImportArchive() error { var archive string target, err := ioutil.TempDir("", "tmp") if err != nil { return fmt.Errorf("Failed to generate a temporary directory name: %v", err) } if err = os.MkdirAll(target, 0755); err != nil { return fmt.Errorf("Failed to create a temporary directory %s: %v", target, err) } defer os.RemoveAll(target) // clean up imp.loader.statusMsg("Created temporary directory %s", target) if imp.cfg.File != "" { archive = imp.cfg.File } else if imp.cfg.URL != "" { archive, err = imp.downloadFile(imp.cfg.URL, target) if err != nil { return fmt.Errorf("Failed to download file: %s. Error: %v", imp.cfg.URL, err) } } else { return errors.New("No archive file or URL is set - please use -file or -url option") } err = imp.unzip(archive, target) if err != nil { return fmt.Errorf("Failed to unzip the archive: %s: %v", archive, err) } dirs, err := getDirectories(target) if err != nil { return err } if len(dirs) != 1 { return fmt.Errorf("Too many directories under %s", target) } dirs, err = getDirectories(dirs[0]) if err != nil { return err } for _, dir := range dirs { imp.loader.statusMsg("Importing Kibana from %s", dir) if imp.cfg.Beat == "" || filepath.Base(dir) == imp.cfg.Beat { err = imp.ImportKibanaDir(dir) if err != nil { return err } } } return nil } func getDirectories(target string) ([]string, error) { files, err := ioutil.ReadDir(target) if err != nil { return nil, err } var dirs []string for _, file := range files { if file.IsDir() { dirs = append(dirs, filepath.Join(target, file.Name())) } } return dirs, nil } func (imp Importer) downloadFile(url string, target string) (string, error) { fileName := filepath.Base(url) targetPath := path.Join(target, fileName) imp.loader.statusMsg("Downloading %s", url) // Create the file out, err := os.Create(targetPath) if err != nil { return targetPath, err } defer out.Close() // Get the data resp, err := http.Get(url) if err != nil { return targetPath, err } if resp.StatusCode != 200 { return targetPath, fmt.Errorf("Server returned: %s", resp.Status) } defer resp.Body.Close() // Writer the body to file _, err = io.Copy(out, resp.Body) if err != nil { return targetPath, err } return targetPath, nil } // import Kibana dashboards and index-pattern or only one of these func (imp Importer) ImportKibanaDir(dir string) error { var err error versionPath := strconv.Itoa(imp.version.Major) dir = path.Join(dir, versionPath) imp.loader.statusMsg("Importing directory %v", dir) if _, err := os.Stat(dir); err != nil { return newErrNotFound("No directory %s", dir) } check := []string{} if !imp.cfg.OnlyDashboards { check = append(check, "index-pattern") } wantDashboards := false if !imp.cfg.OnlyIndex { check = append(check, "dashboard") wantDashboards = true } types := []string{} for _, c := range check { if imp.subdirExists(dir, c) { types = append(types, c) } } if len(types) == 0 { return newErrNotFound("The directory %s does not contain the %s subdirectory."+ " There is nothing to import into Kibana.", dir, strings.Join(check, " or ")) } importDashboards := false for _, t := range types { err = imp.ImportDir(t, dir) if err != nil { return fmt.Errorf("Failed to import %s: %v", t, err) } if t == "dashboard" { importDashboards = true } } if wantDashboards && !importDashboards { return newErrNotFound("No dashboards to import. Please make sure the %s directory "+ "contains a dashboard directory.", dir) } return nil } func (imp Importer) subdirExists(parent string, child string) bool { if _, err := os.Stat(path.Join(parent, child)); err != nil { return false } return true }