├── test ├── __init__.py ├── Makefile.test ├── Makefile ├── README.md ├── TestRsrcs.py ├── test_createErrors.py ├── TestDashboards.py ├── Dockerfiles │ └── centos7 │ │ └── Dockerfile ├── TestAlerts.py ├── test_match.py ├── test_pullWithUnnormalizedPath.py ├── fixtures │ ├── README.md │ └── create_test_fixtures.py ├── test_pullIntoVanillaDir.py ├── test_pullWithDeletedRsrcs.py ├── test_configErrors.py ├── test_config.py ├── test_showErrors.py ├── test_create.py ├── test_pushErrors.py ├── test_pullIntoRepoDir.py └── test_show.py ├── doc ├── bin │ ├── README.md │ ├── alertToTemplate.sh │ ├── wavectl │ └── mutateDashboards.py ├── sphinx │ ├── AutoGeneratedHeader.txt │ ├── index.md │ ├── CommandReference.md │ ├── Makefile │ ├── BrowserIntegration.md │ ├── WavectlConfig.md │ ├── AdvancedGrep.md │ ├── conf.py │ ├── RepetitiveEditing.md │ ├── CommandLine.md │ ├── README.md │ ├── GitIntegration.md │ └── Templating.md ├── BrowserIntegration.md ├── WavectlConfig.md ├── AdvancedGrep.md ├── Templating.md ├── CommandLine.md ├── GitIntegration.md └── RepetitiveEditing.md ├── .gitmodules ├── wavectl ├── main.py ├── __init__.py ├── ResourceFactory.py ├── GitUtils.py ├── Config.py ├── Push.py ├── Create.py ├── Dashboard.py ├── wavectl.py ├── BaseCommand.py ├── Show.py ├── Mutator.py ├── Alert.py └── Resource.py ├── setup.cfg ├── .gitignore ├── requirements.txt ├── CHANGELOG.md ├── setup.py ├── .circleci └── config.yml ├── CONTRIBUTING.md ├── README.md └── LICENSE /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/Makefile.test: -------------------------------------------------------------------------------- 1 | .Makefile.test/Makefile.test -------------------------------------------------------------------------------- /doc/bin/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | This directory contains executables that are used in documentation generation. 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/.Makefile.test"] 2 | path = test/.Makefile.test 3 | url = https://github.com/box/Makefile.test.git 4 | -------------------------------------------------------------------------------- /wavectl/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import print_function 3 | 4 | from wavectl import Wavectl 5 | 6 | 7 | def main(): 8 | wf = Wavectl() 9 | wf.runCmd() 10 | 11 | 12 | if __name__ == '__main__': 13 | main() 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled files 2 | *.py[cod] 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | build/* 8 | dist/* 9 | 10 | # Mac noise 11 | .DS_Store 12 | 13 | # Intermediate files created by Makefile.test 14 | **/.makefile_test_*ed_tests 15 | 16 | doc/sphinx/_build 17 | -------------------------------------------------------------------------------- /doc/sphinx/AutoGeneratedHeader.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argcomplete==1.9.3 2 | certifi==2017.11.5 3 | chardet==3.0.4 4 | future==0.16.0 5 | gitdb2==2.0.3 6 | GitPython==2.1.8 7 | idna==2.6 8 | mock==2.0.0 9 | python-dateutil==2.6.1 10 | recommonmark==0.4.0 11 | six==1.11.0 12 | smmap2==2.0.3 13 | Sphinx==1.7.4 14 | sphinxcontrib-programoutput==0.11 15 | termcolor==1.1.0 16 | urllib3==1.24.2 17 | wavefront-api-client==2.3.1 18 | -------------------------------------------------------------------------------- /wavectl/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import absolute_import 3 | from .wavectl import Wavectl 4 | 5 | from .Config import ConfigCommand 6 | from .BaseCommand import ConfigError 7 | 8 | from .BaseWavefrontCommand import Error 9 | 10 | from .Show import ShowError 11 | from .Show import ShowCommand 12 | 13 | from .Pull import PullError 14 | from .Pull import PullCommand 15 | 16 | from .Mutator import MutatorError 17 | from .Push import PushCommand 18 | from .Create import CreateCommand 19 | -------------------------------------------------------------------------------- /doc/sphinx/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ```eval_rst 6 | .. toctree:: 7 | :hidden: 8 | 9 | AdvancedGrep.md 10 | BrowserIntegration.md 11 | CommandLine.md 12 | CommandReference.md 13 | GitIntegration.md 14 | README.md 15 | RepetitiveEditing.md 16 | Templating.md 17 | WavectlConfig.md 18 | ``` 19 | 20 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | 2 | TESTS ?= \ 3 | test_config.py \ 4 | test_configErrors.py \ 5 | test_create.py \ 6 | test_createErrors.py \ 7 | test_match.py \ 8 | test_pull.py \ 9 | test_pullIntoRepoDir.py \ 10 | test_pullIntoVanillaDir.py \ 11 | test_pullWithDeletedRsrcs.py \ 12 | test_pullWithUnnormalizedPath.py \ 13 | test_pullErrors.py \ 14 | test_push.py \ 15 | test_pushErrors.py \ 16 | test_show.py \ 17 | test_showErrors.py 18 | 19 | MAKEFILE_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 20 | include $(MAKEFILE_DIR)/Makefile.test 21 | 22 | -------------------------------------------------------------------------------- /wavectl/ResourceFactory.py: -------------------------------------------------------------------------------- 1 | 2 | import argparse 3 | 4 | from .Resource import Resource 5 | from .Alert import Alert 6 | from .Dashboard import Dashboard 7 | 8 | 9 | class ResourceFactory(object): 10 | 11 | @staticmethod 12 | def resourceTypeFromString(s): 13 | """ Parse the given string and return the resource type""" 14 | if s == "alert": 15 | return Alert 16 | if s == "dashboard": 17 | return Dashboard 18 | else: 19 | raise argparse.ArgumentTypeError( 20 | "Cannot parse string {0} for resourceType") 21 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Tests 3 | 4 | `wavectl` project primarily uses 5 | [unittest](https://docs.python.org/2/library/unittest.html) framework from the 6 | python standard library. 7 | 8 | [unittest.mock](https://docs.python.org/3/library/unittest.mock.html) is also 9 | heavily utilized. While testing the `wavectl` command line tool, we do not want 10 | to reach out to a running Wavefront remote server. Instead, the tests read the 11 | json blobs of wavefront resources from the `fixtures` directory and "mock" the 12 | behavior of a wavefront server. 13 | 14 | For executing test executables [Makefile.test](https://github.com/box/Makefile.test) 15 | is used. 16 | 17 | -------------------------------------------------------------------------------- /test/TestRsrcs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Alert state used for tests. 4 | 5 | 6 | from __future__ import absolute_import 7 | from __future__ import print_function 8 | 9 | import json 10 | 11 | 12 | class Rsrc(object): 13 | """ Base class for alert dashboard, etc data used for tests """ 14 | 15 | @staticmethod 16 | def getTestRsrcs(fileName): 17 | """Get the resources in the passed json file and return them as a list""" 18 | # TODO: These file lookups should be cached. 19 | with open(fileName) as f: 20 | rv = json.load(f) 21 | return rv 22 | 23 | @staticmethod 24 | def appendToKey(s, k, d): 25 | """Append the string s to the name key in the dictionary d""" 26 | d[k] = d[k] + s 27 | return d 28 | -------------------------------------------------------------------------------- /test/test_createErrors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This file contains negative tests for the push command. 4 | 5 | from __future__ import absolute_import 6 | from __future__ import print_function 7 | import os 8 | import sys 9 | # Extend that path so that we can import the module under test. 10 | sys.path.insert(0, os.path.abspath('..')) 11 | 12 | # Design under test. 13 | import wavectl 14 | 15 | import unittest 16 | import tempfile 17 | import git 18 | import shutil 19 | import copy 20 | import json 21 | import time 22 | 23 | import util 24 | 25 | 26 | class Test(util.TestPullMutate): 27 | """ A class that checks for error conditions in the `create` command""" 28 | 29 | 30 | if __name__ == '__main__': 31 | util.unittestMain() 32 | 33 | # Test Plan 34 | # Try to create an existing resource. The user should use update(push for now) 35 | # instead 36 | -------------------------------------------------------------------------------- /test/TestDashboards.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Alert state used for tests. 4 | 5 | 6 | from __future__ import absolute_import 7 | from __future__ import print_function 8 | 9 | import json 10 | import TestRsrcs 11 | 12 | 13 | class Dashboard(TestRsrcs.Rsrc): 14 | """ A class that encompasses some dashboard data used for tests """ 15 | 16 | kubeBoxPkiNameMatchString = "SomeNameToBeMachedInKubeBoxPki" 17 | skynetMonitoringNameMatchString = "SomeMatchString" 18 | 19 | @staticmethod 20 | def getTestDashboards(): 21 | dashboards = TestRsrcs.Rsrc.getTestRsrcs( 22 | "fixtures/TestDashboards.json") 23 | Dashboard.kubeBoxPki = TestRsrcs.Rsrc.appendToKey( 24 | Dashboard.kubeBoxPkiNameMatchString, 25 | "name", 26 | dashboards[0]) 27 | Dashboard.skynetMonitoring = TestRsrcs.Rsrc.appendToKey( 28 | Dashboard.skynetMonitoringNameMatchString, 29 | "name", 30 | dashboards[1]) 31 | Dashboard.skynetApplier = dashboards[2] 32 | return dashboards 33 | -------------------------------------------------------------------------------- /test/Dockerfiles/centos7/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM openshift/base-centos7 3 | 4 | # We will install a newer version of git. Remove the old one. 5 | RUN yum erase -y git 6 | 7 | # Install both python2 and 3 and other required tools. 8 | # The tools are mostly for tests, documentation generation to run. 9 | RUN yum install -y epel-release \ 10 | && yum-config-manager --add-repo https://copr.fedorainfracloud.org/coprs/randomvariable/jsonnet/repo/epel-7/randomvariable-jsonnet-epel-7.repo \ 11 | && yum install -y jsonnet \ 12 | && yum install -y https://centos7.iuscommunity.org/ius-release.rpm \ 13 | && yum install -y python-pip python36u python36u-pip jq git2u \ 14 | && wget https://github.com/jgm/pandoc/releases/download/2.2.1/pandoc-2.2.1-linux.tar.gz \ 15 | && tar xvzf pandoc-2.2.1-linux.tar.gz --strip-components 1 -C /usr/local/ 16 | 17 | 18 | # Turn off ssh host key checking. Avoid yes/no prompts for user input while circle 19 | # ci is reaching out to github 20 | RUN echo $'Host * \n\ 21 | StrictHostKeyChecking no \n\ 22 | UserKnownHostsFile=/dev/null' >> /etc/ssh/ssh_config 23 | 24 | -------------------------------------------------------------------------------- /wavectl/GitUtils.py: -------------------------------------------------------------------------------- 1 | 2 | # A common file to inlcude some frequently used git related tasks 3 | 4 | from __future__ import absolute_import 5 | from __future__ import print_function 6 | import argparse 7 | import os 8 | import json 9 | import time 10 | import datetime 11 | import sys 12 | import tempfile 13 | import shutil 14 | import logging 15 | 16 | # External dependencies: 17 | import git 18 | 19 | 20 | class GitUtils(object): 21 | @staticmethod 22 | def getExistingRepo(p): 23 | """ Verify that the given path is inside a git repository""" 24 | try: 25 | r = git.Repo(p, search_parent_directories=True) 26 | except git.exc.InvalidGitRepositoryError as e: 27 | print("""The existing path {0} is not in a git repository. The 28 | ` --inGit` expects to find 29 | in a git repository. The saved resource files should 30 | be source controlled in git. If you are trying to start a new repo 31 | from scratch, please pass a non-existing path to the 32 | command""".format(p)) 33 | raise 34 | 35 | return r 36 | -------------------------------------------------------------------------------- /test/TestAlerts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Alert state used for tests. 4 | 5 | 6 | from __future__ import absolute_import 7 | from __future__ import print_function 8 | 9 | import json 10 | 11 | import TestRsrcs 12 | 13 | 14 | class Alert(TestRsrcs.Rsrc): 15 | """ A class that encompasses some alert data used for tests """ 16 | 17 | matchPattern = r"TODO_SOME_TEST_\d+_STRING" 18 | matchString = "TODO_SOME_TEST_12432345_STRING" 19 | 20 | @staticmethod 21 | def getTestAlerts(): 22 | alerts = TestRsrcs.Rsrc.getTestRsrcs("fixtures/TestAlerts.json") 23 | 24 | # Cherry-pick and modify some test alerts to generate better test 25 | # patterns 26 | Alert.kubernetesSkynetTag = alerts[0] 27 | Alert.kubernetesTag = alerts[1] 28 | Alert.skynetTag = alerts[2] 29 | Alert.nameMatch = TestRsrcs.Rsrc.appendToKey( 30 | Alert.matchString, 31 | "name", 32 | alerts[4]) 33 | Alert.additionalInformationMatch = TestRsrcs.Rsrc.appendToKey( 34 | Alert.matchString, 35 | "additionalInformation", 36 | alerts[7]) 37 | return alerts 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented in this file. The 4 | format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and 5 | this project adheres to [Semantic Versioning](http://semver.org). 6 | 7 | ## [Unreleased] 8 | ### Summary 9 | TODO: Not complete yet TBD 10 | 11 | #### Added 12 | - First part of the Templating tutorial 13 | 14 | #### Fixed 15 | - The Wavefront apiToken secret was leaking into the git repo commit messages in 16 | git integration mode. [#23](https://github.com/box/wavectl/issues/23) 17 | - Recompiling all documentation (/doc/sphinx$ make all) does not 18 | overwrite the ~/.wavectl/config file with dummy values anymore. 19 | - Commits executed in wavectl do not execute post-commit hooks of the user 20 | anymore. The user can have all kinds of commit hooks. Wavectl should be skipping 21 | them. 22 | 23 | 24 | ## Supported Release [0.2.0] 25 | ### Summary 26 | Initial release of the `wavectl` project 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /doc/bin/alertToTemplate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # In this script we modify a json representation of an alert into a jsonnet 4 | # template file. 5 | 6 | # Finally we also execute jsonnet to make sure the final file is syntactically 7 | # correct. 8 | 9 | set -x 10 | set -e 11 | 12 | in_dir=/tmp/Templating/alerts 13 | out_dir=/tmp/Templating/alertTemplates 14 | rm -fr ${out_dir} 15 | mkdir -p ${out_dir} 16 | 17 | for in_file in $(ls ${in_dir}/*.alert); 18 | do 19 | base_name=$(basename -- ${in_file}) 20 | file_name="${base_name%.*}" 21 | out_file=${out_dir}/${file_name}.jsonnet 22 | 23 | cp -f ${in_file} ${out_file} 24 | 25 | sed -i 's/collections-service-dev/"+std.extVar("namespace")+"/g' ${out_file} 26 | sed -i 's/Collections Dev/"+std.extVar("namespace")+"/g' ${out_file} 27 | sed -i 's/"collections-service"/std.extVar("teamTag")/g' ${out_file} 28 | sed -i 's/"core-frameworks"/std.extVar("parentTeamTag")/g' ${out_file} 29 | sed -i 's/"pd: .*"/std.extVar("pagerDutyKey")/g' ${out_file} 30 | 31 | jsonnet fmt -i ${out_file} 32 | jsonnet --ext-str namespace=new-team-namespace \ 33 | --ext-str teamTag=new-team-tag \ 34 | --ext-str parentTeamTag=same-parent-team-tag \ 35 | --ext-str pagerDutyKey=newteampagerdutykey \ 36 | ${out_file} 37 | done 38 | 39 | -------------------------------------------------------------------------------- /doc/sphinx/CommandReference.md: -------------------------------------------------------------------------------- 1 | 2 | \# wavectl command reference 3 | 4 | 5 | \#\# Global Options 6 | 7 | ```eval_rst 8 | .. program-output:: python -m wavectl.main --help 9 | :returncode: 0 10 | ``` 11 | 12 | \#\# Subcommand Options 13 | 14 | \#\#\# Config Options 15 | 16 | ```eval_rst 17 | .. program-output:: python -m wavectl.main config --help 18 | :returncode: 0 19 | ``` 20 | 21 | 22 | \#\#\# Show Options 23 | 24 | ```eval_rst 25 | .. program-output:: python -m wavectl.main show --help 26 | :returncode: 0 27 | ``` 28 | 29 | \#\#\# Pull Options 30 | 31 | ```eval_rst 32 | .. program-output:: python -m wavectl.main pull --help 33 | :returncode: 0 34 | ``` 35 | 36 | \#\#\# Push Options 37 | 38 | ```eval_rst 39 | .. program-output:: python -m wavectl.main push --help 40 | :returncode: 0 41 | ``` 42 | 43 | \#\#\# Create Options 44 | 45 | ```eval_rst 46 | .. program-output:: python -m wavectl.main create --help 47 | :returncode: 0 48 | ``` 49 | 50 | \#\# Resource Options 51 | 52 | \#\#\# For Alerts 53 | 54 | ```eval_rst 55 | .. program-output:: python -m wavectl.main push '' alert --help 56 | :returncode: 0 57 | ``` 58 | 59 | \#\#\# For Dashboards 60 | 61 | ```eval_rst 62 | .. program-output:: python -m wavectl.main push '' dashboard --help 63 | :returncode: 0 64 | ``` 65 | 66 | -------------------------------------------------------------------------------- /test/test_match.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This file contains positive tests for regular expression matching/filtering of 4 | # alerts. 5 | 6 | from __future__ import absolute_import 7 | from __future__ import print_function 8 | import os 9 | import sys 10 | # Extend that path so that we can import the module under test. 11 | sys.path.insert(0, os.path.abspath('..')) 12 | 13 | import wavectl 14 | 15 | import unittest 16 | import shutil 17 | import datetime 18 | import tempfile 19 | import time 20 | import copy 21 | 22 | import git 23 | 24 | import util 25 | 26 | 27 | class Test(unittest.TestCase): 28 | """ A test suite for the regex matching comparisons in the wavectl command. 29 | Wavectl provides bunch of options to filter the alerts. In this type 30 | we excerise that code.""" 31 | 32 | def test_match(self): 33 | # testData = [ 34 | # (), 35 | # ] 36 | pass 37 | 38 | # TestPlan 39 | # 1) Add a test for a selector but the selector does not exist for a particular 40 | # resource. For example the command line client has specified --updaterId 41 | # as a selector. However some resources do not have that key. 42 | # If the key does not exist, we should not throw an exception but not select 43 | # the resource. 44 | 45 | 46 | if __name__ == '__main__': 47 | util.unittestMain() 48 | -------------------------------------------------------------------------------- /doc/bin/wavectl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # A light wrapper around the actual wavectl command. It uses the mock test 4 | # resources. This is called from documentation source files during compile time. 5 | # The output of this script gets inserted into code snippets in the generated 6 | # documentation. 7 | 8 | from __future__ import absolute_import 9 | from __future__ import print_function 10 | 11 | import os 12 | import sys 13 | fileDir = os.path.dirname(os.path.abspath(__file__)) 14 | rootDir = os.path.realpath(os.path.join(fileDir, "..", "..")) 15 | testDir = os.path.join(rootDir, "test") 16 | sys.path.insert(0, rootDir) 17 | sys.path.insert(0, testDir) 18 | os.chdir(testDir) 19 | 20 | import argparse 21 | import wavectl 22 | import test.util 23 | 24 | import six 25 | if six.PY3: 26 | import unittest.mock as mock 27 | else: 28 | import mock 29 | 30 | import webbrowser 31 | 32 | 33 | def main(): 34 | 35 | testAlerts = test.util.TestAlerts.Alert.getTestAlerts() 36 | testDashboards = test.util.TestDashboards.Dashboard.getTestDashboards() 37 | test.util.mockRsrcType(wavectl.Alert.Alert, testAlerts, []) 38 | test.util.mockRsrcType(wavectl.Dashboard.Dashboard, testDashboards, []) 39 | 40 | # Overwrite the webbrowser open functions. During documentation generation 41 | # we do not want to launch a browser 42 | m = mock.Mock() 43 | webbrowser.open_new = m 44 | webbrowser.open_new_tab = m 45 | 46 | # Consume the command name and pass down the command line args 47 | wf = wavectl.Wavectl(designForTestArgv=sys.argv[1:]) 48 | wf.runCmd() 49 | 50 | 51 | if __name__ == "__main__": 52 | main() 53 | -------------------------------------------------------------------------------- /wavectl/Config.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | 6 | import os 7 | import json 8 | 9 | from .BaseCommand import BaseCommand 10 | from builtins import input 11 | 12 | 13 | class ConfigCommand(BaseCommand): 14 | """ A class implementing the `config .....` command. It is mainly used 15 | for saving the host name, api key and the default tag.""" 16 | 17 | def handleCmd(self, args): 18 | """Ask for some input from the user. Get the wavefront host uri, the 19 | api key to use and the default tags if the user wants to narrow down 20 | her requests with some tags by default""" 21 | 22 | h = input("Wavefront host url: ") 23 | token = input("Api token: ") 24 | 25 | self.config = { 26 | self.wavefrontHostKey: h, 27 | self.apiTokenKey: token, 28 | } 29 | 30 | s = json.dumps( 31 | self.config, 32 | sort_keys=True, 33 | indent=4, 34 | separators=( 35 | ',', 36 | ': ')) 37 | 38 | BaseCommand.makeDirsIgnoreExisting(self.configDirPath) 39 | 40 | fn = os.path.join(self.configDirPath, self.configFileName) 41 | print( 42 | "Writing the following config to the config file at {0}: \n{1}".format( 43 | fn, 44 | s)) 45 | with open(fn, "w") as f: 46 | f.write(s) 47 | 48 | def addCmd(self, subParsers): 49 | p = subParsers.add_parser( 50 | "config", help="Set wavefront host, api token.") 51 | p.set_defaults(wavefrontConfigFuncToCall=self.handleCmd) 52 | 53 | def __init__(self, *args, **kwargs): 54 | super(ConfigCommand, self).__init__(*args, **kwargs) 55 | -------------------------------------------------------------------------------- /doc/sphinx/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -W 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = wavectl 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile commandReference 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | # %: Makefile 20 | # @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | 23 | MAKEFILE_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 24 | PANDOC_OPTIONS := -f markdown_strict -t gfm --wrap=none --include-in-header AutoGeneratedHeader.txt 25 | SPHINX_BUILDEER := text 26 | 27 | # Create the command reference 28 | # WIth PYTHONPATH, wavectl executions use the most recent version in the repo. 29 | all: 30 | set -x; \ 31 | rm -rf "$(BUILDDIR)" &&\ 32 | PYTHONPATH=$(realpath ../../):$(realpath ../../test) \ 33 | HOME=/tmp/Users/someuser \ 34 | PATH=$(realpath ../)/bin:$(PATH) \ 35 | $(SPHINXBUILD) -M $(SPHINX_BUILDEER) "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) &&\ 36 | \ 37 | for file in \ 38 | AdvancedGrep \ 39 | BrowserIntegration \ 40 | CommandLine \ 41 | CommandReference \ 42 | GitIntegration \ 43 | RepetitiveEditing \ 44 | Templating\ 45 | WavectlConfig \ 46 | ; do \ 47 | pandoc $(PANDOC_OPTIONS) $(MAKEFILE_DIR)/_build/$(SPHINX_BUILDEER)/$${file}.txt -o $(shell dirname $(MAKEFILE_DIR))/$${file}.md; \ 48 | done && \ 49 | pandoc $(PANDOC_OPTIONS) $(MAKEFILE_DIR)/_build/$(SPHINX_BUILDEER)/README.txt -o $(shell dirname $(shell dirname $(MAKEFILE_DIR)))/README.md 50 | -------------------------------------------------------------------------------- /doc/sphinx/BrowserIntegration.md: -------------------------------------------------------------------------------- 1 | 2 | \# Launch Wavefront GUI via \`wavectl\` 3 | 4 | Wavefront has an amazing GUI that we all love and use regularly. With its great 5 | performance and design, it significantly enables us to analyze our metrics. As 6 | a command line client to Wavefront, \`wavectl\` cannot replace the powerful GUI 7 | and all the crucial use cases addressed by the GUI. It was imperative for 8 | \`wavectl\` users to be able to switch to Wavefront GUI effortlessly. Because 9 | of that, we have build an \`--in-browser\` option into the 10 | \[\`show\`\]\(CommandReference.md\#show-options\) command. With 11 | \`--in-browser\`, the \`show\` command launches new browser tabs and loads your 12 | selected alerts and dashboards. 13 | 14 | For example say you want to investigate your alerts that have the name 15 | "Kubernetes" in them. You could narrow down your shown alerts with the \`--name REGEX\` 16 | command line option. After you list them in the terminal and are convinved of the 17 | selected alerts, you would probably interact with them via the Wavefront GUI. 18 | \`wavectl\` \`show\` can load all selected alerts in a browser tab with the 19 | \`--in-browser\` option. This saves a lot of clicking in the browser and unncessary 20 | copy paste from the command line to the browser. 21 | 22 | For example, the following command list all alerts with "Kubernetes" in their name 23 | and will create new browser tabs for each selected one and load the Wavefront page 24 | to that alert. 25 | 26 | ```eval_rst 27 | .. program-output:: wavectl show --in-browser alert --name Kubernetes 28 | :prompt: 29 | :returncode: 0 30 | ``` 31 | 32 | Similarly, the following views all Metadata dashboards in Wavefront GUI. 33 | 34 | ```eval_rst 35 | .. program-output:: wavectl show --in-browser dashboard --name Metadata 36 | :prompt: 37 | :returncode: 0 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | from setuptools import setup, find_packages 6 | import os 7 | 8 | CLASSIFIERS = [ 9 | 'Development Status :: 5 - Production/Stable', 10 | 'Intended Audience :: Developers', 11 | 'License :: OSI Approved :: Apache Software License', 12 | 'Programming Language :: Python', 13 | 'Programming Language :: Python :: 2.7', 14 | 'Programming Language :: Python :: 3.6', 15 | 'Operating System :: OS Independent', 16 | 'Operating System :: POSIX', 17 | 'Operating System :: MacOS :: MacOS X', 18 | 'Topic :: Software Development :: Libraries :: Python Modules', 19 | ] 20 | 21 | install_requires = [ 22 | "argcomplete", 23 | "future", 24 | "GitPython", 25 | "python-dateutil", 26 | "six", 27 | "termcolor", 28 | "wavefront-api-client>=2.3.1"] 29 | 30 | test_require = [ 31 | "mock", 32 | "recommonmark", 33 | "Sphinx", 34 | "sphinxcontrib-programoutput", 35 | ] 36 | 37 | base_dir = os.path.dirname(__file__) 38 | 39 | setup( 40 | name="wavectl", 41 | version="0.3.0", 42 | description="Command Line Client For Wavefront", 43 | long_description=open(os.path.join(base_dir, 'README.md'),).read(), 44 | long_description_content_type='text/markdown', 45 | url="https://github.com/box/wavectl", 46 | author="Box", 47 | author_email="oss@box.com", 48 | keywords=["Wavefront", "Wavefront Public API", "wavectl", "cli"], 49 | packages=find_packages(), 50 | install_requires=install_requires, 51 | test_require=test_require, 52 | entry_points={ 53 | 'console_scripts': [ 54 | 'wavectl = wavectl.main:main', 55 | ] 56 | }, 57 | classifiers=CLASSIFIERS, 58 | license='Apache Software License, Version 2.0, http://www.apache.org/licenses/LICENSE-2.0', 59 | ) 60 | -------------------------------------------------------------------------------- /doc/bin/mutateDashboards.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # A simple script that executes push and prints the output to std out . This 4 | # script is called during documentation generation time. 5 | 6 | from __future__ import absolute_import 7 | from __future__ import print_function 8 | 9 | import os 10 | import sys 11 | fileDir = os.path.dirname(os.path.abspath(__file__)) 12 | rootDir = os.path.realpath(os.path.join(fileDir, "..", "..")) 13 | testDir = os.path.join(rootDir, "test") 14 | sys.path.insert(0, rootDir) 15 | sys.path.insert(0, testDir) 16 | os.chdir(testDir) 17 | 18 | import argparse 19 | import wavectl 20 | import test.util 21 | import test.TestAlerts 22 | 23 | from argparse import Namespace 24 | 25 | 26 | def main(): 27 | 28 | testDashboards = test.util.TestDashboards.Dashboard.getTestDashboards()[:] 29 | test.util.mockRsrcType( 30 | wavectl.Dashboard.Dashboard, 31 | [testDashboards[7]], []) 32 | 33 | with test.util.TempDir() as td: 34 | d = td.dir() 35 | 36 | ns = Namespace( 37 | rsrcType="dashboard", 38 | dir=d, 39 | inGit=False, 40 | customerTag=None, 41 | wavefrontHost=test.util.wavefrontHostName, 42 | apiToken=test.util.wavefrontApiToken) 43 | 44 | pl = wavectl.PullCommand() 45 | pl.handleCmd(ns) 46 | 47 | ns = Namespace( 48 | rsrcType="dashboard", 49 | target=os.path.join(d, 50 | testDashboards[7][wavectl.Dashboard.Dashboard._uniqueKey]) 51 | + wavectl.Dashboard.Dashboard.fileExtension(), 52 | inGit=False, 53 | customerTag=None, 54 | quiet=False, 55 | wavefrontHost=test.util.wavefrontHostName, 56 | apiToken=test.util.wavefrontApiToken) 57 | 58 | pu = wavectl.PushCommand() 59 | pu.handleCmd(ns) 60 | 61 | 62 | if __name__ == "__main__": 63 | main() 64 | -------------------------------------------------------------------------------- /test/test_pullWithUnnormalizedPath.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Test that the pull command can handle path parameters if the path is not 4 | # normalized. Has characters like ../ ./ // ,etc 5 | 6 | from __future__ import absolute_import 7 | from __future__ import print_function 8 | import os 9 | import sys 10 | # Extend that path so that we can import the module under test. 11 | sys.path.insert(0, os.path.abspath('..')) 12 | 13 | import wavectl 14 | 15 | import unittest 16 | import shutil 17 | import datetime 18 | import tempfile 19 | import time 20 | import copy 21 | import logging 22 | 23 | 24 | import git 25 | 26 | import util 27 | 28 | import wavefront_api_client 29 | 30 | 31 | class Test(util.TestPull): 32 | 33 | def unnormalizedPath(self, rsrcType, rsrcs): 34 | """The passed directory path to the pull command may be 35 | unnormalized. The pull command should work regardless. For example the 36 | path may contain elements like /..//./""" 37 | 38 | dir = tempfile.mkdtemp() 39 | 40 | dirs = [ 41 | dir, 42 | os.path.join(dir, ""), 43 | dir.replace("/", "//"), 44 | dir.replace("/", "//").replace("//", "/./"), 45 | os.path.join(dir, "SomeDir", ".."), 46 | ] 47 | 48 | for d in dirs: 49 | # Always start from a clean state. Remove the dir. 50 | shutil.rmtree(dir, ignore_errors=True) 51 | shutil.rmtree(d, ignore_errors=True) 52 | 53 | self.executePull(rsrcType, d, None, rsrcs, 54 | pullAdditionalParams=["--inGit"]) 55 | 56 | r = git.Repo(d) 57 | 58 | shutil.rmtree(d, ignore_errors=True) 59 | 60 | def test_unnormalizedPath(self): 61 | self.unnormalizedPath("alert", util.allAlerts) 62 | self.unnormalizedPath("dashboard", util.allDashboards) 63 | 64 | 65 | if __name__ == '__main__': 66 | # util.initLog() 67 | util.unittestMain() 68 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | 2 | defaults: &defaults 3 | # This is the directory where circle ci checks out the repo. 4 | working_directory: /home/circleci/project 5 | 6 | container_env: &container_env 7 | docker: 8 | - image: boxinc/wavectl:centos7 9 | 10 | version: 2 11 | jobs: 12 | "python2": 13 | <<: *defaults 14 | <<: *container_env 15 | steps: 16 | - checkout 17 | - run: 18 | name: make check 19 | command: > 20 | git submodule update --init --force 21 | && pip install virtualenv 22 | && virtualenv -p $(which python2.7) venv 23 | && source venv/bin/activate 24 | && pip install -r requirements.txt 25 | && make -C test check 26 | && mkdir -p ~/.wavectl 27 | && printf '{"apiToken":"%s","wavefrontHost":"%s"}\n' "98jsb6ef-3939-kk88-8jv2-f84knf71vq68" "https://acme.wavefront.com" > ~/.wavectl/config 28 | && make -C doc/sphinx all 29 | 30 | "python3": 31 | <<: *defaults 32 | <<: *container_env 33 | steps: 34 | - checkout 35 | - run: 36 | name: make check 37 | command: > 38 | git submodule update --init --force 39 | && pip install virtualenv 40 | && virtualenv -p $(which python3.6) venv 41 | && source venv/bin/activate 42 | && pip install -r requirements.txt 43 | && make -C test check 44 | && mkdir -p ~/.wavectl 45 | && printf '{"apiToken":"%s","wavefrontHost":"%s"}\n' "98jsb6ef-3939-kk88-8jv2-f84knf71vq68" "https://acme.wavefront.com" > ~/.wavectl/config 46 | && make -C doc/sphinx all 47 | 48 | "allpass": 49 | docker: 50 | - image: ubuntu:14.04 51 | steps: 52 | - checkout 53 | - run: echo allpass 54 | 55 | workflows: 56 | version: 2 57 | tests: 58 | jobs: 59 | - "python2" 60 | - "python3" 61 | - allpass: 62 | requires: 63 | - "python2" 64 | - "python3" 65 | 66 | -------------------------------------------------------------------------------- /test/fixtures/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Test Fixtures 3 | 4 | This directory contains necessary data to run automated unit tests. 5 | 6 | During execution, `wavectl` reaches out to your specified Wavefront server 7 | using the [Wavefront API](https://docs.wavefront.com/wavefront_api.html). 8 | However, during automated test execution, we do not want to reach out to a live 9 | Wavefront server. Instead, the json state of some selected alerts and 10 | dashboards are saved in json files in this directory. The json representation 11 | in files are in the same format as a Wavefront server returns them via http 12 | calls. 13 | 14 | During unit test execution, the test functions read the json files in this 15 | directory and "mock" the behavior of a Wavefront server. That way the `wavectl` 16 | unit tests can execute independently from a live running Wavefront server. 17 | 18 | As it turns out, Wavefront can change the json representation of alerts or 19 | dashboards any time, without giving much notice to users. For that reason, 20 | whenever a new Wavefront server version is deployed, we would like to update 21 | the json files in this directory to contain the latest representations. With a 22 | new version of Wavefont server, the json state may get new keys, or some keys 23 | may be removed or renamed. 24 | 25 | To update the json files in this directory, the user needs to execute the 26 | included `create_test_fixtures.py` script. That script reaches out to the 27 | specified Wavefront server. Then the existing alerts and dashboards in this 28 | directory are written into the Wavefront server. After that, they are read back 29 | and again saved in the same json files in this directory. If anything in the 30 | Wavefront representation has changed, the json files in this directory will 31 | have the same change. 32 | 33 | 34 | For example to update the json files in-place execute the following from this 35 | directory: 36 | 37 | ``` 38 | ./create_test_fixtures.py alert TestAlerts.json TestAlerts.json 39 | ./create_test_fixtures.py dashboard TestDashboards.json TestDashboards.json 40 | ``` 41 | 42 | An example for `` is `https://try.wavefront.com` 43 | -------------------------------------------------------------------------------- /doc/sphinx/WavectlConfig.md: -------------------------------------------------------------------------------- 1 | 2 | \# Easy configuration of \`wavectl\` 3 | 4 | During execution, \`wavectl\` needs to talk to a Wavefront server. The address of 5 | the Wavefront Server and the necessary tokens for authorization are required 6 | for \`wavectl\` to work. 7 | 8 | All applicable wavectl 9 | \[subcommands\]\(CommandReference.md#subcommand-options\) accept a 10 | \`--wavefrontHost\` and a \`--apiToken\` parameter to discover and authenticate 11 | to the Wavefront Server. With these command line parameters, the user needs to 12 | speficy the server path and the api token for every command. That may result in 13 | too much typing at the command line and the user experience may degrade. 14 | 15 | For example: 16 | 17 | ```eval_rst 18 | .. program-output:: wavectl show --wavefrontHost https://acme.wavefront.com --apiToken 98jsb6ef-3939-kk88-8jv2-f84knf71vq68 alert 19 | :prompt: 20 | :ellipsis: 5 21 | :returncode: 0 22 | ``` 23 | 24 | To make \`wavectl\` more usaable we have added a \`config\` 25 | \[subcommand\]\(CommandReference.md#config-options\). You only need to execute 26 | \`wavect config\` once and speficy your Wavefront host and api token once. \`config\` 27 | command creates an \*unencrypted\* file at \`${HOME}/.wavectl/config\` and saves the 28 | specified values there. Any other wavectl 29 | \[subcommand\]\(CommandReference.md#subcommand-options\) after a \`wavectl config\` 30 | execution will use the credentials saved in \`${HOME}/.wavectl/config\` file. 31 | 32 | For example: 33 | 34 | ```eval_rst 35 | .. program-output:: printf 'https://acme.wavefront.com \n 98jsb6ef-3939-kk88-8jv2-f84knf71vq68 \n' | wavectl config 36 | :prompt: 37 | :shell: 38 | :returncode: 0 39 | ``` 40 | 41 | ```eval_rst 42 | .. program-output:: wavectl show alert 43 | :prompt: 44 | :ellipsis: 5 45 | :returncode: 0 46 | ``` 47 | 48 | The \`config\` subcommand supports only one set of Wavefront host and api token 49 | specification. If your organization has two different Wavefront environments, 50 | then you may need to fall back to the \`--wavefrontHost\` and \`--apiToken\` 51 | command line parameters for the second environment. The command line options 52 | take precedence over the values in the \`${HOME}/.wavectl/config\` file. 53 | 54 | 55 | -------------------------------------------------------------------------------- /test/test_pullIntoVanillaDir.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Tests for pulling into a vanilla dir. A plain directory not in git. 4 | 5 | from __future__ import absolute_import 6 | from __future__ import print_function 7 | import os 8 | import sys 9 | # Extend that path so that we can import the module under test. 10 | sys.path.insert(0, os.path.abspath('..')) 11 | 12 | import wavectl 13 | 14 | import unittest 15 | import shutil 16 | import datetime 17 | import tempfile 18 | import time 19 | import copy 20 | import logging 21 | 22 | 23 | import git 24 | 25 | import util 26 | 27 | import wavefront_api_client 28 | 29 | 30 | class Test(util.TestPull): 31 | 32 | def existingVanillaDir(self, rsrcType, rsrcs): 33 | """The pull operation is done on a regular dir outside of source control""" 34 | with util.TempDir() as td: 35 | d = td.dir() 36 | self.executePull(rsrcType, d, None, rsrcs) 37 | 38 | def test_existingVanillaDir(self): 39 | self.existingVanillaDir("alert", util.allAlerts) 40 | self.existingVanillaDir("dashboard", util.allDashboards) 41 | 42 | def newVanillaDir(self, rsrcType, rsrcs, nested=False): 43 | """The pull operation is passed a path that does not exist yet and 44 | no git source control is used. If nested is true, the pulled dir has other 45 | intermediate directories that also do not exist.""" 46 | 47 | d = tempfile.mkdtemp() 48 | shutil.rmtree(d, ignore_errors=True) 49 | 50 | pulledDir = d 51 | if nested: 52 | # We want to use a nested pulledDir. Have another intermediate 53 | # directory that does not exist. 54 | pulledDir = os.path.join(d, "leafDir") 55 | 56 | self.executePull(rsrcType, pulledDir, None, rsrcs) 57 | shutil.rmtree(d, ignore_errors=True) 58 | 59 | def test_newVanillaDir(self): 60 | self.newVanillaDir("alert", util.allAlerts) 61 | self.newVanillaDir("dashboard", util.allDashboards) 62 | 63 | def test_newNestedVanillaDir(self): 64 | self.newVanillaDir("alert", util.allAlerts, nested=True) 65 | self.newVanillaDir("dashboard", util.allDashboards, nested=True) 66 | 67 | 68 | if __name__ == '__main__': 69 | # util.initLog() 70 | util.unittestMain() 71 | -------------------------------------------------------------------------------- /test/test_pullWithDeletedRsrcs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This file contains positive tests for pull command 4 | 5 | from __future__ import absolute_import 6 | from __future__ import print_function 7 | import os 8 | import sys 9 | # Extend that path so that we can import the module under test. 10 | sys.path.insert(0, os.path.abspath('..')) 11 | 12 | import wavectl 13 | 14 | import unittest 15 | import shutil 16 | import datetime 17 | import tempfile 18 | import time 19 | import copy 20 | import logging 21 | 22 | 23 | import git 24 | 25 | import util 26 | 27 | import wavefront_api_client 28 | 29 | 30 | class Test(util.TestPull): 31 | 32 | def deletedRsrcs(self, rsrcType, rsrcs): 33 | """Some resources get deleted in the wavefront gui. Make sure 34 | consecutive pulls do not retain those resources. The resource' files in 35 | the pull directory also get deleted in following pulls""" 36 | 37 | assert len( 38 | rsrcs) > 5 and "This test expects to have a handful of resources" 39 | 40 | with util.TempDir() as td: 41 | d = td.dir() 42 | r = self.repoInit(d) 43 | 44 | self.addReadmeFileToRepo(r) 45 | self.createPullBranch(r, wavectl.PullCommand.datetimeFormat, 46 | wavectl.PullCommand.pullBranchSuffix) 47 | 48 | self.executePull( 49 | rsrcType, 50 | d, 51 | r, 52 | rsrcs, 53 | pullAdditionalParams=["--inGit"]) 54 | 55 | time.sleep(2) 56 | 57 | # Mock the deleted rsrcs call to return a value. 58 | rt = util.resourceTypeFromString(rsrcType) 59 | util.mockRsrcType(rt, rsrcs[2:], rsrcs[0:2]) 60 | 61 | # Do another pull. After that make sure that the deleted alert's file 62 | # also disappears. 63 | self.executePull( 64 | rsrcType, 65 | d, 66 | r, 67 | rsrcs[2:], 68 | pullAdditionalParams=["--inGit"]) 69 | 70 | def test_deletedRsrcs(self): 71 | self.deletedRsrcs("alert", util.allAlerts) 72 | self.deletedRsrcs("dashboard", util.allDashboards) 73 | 74 | 75 | if __name__ == '__main__': 76 | # util.initLog() 77 | util.unittestMain() 78 | -------------------------------------------------------------------------------- /doc/sphinx/AdvancedGrep.md: -------------------------------------------------------------------------------- 1 | 2 | \# Advanced grep in your alerts and dashboards. 3 | 4 | \`wavectl\` allows users to execute regular expression searches on 5 | alerts and dashboards. Using the 6 | \[resource options\]\(CommandReference.md#resource-options\), the user can specify 7 | fields and a regular expression to match for each field. \`wavectl\` will 8 | process only resources that satisfy all specified regular expressions. 9 | For \`show\` command, only the matched alerts/dashboards will be displayed, for 10 | \`push\` only the matching ones will be written to Wavefront, and so on. 11 | 12 | 13 | 14 | 15 | ```eval_rst 16 | .. program-output:: printf 'https://acme.wavefront.com \n 98jsb6ef-3939-kk88-8jv2-f84knf71vq68 \n' | wavectl config > /dev/null 17 | :shell: 18 | :returncode: 0 19 | ``` 20 | 21 | For example: show alerts that have "Kubernetes" and "Utilization" in their names: 22 | 23 | ```eval_rst 24 | .. program-output:: wavectl show alert --name "Kubernetes.*Utilization" 25 | :prompt: 26 | :returncode: 0 27 | ``` 28 | 29 | The \`--match\` parameter can be used to search anywhere in the json representation 30 | of an alert or a dashboard rather than in known key value pairs. 31 | 32 | For example: Show the dashboards that use metrics from the live environment 33 | 34 | ```eval_rst 35 | .. program-output:: wavectl show dashboard --match "env=live" 36 | :prompt: 37 | :returncode: 0 38 | ``` 39 | 40 | Write alerts back to Wavefront that have a specific person in the \`updaterId\` 41 | 42 | 43 | 44 | 45 | ```eval_rst 46 | .. program-output:: rm -rf /tmp/AdvancedGrep/dashboards 47 | :returncode: 0 48 | ``` 49 | 50 | 51 | 52 | ```eval_rst 53 | .. program-output:: wavectl pull /tmp/AdvancedGrep/dashboards dashboard 54 | :returncode: 0 55 | :shell: 56 | ``` 57 | 58 | ```eval_rst 59 | .. program-output:: wavectl push /tmp/AdvancedGrep/dashboards dashboard --updaterId hbaba 60 | :returncode: 0 61 | :prompt: 62 | :ellipsis: 8 63 | ``` 64 | 65 | 66 | \`wavectl\` uses python standard library's regular expression 67 | \[module\]\(https://docs.python.org/3.4/library/re.html\). Any valid python 68 | regular expression can be specified for the \[resource 69 | options\]\(CommandReference.md#resource-options\). 70 | -------------------------------------------------------------------------------- /wavectl/Push.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import absolute_import 3 | from __future__ import print_function 4 | import argparse 5 | import os 6 | import json 7 | import time 8 | import datetime 9 | import sys 10 | import tempfile 11 | import shutil 12 | import logging 13 | import threading 14 | import multiprocessing 15 | 16 | # External dependencies: 17 | import git 18 | 19 | # TODO: The name is not correct anymore. It should be like 20 | # WavefrontCmdImpl or something like that. 21 | from .BaseWavefrontCommand import BaseWavefrontCommand 22 | from .ResourceFactory import ResourceFactory 23 | from .Mutator import Mutator 24 | from .GitUtils import GitUtils 25 | 26 | 27 | class PushCommand(Mutator): 28 | """ The implementation of the `push` command. Used to post resource files 29 | from local dir to the remote wavefront server""" 30 | 31 | def handleCmd(self, args): 32 | """ Handles the pull [...] commands. 33 | Reads the given resources from the given dir. Does filtering if necessary. 34 | Writes them back to the wavefront server""" 35 | 36 | super(PushCommand, self).handleCmd(args) 37 | 38 | rsrcs = self.doChecksGetRsrcsFromTarget(args) 39 | 40 | threadPool = multiprocessing.pool.ThreadPool( 41 | processes=BaseWavefrontCommand.concurrentThreadCount) 42 | 43 | rsrcType = args.rsrcType 44 | rt = ResourceFactory.resourceTypeFromString(rsrcType) 45 | updateFunction = rt.updateFunction(self.getWavefrontApiClient()) 46 | 47 | asyncResults = [] 48 | for r in rsrcs: 49 | ar = threadPool.apply_async( 50 | Mutator.withApiExceptionRetry(updateFunction), 51 | [r.uniqueId()], 52 | {"body": r._state, "_preload_content": False}) 53 | asyncResults.append(ar) 54 | 55 | # TODO: How to handle mutateFunction exceptions in this pattern ? 56 | 57 | if not args.quiet: 58 | print("Replaced {}(s):".format(rsrcType)) 59 | print(rt.summaryTableHeader()) 60 | for ar in asyncResults: 61 | res = ar.get() 62 | data = json.loads(res.read()) 63 | rawRsrc = data["response"] 64 | rsrc = rt.fromDict(rawRsrc) 65 | if not args.quiet: 66 | print(rsrc.summaryTableRow( 67 | BaseWavefrontCommand.doesTermSupportColor())) 68 | 69 | def addCmd(self, subParsers): 70 | 71 | p = subParsers.add_parser( 72 | "push", help=( 73 | "push resource files from the given target to " 74 | " Wavefront")) 75 | p.set_defaults(wavefrontConfigFuncToCall=self.handleCmd) 76 | self._addInGitOption(p) 77 | Mutator._addQuietOption(p) 78 | Mutator._addTargetOption(p) 79 | 80 | self._addRsrcTypeSubParsers(p) 81 | 82 | super(PushCommand, self).addCmd(p) 83 | 84 | def __init__(self, *args, **kwargs): 85 | super(PushCommand, self).__init__(*args, **kwargs) 86 | -------------------------------------------------------------------------------- /test/test_configErrors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This file contains negative tests for the config command 4 | 5 | from __future__ import absolute_import 6 | from __future__ import print_function 7 | import os 8 | import sys 9 | # Extend that path so that we can import the module under test. 10 | sys.path.insert(0, os.path.abspath('..')) 11 | 12 | # Design under test. 13 | import wavectl 14 | 15 | import unittest 16 | import tempfile 17 | import git 18 | import shutil 19 | 20 | import util 21 | 22 | 23 | class Test(unittest.TestCase): 24 | """ A class that checks for error conditions with the config files """ 25 | 26 | def test_wavefrontHostNotFound(self): 27 | """In this test the user attempts to execute a command that 28 | reaches out to the wavefront api server. However the config file 29 | mentioning the host name and the api token have not been initialized yet. 30 | The user also has not passed command line options specifying the wavefront 31 | host or the api token. There should be a raised exception""" 32 | 33 | # The config file dir gets deleted. So the command cannot find the 34 | # desired config file 35 | d = tempfile.mkdtemp() 36 | shutil.rmtree(d, ignore_errors=True) 37 | 38 | args = ["show", "alert"] 39 | # Since the designForTestRsrcs is not given, the command will try to 40 | # reach out to wavefront. It will """ 41 | wc = wavectl.Wavectl( 42 | designForTestArgv=args, 43 | designForTestConfigDir=d) 44 | 45 | self.assertRaisesRegexp( 46 | wavectl.ConfigError, 47 | ("The wavefront host url is not known. Either execute " 48 | "`wavectl config ...` or pass {0} command line option").format( 49 | wc.pull.wavefrontHostOptionName), 50 | wc.runCmd) 51 | 52 | def test_apiTokenNotFound(self): 53 | """the api token is not specified. The config file is not populated and 54 | the command line ars do not have the --apiToken parameter. We expect 55 | an exception raised.""" 56 | 57 | # The config file dir gets deleted. So the command cannot find the 58 | # desired config file 59 | d = tempfile.mkdtemp() 60 | shutil.rmtree(d, ignore_errors=True) 61 | 62 | args = ["show", "--wavefrontHost", "https://someHost.com", "alert"] 63 | # Since the designForTestRsrcs is not given, the command will try to 64 | # reach out to wavefront. It will look for the config file. 65 | wc = wavectl.Wavectl( 66 | designForTestArgv=args, 67 | designForTestConfigDir=d) 68 | 69 | self.assertRaisesRegexp( 70 | wavectl.ConfigError, 71 | ("The wavefront api token is not known. Either execute " 72 | "`wavectl config ...` or pass {0} command line option").format( 73 | wc.pull.apiTokenOptionName), 74 | wc.runCmd) 75 | 76 | 77 | if __name__ == '__main__': 78 | util.unittestMain() 79 | -------------------------------------------------------------------------------- /test/test_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This file contains positive tests for config command 4 | 5 | from __future__ import absolute_import 6 | from __future__ import print_function 7 | import os 8 | import sys 9 | # Extend that path so that we can import the module under test. 10 | sys.path.insert(0, os.path.abspath('..')) 11 | 12 | import wavectl 13 | 14 | import tempfile 15 | import unittest 16 | import shutil 17 | try: 18 | # Python 2 19 | from cStringIO import StringIO 20 | except ImportError: 21 | from io import StringIO 22 | import json 23 | import threading 24 | 25 | 26 | import util 27 | 28 | 29 | class Test(unittest.TestCase): 30 | 31 | def configFileCreationTest(self, d, hostNameEntry, apiTokenEntry): 32 | """Test basic functionality. You call config and specify the 33 | entries. The generated config file should contain the same information""" 34 | 35 | args = ["config"] 36 | wc = wavectl.Wavectl(designForTestArgv=args, designForTestConfigDir=d) 37 | 38 | input = StringIO() 39 | input.write(hostNameEntry) 40 | input.write(apiTokenEntry) 41 | 42 | with util.StdinRedirect(input) as redIn: 43 | wc.runCmd() 44 | 45 | # In the config dir, a config file must be created. 46 | self.assertListEqual([wc.config.configFileName], os.listdir(d)) 47 | 48 | fn = os.path.join(d, wc.config.configFileName) 49 | with open(fn) as f: 50 | c = json.load(f) 51 | 52 | # Check the values in the read config. 53 | self.assertDictEqual( 54 | c, 55 | {wc.config.wavefrontHostKey: hostNameEntry.strip(), 56 | wc.config.apiTokenKey: apiTokenEntry.strip(), 57 | }) 58 | 59 | def test_configFileCreation(self): 60 | """Test basic functionality. You call config and specify the 61 | entries. The generated config file should contain the same information. 62 | Make sure that the config file creation works if the configDir exists 63 | already or not and an old config file exists already or not """ 64 | 65 | hostNameEntry = "TestHostName\n" 66 | apiTokenEntry = "TestApiKey\n" 67 | 68 | # Config Dir Config File 69 | # Exists Exists 70 | with util.TempDir() as td: 71 | d = td.dir() 72 | with open(os.path.join(d, wavectl.ConfigCommand.configFileName), "w"): 73 | self.configFileCreationTest(d, hostNameEntry, apiTokenEntry) 74 | 75 | # Config Dir Config File 76 | # Exists Does Not 77 | with util.TempDir() as td: 78 | d = td.dir() 79 | self.configFileCreationTest(d, hostNameEntry, apiTokenEntry) 80 | 81 | # Config Dir Config File 82 | # Does Not Does Not 83 | d = tempfile.mkdtemp() 84 | shutil.rmtree(d, ignore_errors=True) 85 | self.configFileCreationTest(d, hostNameEntry, apiTokenEntry) 86 | 87 | 88 | if __name__ == '__main__': 89 | util.unittestMain() 90 | -------------------------------------------------------------------------------- /wavectl/Create.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import absolute_import 3 | from __future__ import print_function 4 | import argparse 5 | import os 6 | import json 7 | import time 8 | import datetime 9 | import sys 10 | import tempfile 11 | import shutil 12 | import logging 13 | import threading 14 | import multiprocessing 15 | 16 | # External dependencies: 17 | import git 18 | 19 | # TODO: The name is not correct anymore. It should be like 20 | # WavefrontCmdImpl or something like that. 21 | from .BaseWavefrontCommand import BaseWavefrontCommand 22 | from .ResourceFactory import ResourceFactory 23 | from .Mutator import Mutator 24 | from .GitUtils import GitUtils 25 | 26 | from .Alert import Alert 27 | 28 | 29 | class CreateCommand(Mutator): 30 | """ The implementation of the `create` command. Used to post resource files 31 | from local dir to the remote wavefront server. This assumes that the wavefront 32 | resources do not exists in the wavefront server to start with""" 33 | 34 | def handleCmd(self, args): 35 | """ Handles the pull [...] commands. 36 | Reads the given resources from the given dir. Does filtering if necessary. 37 | Writes them back to the wavefront server""" 38 | 39 | super(CreateCommand, self).handleCmd(args) 40 | 41 | rsrcType = args.rsrcType 42 | parallelThreadCount = BaseWavefrontCommand.concurrentThreadCount 43 | 44 | rsrcs = self.doChecksGetRsrcsFromTarget(args) 45 | 46 | threadPool = multiprocessing.pool.ThreadPool( 47 | processes=parallelThreadCount) 48 | 49 | rsrcType = args.rsrcType 50 | rt = ResourceFactory.resourceTypeFromString(rsrcType) 51 | createFunction = rt.createFunction(self.getWavefrontApiClient()) 52 | 53 | asyncResults = [] 54 | for r in rsrcs: 55 | ar = threadPool.apply_async( 56 | Mutator.withApiExceptionRetry(createFunction), 57 | [], 58 | {"body": r._state, "_preload_content": False}) 59 | asyncResults.append(ar) 60 | 61 | # TODO: How to handle mutateFunction exceptions in this pattern ? 62 | 63 | if not args.quiet: 64 | print("Created {}(s):".format(rsrcType)) 65 | print(rt.summaryTableHeader()) 66 | for ar in asyncResults: 67 | res = ar.get() 68 | data = json.loads(res.read()) 69 | rawRsrc = data["response"] 70 | rsrc = rt.fromDict(rawRsrc) 71 | if not args.quiet: 72 | print(rsrc.summaryTableRow( 73 | BaseWavefrontCommand.doesTermSupportColor())) 74 | 75 | def addCmd(self, subParsers): 76 | p = subParsers.add_parser( 77 | "create", help="create new resources in Wavefront using target") 78 | p.set_defaults(wavefrontConfigFuncToCall=self.handleCmd) 79 | self._addInGitOption(p) 80 | Mutator._addQuietOption(p) 81 | Mutator._addTargetOption(p) 82 | 83 | self._addRsrcTypeSubParsers(p) 84 | super(CreateCommand, self).addCmd(p) 85 | 86 | def __init__(self, *args, **kwargs): 87 | super(CreateCommand, self).__init__(*args, **kwargs) 88 | -------------------------------------------------------------------------------- /test/fixtures/create_test_fixtures.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import wavefront_api_client 5 | import json 6 | 7 | 8 | def main(): 9 | 10 | alert, dashboard = ["alert", "dashboard"] 11 | 12 | parser = argparse.ArgumentParser( 13 | description="""A tool to create test fixtures using the latest version of 14 | the wavefront server. In wavectl tests, we have local representations 15 | of the json data that is returned by the wavefront api server. Depending 16 | on the server version and the api version, this data may change over time. 17 | With this script one could update the locally saves json test fixtures.""") 18 | parser.add_argument("wavefrontHost", 19 | help="""Speficy the url of the wavefront host.""") 20 | parser.add_argument("apiToken", help="""Speficy the api token to use while 21 | communicating with the wavefront host.""") 22 | parser.add_argument( 23 | "rsrcType", 24 | choices=[ 25 | alert, 26 | dashboard], 27 | help="Specify the resource kind represented in the file") 28 | parser.add_argument("inFile", help="Input File") 29 | parser.add_argument("outFile", help="Out File") 30 | 31 | args = parser.parse_args() 32 | 33 | config = wavefront_api_client.Configuration() 34 | config.host = args.wavefrontHost 35 | wavefrontClient = wavefront_api_client.ApiClient( 36 | configuration=config, 37 | header_name="Authorization", 38 | header_value="Bearer " + args.apiToken) 39 | 40 | alertApi = wavefront_api_client.AlertApi(wavefrontClient) 41 | dashboardApi = wavefront_api_client.DashboardApi(wavefrontClient) 42 | searchApi = wavefront_api_client.SearchApi(wavefrontClient) 43 | 44 | with open(args.inFile) as f: 45 | inRsrcs = json.load(f) 46 | 47 | uKey = "id" 48 | if args.rsrcType == alert: 49 | api = alertApi 50 | createFunc = api.create_alert 51 | searchFunc = searchApi.search_alert_entities 52 | delFunc = api.delete_alert 53 | elif args.rsrcType == dashboard: 54 | api = dashboardApi 55 | createFunc = api.create_dashboard 56 | searchFunc = searchApi.search_dashboard_entities 57 | delFunc = api.delete_dashboard 58 | else: 59 | assert not "Unexpected value in rsrcType parameter" 60 | 61 | outRsrcs = [] 62 | for r in inRsrcs: 63 | createFuncParams = {"body": r} 64 | rawRes = createFunc(_preload_content=False, **createFuncParams) 65 | strRes = rawRes.read().decode('utf-8') 66 | res = json.loads(strRes)["response"] 67 | outRsrcs.append(res) 68 | 69 | with open(args.outFile, "w") as f: 70 | json.dump( 71 | outRsrcs, 72 | f, 73 | indent=4, 74 | sort_keys=True, 75 | separators=( 76 | ',', 77 | ': ')) 78 | 79 | # Permanently delete every rsrc 80 | for r in outRsrcs: 81 | uId = r[uKey] 82 | delFunc(uId, _preload_content=False) 83 | delFunc(uId, _preload_content=False) 84 | 85 | 86 | if __name__ == "__main__": 87 | main() 88 | -------------------------------------------------------------------------------- /test/test_showErrors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This file contains negative tests for the show command. 4 | 5 | from __future__ import absolute_import 6 | from __future__ import print_function 7 | import os 8 | import sys 9 | # Extend that path so that we can import the module under test. 10 | sys.path.insert(0, os.path.abspath('..')) 11 | 12 | # Design under test. 13 | import wavectl 14 | 15 | import unittest 16 | import tempfile 17 | import git 18 | import shutil 19 | 20 | import util 21 | 22 | 23 | class Test(unittest.TestCase): 24 | """ A class that checks for error conditions in the `show` command""" 25 | 26 | def tooManyRsrcsToDisplayInBrowser(self, rsrcType, rsrcs, maxBrowserTabs): 27 | """If we have too many selected rsrcs, displaying them in a 28 | browser should throw an exception saying that it is not allowed. 29 | This function checks for that error reporting""" 30 | 31 | with util.EnvModifier("MAX_BROWSER_TABS", maxBrowserTabs) as em: 32 | args = ["show", "--in-browser", rsrcType] 33 | wc = wavectl.Wavectl( 34 | designForTestArgv=args, 35 | designForTestRsrcs=rsrcs) 36 | 37 | # In this test we should attempt to open more tabs than allowed. 38 | # Checking that we are actually doing that. 39 | assert(len(rsrcs) > wc.show._maxBrowserTabs()) 40 | 41 | # This show call prints a lot of lines to the output. Avoid the 42 | # print out by capturing the output 43 | with util.StdoutCapture() as capturedOut: 44 | self.assertRaisesRegexp( 45 | wavectl.ShowError, 46 | "Too many resources to display in browser.*", 47 | wc.runCmd) 48 | 49 | def test_tooManyRsrcsToDisplayInBrowser(self): 50 | self.tooManyRsrcsToDisplayInBrowser("alert", util.allAlerts, None) 51 | self.tooManyRsrcsToDisplayInBrowser("alert", 52 | [util.Alert.nameMatch, 53 | util.Alert.additionalInformationMatch, 54 | util.Alert.nameMatch], 55 | "2") 56 | 57 | self.tooManyRsrcsToDisplayInBrowser( 58 | "dashboard", util.allDashboards, None) 59 | self.tooManyRsrcsToDisplayInBrowser("dashboard", 60 | [util.Dashboard.kubeBoxPki, 61 | util.Dashboard.skynetApplier, 62 | util.Dashboard.skynetMonitoring], 63 | "2") 64 | 65 | def test_invalidMaxBrowserTabs(self): 66 | """The user specified an invalid value to the maxBrowserTabs env var. 67 | It should fall back to the default 10.""" 68 | self.tooManyRsrcsToDisplayInBrowser( 69 | "alert", util.allAlerts, "invalidNumber") 70 | self.tooManyRsrcsToDisplayInBrowser("dashboard", util.allDashboards, 71 | "invalidNumber") 72 | 73 | 74 | if __name__ == '__main__': 75 | util.unittestMain() 76 | -------------------------------------------------------------------------------- /doc/BrowserIntegration.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | # Launch Wavefront GUI via `wavectl` 8 | 9 | Wavefront has an amazing GUI that we all love and use regularly. With its great performance and design, it significantly enables us to analyze our metrics. As a command line client to Wavefront, `wavectl` cannot replace the powerful GUI and all the crucial use cases addressed by the GUI. It was imperative for `wavectl` users to be able to switch to Wavefront GUI effortlessly. Because of that, we have build an `--in-browser` option into the [`show`](CommandReference.md#show-options) command. With `--in-browser`, the `show` command launches new browser tabs and loads your selected alerts and dashboards. 10 | 11 | For example say you want to investigate your alerts that have the name "Kubernetes" in them. You could narrow down your shown alerts with the `--name REGEX` command line option. After you list them in the terminal and are convinved of the selected alerts, you would probably interact with them via the Wavefront GUI. `wavectl` `show` can load all selected alerts in a browser tab with the `--in-browser` option. This saves a lot of clicking in the browser and unncessary copy paste from the command line to the browser. 12 | 13 | For example, the following command list all alerts with "Kubernetes" in their name and will create new browser tabs for each selected one and load the Wavefront page to that alert. 14 | 15 | ``` 16 | $ wavectl show --in-browser alert --name Kubernetes 17 | ID NAME STATUS SEVERITY 18 | 1530723441304 Kubernetes - Node Network Utilization - HIGH (Prod) CHECKING WARN 19 | 1530723441442 Kubernetes - Node Cpu Utilization - HIGH (Prod) CHECKING WARN 20 | 1530723441589 Kubernetes - Node Memory Swap Utilization - HIGH (Prod) SNOOZED WARN 21 | ``` 22 | 23 | Similarly, the following views all Metadata dashboards in Wavefront GUI. 24 | 25 | ``` 26 | $ wavectl show --in-browser dashboard --name Metadata 27 | ID NAME DESCRIPTION 28 | metadata-operations Metadata Operations Metrics about each operation that can be performed against the data store 29 | metadata-perfpod Metadata PerfPod Monitors for testing Metadata in the PerfPods 30 | metadata-php Metadata PHP Monitors for Metadata in the PHP webapp 31 | ``` 32 | -------------------------------------------------------------------------------- /doc/sphinx/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | from recommonmark.parser import CommonMarkParser 20 | from recommonmark.transform import AutoStructify 21 | 22 | 23 | # -- Project information ----------------------------------------------------- 24 | 25 | project = u'wavectl' 26 | copyright = u'2018, Box' 27 | author = u'Box' 28 | 29 | # The short X.Y version 30 | version = u'' 31 | # The full version, including alpha/beta/rc tags 32 | release = u'' 33 | 34 | 35 | # -- General configuration --------------------------------------------------- 36 | 37 | # If your documentation needs a minimal Sphinx version, state it here. 38 | # 39 | # needs_sphinx = '1.0' 40 | 41 | # Add any Sphinx extension module names here, as strings. They can be 42 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 43 | # ones. 44 | extensions = [ 45 | "sphinxcontrib.programoutput", 46 | ] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | source_parsers = { 52 | '.md': CommonMarkParser, 53 | } 54 | 55 | 56 | def setup(app): 57 | app.add_config_value('recommonmark_config', { 58 | 'enable_eval_rst': True, 59 | }, True) 60 | app.add_transform(AutoStructify) 61 | 62 | 63 | # The suffix(es) of source filenames. 64 | # You can specify multiple suffix as a list of string: 65 | # 66 | # source_suffix = ['.rst', '.md'] 67 | source_suffix = ['.md'] 68 | 69 | # The master toctree document. 70 | master_doc = 'index' 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | # 75 | # This is also used if you do content translation via gettext catalogs. 76 | # Usually you set "language" from the command line for these cases. 77 | language = None 78 | 79 | # List of patterns, relative to source directory, that match files and 80 | # directories to ignore when looking for source files. 81 | # This pattern also affects html_static_path and html_extra_path . 82 | exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | 88 | # -- Options for Text output ------------------------------------------------- 89 | 90 | # Essentially disable line wrapping in text output. 91 | # Text output sometimes wraps the lines at unfortunate places and some Markdown 92 | # macros break because of that. 93 | import sphinx.writers.text 94 | sphinx.writers.text.MAXWIDTH = 100000 95 | 96 | text_sectionchars = '=-~"+`' 97 | 98 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributions are welcome to this project. 4 | 5 | ## Contributor License Agreement 6 | 7 | Before a contribution can be merged into this project, please fill out the Contributor License Agreement (CLA) located at: 8 | 9 | http://opensource.box.com/cla 10 | 11 | To learn more about CLAs and why they are important to open source projects, please see the [Wikipedia entry](http://en.wikipedia.org/wiki/Contributor_License_Agreement). 12 | 13 | ## Code of Conduct 14 | 15 | This project adheres to the [Box Open Code of Conduct](http://opensource.box.com/code-of-conduct/). By participating, you are expected to uphold this code. 16 | 17 | ## How to contribute 18 | 19 | * **File an issue** - if you found a bug, want to request an enhancement, or want to implement something (bug fix or feature). 20 | * **Send a pull request** - if you want to contribute code. Please be sure to file an issue first. 21 | 22 | ## Pull request best practices 23 | 24 | We want to accept your pull requests. Please follow these steps: 25 | 26 | ### Step 1: File an issue 27 | 28 | Before writing any code, please file an issue stating the problem you want to solve or the feature you want to implement. This allows us to give you feedback before you spend any time writing code. There may be a known limitation that can't be addressed, or a bug that has already been fixed in a different way. The issue allows us to communicate and figure out if it's worth your time to write a bunch of code for the project. 29 | 30 | ### Step 2: Fork this repository in GitHub 31 | 32 | This will create your own copy of our repository. 33 | 34 | ### Step 3: Add the upstream source 35 | 36 | The upstream source is the project under the Box organization on GitHub. To add an upstream source for this project, type: 37 | 38 | ``` 39 | git remote add upstream git@github.com:box/wavectl.git 40 | ``` 41 | 42 | This will come in useful later. 43 | 44 | ### Step 4: Create a feature branch 45 | 46 | Create a branch with a descriptive name, such as `add-search`. 47 | 48 | ### Step 5: Push your feature branch to your fork 49 | 50 | As you develop code, continue to push code to your remote feature branch. Please make sure to include the issue number you're addressing in your commit message, such as: 51 | 52 | ``` 53 | git commit -m "Adding search (fixes #123)" 54 | ``` 55 | 56 | This helps us out by allowing us to track which issue your commit relates to. 57 | 58 | Keep a separate feature branch for each issue you want to address. 59 | 60 | ### Step 6: Rebase 61 | 62 | Before sending a pull request, rebase against upstream, such as: 63 | 64 | ``` 65 | git fetch upstream 66 | git rebase upstream/master 67 | ``` 68 | 69 | This will add your changes on top of what's already in upstream, minimizing merge issues. 70 | 71 | ### Step 7: Run the tests 72 | 73 | Make sure that all tests are passing before submitting a pull request. 74 | 75 | ### Step 8: Send the pull request 76 | 77 | Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did. 78 | 79 | Keep in mind that we like to see one issue addressed per pull request, as this helps keep our git history clean and we can more easily track down issues. 80 | -------------------------------------------------------------------------------- /test/test_create.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This file contains positive tests for create command 4 | 5 | from __future__ import absolute_import 6 | from __future__ import print_function 7 | import os 8 | import sys 9 | # Extend that path so that we can import the module under test. 10 | sys.path.insert(0, os.path.abspath('..')) 11 | 12 | import wavectl 13 | 14 | import unittest 15 | import shutil 16 | import datetime 17 | import tempfile 18 | import time 19 | import copy 20 | import logging 21 | import datetime 22 | import json 23 | import random 24 | import socket 25 | 26 | 27 | from wavectl.BaseWavefrontCommand import BaseWavefrontCommand 28 | from wavectl.Mutator import Mutator 29 | import git 30 | 31 | import util 32 | import wavefront_api_client 33 | 34 | 35 | class Test(util.TestMutate): 36 | 37 | def executeCreate(self, rsrcType, target): 38 | """Call create call on target and return the generated output""" 39 | 40 | args = ["create", target, 41 | "--wavefrontHost", util.wavefrontHostName, 42 | "--apiToken", util.wavefrontApiToken] \ 43 | + [rsrcType] 44 | wc = wavectl.Wavectl(designForTestArgv=args) 45 | 46 | with util.StdoutCapture() as captOut: 47 | wc.runCmd() 48 | 49 | return captOut.str() 50 | 51 | def createSingleRsrc(self, rsrcType, rsrc, expectedOutRegex): 52 | """Test the creation of one rsrc. Write the json blob of the given rsrc 53 | on a file and call create on it. Create function prints out the 54 | summary line for the created rsrc. After create's completion, 55 | compare the stdout""" 56 | 57 | with util.TempDir() as td: 58 | d = td.dir() 59 | 60 | # Write the selected resource to create it in the wavefront 61 | # server via wavectl create. 62 | rt = util.resourceTypeFromString(rsrcType) 63 | targetFile = tempfile.NamedTemporaryFile( 64 | mode="w", dir=d, delete=False, suffix=rt.fileExtension()) 65 | json.dump(rsrc, targetFile) 66 | targetFile.close() # so that writes are flushed. 67 | 68 | out = self.executeCreate(rsrcType, targetFile.name) 69 | 70 | actualOut = out.strip().split("\n") 71 | util.SummaryLineProcessor.compareExpectedActualLineByLine( 72 | self, expectedOutRegex, actualOut) 73 | 74 | def test_createSingleFile(self): 75 | """Using only one file, create the resouces described in that file""" 76 | 77 | expectedOut = [r"Created alert\(s\):"] 78 | expectedOut.append(r"ID\s*NAME\s*STATUS\s*SEVERITY") 79 | rsrcToCreate = util.Alert.kubernetesSkynetTag 80 | expectedOut.append( 81 | util.SummaryLineProcessor.expectedAlertSummaryLineRegex(rsrcToCreate)) 82 | self.createSingleRsrc("alert", rsrcToCreate, expectedOut) 83 | 84 | expectedOut = [r"Created dashboard\(s\):"] 85 | expectedOut.append(r"ID\s*NAME\s*DESCRIPTION") 86 | rsrcToCreate = util.Dashboard.kubeBoxPki 87 | expectedOut.append( 88 | util.SummaryLineProcessor.expectedDashboardSummaryLineRegex(rsrcToCreate)) 89 | self.createSingleRsrc("dashboard", rsrcToCreate, expectedOut) 90 | 91 | 92 | if __name__ == '__main__': 93 | util.initLog() 94 | util.unittestMain() 95 | -------------------------------------------------------------------------------- /wavectl/Dashboard.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import termcolor 4 | import collections 5 | import copy 6 | import abc 7 | from six import with_metaclass 8 | 9 | import wavefront_api_client 10 | 11 | 12 | from .Resource import Resource 13 | 14 | 15 | class Meta(abc.ABCMeta): 16 | def __str__(self): 17 | return "dashboard" 18 | 19 | 20 | class Dashboard(with_metaclass(Meta, Resource)): 21 | """ A class representing a Dashboard""" 22 | 23 | def __str__(self): 24 | s = self.summaryTableRow(False) 25 | return s 26 | 27 | __repr__ = __str__ 28 | 29 | # Some fields in alerts are not interesting. Omit displaying and saving 30 | # them. 31 | _omittedFields = set([ 32 | "viewsLastDay", 33 | "viewsLastMonth", 34 | "viewsLastWeek", 35 | ]) 36 | 37 | # The command line options used to narrow down Alert resources. 38 | _supportedFilters = { 39 | "id": "i", 40 | "name": "n", 41 | "description": "d", 42 | "updaterId": "s", 43 | } 44 | 45 | _summaryTableKeys = ["id", "name", "description"] 46 | _uniqueKey = "id" 47 | _coloredKeys = [] 48 | 49 | @staticmethod 50 | def getFunction(wavefrontClient): 51 | """Given a wavefrontClient object return the function to call to get all 52 | dashboards from the wavefront Api server""" 53 | api = wavefront_api_client.SearchApi(wavefrontClient) 54 | f = api.search_dashboard_entities 55 | return f 56 | 57 | @staticmethod 58 | def getDeletedFunction(wavefrontClient): 59 | """Given a wavefrontClient object return the function to call to get all 60 | deleted dashboards from the wavefront Api server""" 61 | api = wavefront_api_client.SearchApi(wavefrontClient) 62 | f = api.search_dashboard_deleted_entities 63 | return f 64 | 65 | @staticmethod 66 | def updateFunction(wavefrontClient): 67 | """Given a wavefrontClient object return the function to call to update 68 | one dashboard in wavefront api server""" 69 | api = wavefront_api_client.DashboardApi(wavefrontClient) 70 | f = api.update_dashboard 71 | return f 72 | 73 | @staticmethod 74 | def createFunction(wavefrontClient): 75 | """Given a wavefrontClient object return the function to call to create 76 | one new dashboard in wavefront api server""" 77 | api = wavefront_api_client.DashboardApi(wavefrontClient) 78 | f = api.create_dashboard 79 | return f 80 | 81 | @staticmethod 82 | def fileExtension(): 83 | """Return the extension used for the file on disc representing this 84 | resource """ 85 | return ".dashboard" 86 | 87 | def __init__(self, state): 88 | 89 | super(Dashboard, self).__init__(state) 90 | 91 | # Save a reference of self to the static global list. 92 | self.allRsrcs.append(self) 93 | 94 | @classmethod 95 | def fromDict(cls, dict): 96 | """Create a Dashboard object from the given dict dictionary""" 97 | return cls(dict) 98 | 99 | def uniqueId(self): 100 | return self._state[Dashboard._uniqueKey] 101 | 102 | def browserUrlSuffix(self): 103 | return "/dashboard/" + str(self.uniqueId()) 104 | 105 | def summaryTableRow(self, enableColor): 106 | # Return the one line summary string. This string becomes the row in the 107 | # summary table representing this alert. 108 | 109 | fmt = self._summaryTableRowFormat(enableColor) 110 | row = fmt.format( 111 | **{k: self._state.get(k, "") for k in Dashboard._summaryTableKeys}) 112 | return row 113 | -------------------------------------------------------------------------------- /doc/WavectlConfig.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | # Easy configuration of `wavectl` 8 | 9 | During execution, `wavectl` needs to talk to a Wavefront server. The address of the Wavefront Server and the necessary tokens for authorization are required for `wavectl` to work. 10 | 11 | All applicable wavectl [subcommands](CommandReference.md#subcommand-options) accept a `--wavefrontHost` and a `--apiToken` parameter to discover and authenticate to the Wavefront Server. With these command line parameters, the user needs to speficy the server path and the api token for every command. That may result in too much typing at the command line and the user experience may degrade. 12 | 13 | For example: 14 | 15 | ``` 16 | $ wavectl show --wavefrontHost https://acme.wavefront.com --apiToken 98jsb6ef-3939-kk88-8jv2-f84knf71vq68 alert 17 | ID NAME STATUS SEVERITY 18 | 1530723441304 Kubernetes - Node Network Utilization - HIGH (Prod) CHECKING WARN 19 | 1530723441442 Kubernetes - Node Cpu Utilization - HIGH (Prod) CHECKING WARN 20 | 1530723441589 Kubernetes - Node Memory Swap Utilization - HIGH (Prod) SNOOZED WARN 21 | 1530723441737 Wavefront Freshness CHECKING WARN 22 | ... 23 | ``` 24 | 25 | To make `wavectl` more usaable we have added a `config` [subcommand](CommandReference.md#config-options). You only need to execute `wavect config` once and speficy your Wavefront host and api token once. `config` command creates an *unencrypted* file at `${HOME}/.wavectl/config` and saves the specified values there. Any other wavectl [subcommand](CommandReference.md#subcommand-options) after a `wavectl config` execution will use the credentials saved in `${HOME}/.wavectl/config` file. 26 | 27 | For example: 28 | 29 | ``` 30 | $ printf 'https://acme.wavefront.com \n 98jsb6ef-3939-kk88-8jv2-f84knf71vq68 \n' | wavectl config 31 | Wavefront host url: Api token: Writing the following config to the config file at /tmp/Users/someuser/.wavectl/config: 32 | { 33 | "apiToken": " 98jsb6ef-3939-kk88-8jv2-f84knf71vq68 ", 34 | "wavefrontHost": "https://acme.wavefront.com " 35 | } 36 | 37 | $ wavectl show alert 38 | ID NAME STATUS SEVERITY 39 | 1530723441304 Kubernetes - Node Network Utilization - HIGH (Prod) CHECKING WARN 40 | 1530723441442 Kubernetes - Node Cpu Utilization - HIGH (Prod) CHECKING WARN 41 | 1530723441589 Kubernetes - Node Memory Swap Utilization - HIGH (Prod) SNOOZED WARN 42 | 1530723441737 Wavefront Freshness CHECKING WARN 43 | ... 44 | ``` 45 | 46 | The `config` subcommand supports only one set of Wavefront host and api token specification. If your organization has two different Wavefront environments, then you may need to fall back to the `--wavefrontHost` and `--apiToken` command line parameters for the second environment. The command line options take precedence over the values in the `${HOME}/.wavectl/config` file. 47 | -------------------------------------------------------------------------------- /test/test_pushErrors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This file contains negative tests for the push command. 4 | 5 | from __future__ import absolute_import 6 | from __future__ import print_function 7 | import os 8 | import sys 9 | # Extend that path so that we can import the module under test. 10 | sys.path.insert(0, os.path.abspath('..')) 11 | 12 | # Design under test. 13 | import wavectl 14 | 15 | import unittest 16 | import tempfile 17 | import git 18 | import shutil 19 | import copy 20 | import json 21 | import time 22 | 23 | import util 24 | 25 | 26 | class Test(util.TestPullMutate): 27 | """ A class that checks for error conditions in the `push` command""" 28 | 29 | def pushTargetDoesNotExist(self, rsrcType): 30 | """The push command is passed a target that does not exist. 31 | We expect an exception from the wavectl command""" 32 | 33 | nonExistingTarget = "someRandomPathDoesNotExist_lkjshfglkjdsfh" 34 | args = ["push", nonExistingTarget, "--inGit", rsrcType, ] 35 | wc = wavectl.Wavectl(designForTestArgv=args) 36 | 37 | self.assertRaisesRegexp( 38 | wavectl.Mutator.MutatorError, 39 | "The given path: .*" + nonExistingTarget + " does not exist", 40 | wc.runCmd) 41 | 42 | def test_pushTargetDoesNotExist(self): 43 | self.pushTargetDoesNotExist("alert") 44 | self.pushTargetDoesNotExist("dashboard") 45 | 46 | def pushTargetFileIsDirty(self, rsrcType): 47 | """Push is using one file to push. In a repo the pushed single file is 48 | dirty. Push should complain""" 49 | rt = util.resourceTypeFromString(rsrcType) 50 | 51 | with util.TempDir() as td: 52 | d = td.dir() 53 | r = self.repoInit(d) 54 | 55 | self.addReadmeFileToRepo(r) 56 | fileName = "newRsrc" + rt.fileExtension() 57 | self.addNewFileToRepo(r, fileName) 58 | 59 | args = ["push", os.path.join(d, fileName), "--inGit", rsrcType] 60 | 61 | wc = wavectl.Wavectl(designForTestArgv=args) 62 | 63 | self.assertRaisesRegexp( 64 | wavectl.MutatorError, 65 | (r"The path at .+ is dirty. " 66 | r"Please commit your outstanding changes .*"), 67 | wc.runCmd) 68 | 69 | def test_pushTargetFileIsDirty(self): 70 | self.pushTargetFileIsDirty("alert") 71 | self.pushTargetFileIsDirty("dashboard") 72 | 73 | def test_existingRepoDirNotGit(self): 74 | """ The repoDir is an existing directory however it is not a source 75 | controlled directory. The alert pull command should raise an exception""" 76 | self.existingRepoDirNotGit("push", "alert", util.allAlerts) 77 | self.existingRepoDirNotGit("push", "dashboard", util.allDashboards) 78 | 79 | def test_repoIndexIsDirtyInPushDir(self): 80 | """ Attempt to do a push while the given dir has staged but uncommitted 81 | changes""" 82 | self.repoIndexIsDirtyInUsedDir("push", "alert", util.allAlerts, 83 | wavectl.MutatorError) 84 | self.repoIndexIsDirtyInUsedDir("push", "dashboard", util.allDashboards, 85 | wavectl.MutatorError) 86 | 87 | def test_repoWorkingTreeIsDirtyInPushDir(self): 88 | """ Attempt to do a push from a dir that has local modifications. 89 | The push command should not allow for a push with local changes""" 90 | 91 | self.repoWorkingTreeIsDirtyInUsedDir("push", "alert", util.allAlerts, 92 | wavectl.MutatorError) 93 | self.repoWorkingTreeIsDirtyInUsedDir( 94 | "push", "dashboard", util.allDashboards, wavectl.MutatorError) 95 | 96 | 97 | if __name__ == '__main__': 98 | util.unittestMain() 99 | 100 | 101 | # Test Plan 102 | # 1) Given push needs to be renamed to replace. 103 | # Try to update a non-existent resource. The user should use create instead. 104 | -------------------------------------------------------------------------------- /doc/sphinx/RepetitiveEditing.md: -------------------------------------------------------------------------------- 1 | 2 | \# Repetitive editing of alerts, dashboards 3 | 4 | At times one needs to change multiple alerts or various queries in several 5 | dashboards at once. A change in a metric name can cause something like this. 6 | For example the metric generator in kubernetes, 7 | \[kube-state-metrics\]\(https://github.com/kubernetes/kube-state-metrics\), 8 | occasionally changes a metric's 9 | [name](https://github.com/kubernetes/kube-state-metrics/commit/aa8aa440de232fabb86af11c79b410d79dc48367#diff-28d2be13094298bdd1fac746cc0d3361R7). 10 | 11 | After such a change, the platform owner needs to update all alerts and 12 | dashboards with that metric. Searching for that in the Wavefront GUI and 13 | updating the queries manually can be very labor intensive, error prone and time 14 | consuming. \`wavectl\` can help with automating such a global change. 15 | 16 | For the sake of an example, let's say because of an upsteam change, all metrics 17 | that started with \`proc.\` have been renamed to start with \`host.proc.\`. 18 | Once this upstream change gets deployed, numerous alerts and dashboards will be 19 | broken. They will try to display the old metric name and will not show data. 20 | In order to quickly fix this problem via \`wavectl\` we first 21 | \[\`pull\`\]\(CommandReference.md#pull-options\) all alerts and resources that 22 | match the \`proc\\.\` regular expression. The 23 | \[\`--match\`\](CommandReference#resource-options) option can be used to narrow 24 | down the returned set via a regular expression search. 25 | 26 | 27 | 28 | 29 | ```eval_rst 30 | .. program-output:: rm -rf /tmp/RepetitiveEditing 31 | :returncode: 0 32 | ``` 33 | 34 | ```eval_rst 35 | .. program-output:: wavectl pull /tmp/RepetitiveEditing/alerts alert --match "proc\." 36 | :returncode: 0 37 | :prompt: 38 | ``` 39 | 40 | ```eval_rst 41 | .. program-output:: wavectl pull /tmp/RepetitiveEditing/dashboards dashboard --match "proc\." 42 | :returncode: 0 43 | :prompt: 44 | ``` 45 | 46 | See the pulled alerts, dashboards. 47 | 48 | ```eval_rst 49 | .. program-output:: find /tmp/RepetitiveEditing -type f 50 | :returncode: 0 51 | :prompt: 52 | :shell: 53 | ``` 54 | 55 | See the usage of the metrics starting with \`proc.\` in pulled alerts, dashboards. 56 | 57 | ```eval_rst 58 | .. program-output:: find /tmp/RepetitiveEditing -type f | xargs grep "proc." 59 | :returncode: 0 60 | :prompt: 61 | :shell: 62 | :ellipsis: 15 63 | ``` 64 | 65 | Then using \[\`sed\`\]\(https://www.gnu.org/software/sed/manual/sed.html\) 66 | replace all occurances of \`proc.\` with \`host.proc.\` 67 | 68 | 69 | ```eval_rst 70 | .. program-output:: find /tmp/RepetitiveEditing -type f | xargs sed -i -e 's/proc\./host.proc./g' 71 | :returncode: 0 72 | :prompt: 73 | :shell: 74 | ``` 75 | 76 | 77 | 78 | Check the changes you have make 79 | 80 | ```eval_rst 81 | .. program-output:: find /tmp/RepetitiveEditing -type f | xargs grep "host.proc." 82 | :returncode: 0 83 | :prompt: 84 | :shell: 85 | :ellipsis: 15 86 | ``` 87 | 88 | Replace the Wavefront alerts and dashboards using 89 | \[\`wavectl push\`\]\(CommandReference.md#push-options\) 90 | 91 | ```eval_rst 92 | .. program-output:: wavectl push /tmp/RepetitiveEditing/alerts alert 93 | :returncode: 0 94 | :prompt: 95 | ``` 96 | 97 | ```eval_rst 98 | .. program-output:: wavectl push /tmp/RepetitiveEditing/dashboards dashboard 99 | :returncode: 0 100 | :prompt: 101 | ``` 102 | 103 | After these steps all your alerts and dashboards in Wavefront will use the 104 | new metric names. 105 | 106 | \> NOTE: Doing local modifications via \`sed\` like commands and writing the 107 | resulting files to Wavefront may be risky and dangerous. Some unintended 108 | changes may be written to Wavefront by mistake. If you want to execute safer 109 | local modifications, where you have a better handle on the resulting diff, take 110 | a look at the \[git integration to push command\]\(GitIntegration.md\) section. 111 | 112 | -------------------------------------------------------------------------------- /doc/AdvancedGrep.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | # Advanced grep in your alerts and dashboards. 8 | 9 | `wavectl` allows users to execute regular expression searches on alerts and dashboards. Using the [resource options](CommandReference.md#resource-options), the user can specify fields and a regular expression to match for each field. `wavectl` will process only resources that satisfy all specified regular expressions. For `show` command, only the matched alerts/dashboards will be displayed, for `push` only the matching ones will be written to Wavefront, and so on. 10 | 11 | For example: show alerts that have "Kubernetes" and "Utilization" in their names: 12 | 13 | ``` 14 | $ wavectl show alert --name "Kubernetes.*Utilization" 15 | ID NAME STATUS SEVERITY 16 | 1530723441304 Kubernetes - Node Network Utilization - HIGH (Prod) CHECKING WARN 17 | 1530723441442 Kubernetes - Node Cpu Utilization - HIGH (Prod) CHECKING WARN 18 | 1530723441589 Kubernetes - Node Memory Swap Utilization - HIGH (Prod) SNOOZED WARN 19 | ``` 20 | 21 | The `--match` parameter can be used to search anywhere in the json representation of an alert or a dashboard rather than in known key value pairs. 22 | 23 | For example: Show the dashboards that use metrics from the live environment 24 | 25 | ``` 26 | $ wavectl show dashboard --match "env=live" 27 | ID NAME DESCRIPTION 28 | metadata-php Metadata PHP Monitors for Metadata in the PHP webapp 29 | octoproxy Skynet Octoproxy One look summary about the load balancer 30 | skynet-kube-box-pki SKYNET KUBE BOX PKISomeNameToBeMachedInKubeBoxPki Info about PKI system. 31 | ``` 32 | 33 | Write alerts back to Wavefront that have a specific person in the `updaterId` 34 | 35 | ``` 36 | $ wavectl push /tmp/AdvancedGrep/dashboards dashboard --updaterId hbaba 37 | Replaced dashboard(s): 38 | ID NAME DESCRIPTION 39 | SKYNET-OCTOPROXY-DEV Skynet Octoproxy Dev One look summary about the load balancer 40 | data-retention Data Retention Info about Data 41 | jgranger001 Skynet Monitoring (Cloned)SomeMatchStringSomeMatchString 42 | metadata-operations Metadata Operations Metrics about each operation that can be performed against the data store 43 | metadata-perfpod Metadata PerfPod Monitors for testing Metadata in the PerfPods 44 | metadata-php Metadata PHP Monitors for Metadata in the PHP webapp 45 | ... 46 | ``` 47 | 48 | `wavectl` uses python standard library's regular expression [module](https://docs.python.org/3.4/library/re.html). Any valid python regular expression can be specified for the [resource options](CommandReference.md#resource-options). 49 | -------------------------------------------------------------------------------- /doc/sphinx/CommandLine.md: -------------------------------------------------------------------------------- 1 | 2 | \# Command line operations on your alerts, dashboards 3 | 4 | In this document we describe general tasks using the command line that you can 5 | accomplish with \`wavectl\` using your alerts and dashboards. 6 | 7 | Many of these examples can be accomplished by using the Wavefront gui too. 8 | \`wavectl\` enables accomplishing these tasks in a command line interface and in 9 | conjunction with powerful tools like \`grep\`, \`awk\`, \`sed\`, 10 | \[\`jq\`\]\(https://stedolan.github.io/jq/\), or similar 11 | 12 | 13 | \#\# Print one line summaries of your alerts, dashboards. 14 | 15 | \`wavectl\` can be used to list all alerts in a one line summary form. 16 | 17 | For example: 18 | 19 | 20 | ```eval_rst 21 | .. program-output:: wavectl show alert 22 | :prompt: 23 | :ellipsis: 5 24 | :returncode: 0 25 | ``` 26 | 27 | The short summary form contains the \[alert 28 | state\]\(https://docs.wavefront.com/alerts_states_lifecycle.html#alert-states\) 29 | and the severity in addition to the name and unique id of the alert. Once you have a 30 | structured columned print of your alerts, you can do all sorts of processing with them. 31 | 32 | For example: 33 | 34 | \#\#\# Find the alerts firing right now. 35 | 36 | ```eval_rst 37 | .. program-output:: wavectl show alert | grep FIRING 38 | :prompt: 39 | :shell: 40 | :returncode: 0 41 | ``` 42 | 43 | This could be used from a script too. For example, an operator may want to ensure 44 | no alerts from "kubernetes" are firing before executing a script that is going to 45 | downtime one of the kubernetes control plane hosts. 46 | 47 | \#\#\# Count the total number of alerts in your organization. 48 | 49 | ```eval_rst 50 | .. program-output:: wavectl show --no-header alert | wc -l 51 | :prompt: 52 | :shell: 53 | :returncode: 0 54 | ``` 55 | 56 | 57 | \#\# Inspect all attributes of alerts, dashboards. 58 | 59 | In addition to printing one line summaries the \`show\` command can also print 60 | detailed state of your alerts in json form: 61 | 62 | ```eval_rst 63 | .. program-output:: wavectl show -o json alert 64 | :prompt: 65 | :returncode: 0 66 | :ellipsis: 19 67 | ``` 68 | 69 | One you have an easy way to retrieve the json representation of alerts, dashboards, 70 | this can lead to various powerful use cases with using text processing tools like 71 | \[\`jq\`\]\(https://stedolan.github.io/jq/\) or grep. For example: 72 | 73 | \#\#\# Print the name and the 74 | \[condition\]\(https://docs.wavefront.com/alerts_states_lifecycle.html#alert-conditions\) for 75 | each alert. 76 | 77 | ```eval_rst 78 | .. program-output:: wavectl show -o json alert | jq '{name,condition}' 79 | :prompt: 80 | :shell: 81 | :returncode: 0 82 | :ellipsis: 11 83 | ``` 84 | 85 | 86 | \#\#\# See the existing usages of a particular metric. 87 | 88 | You may want to see a metric's usages in all dashboard queries. You may be 89 | unsure about the semantics of a metric and seeing its correct usages definitely 90 | helps. 91 | 92 | Dashboards' json state can be inspected similarly to alerts. Seeing all dashboard 93 | queries regarding haproxy backends: 94 | 95 | ```eval_rst 96 | .. program-output:: wavectl show -o json dashboard | grep haproxy_backend 97 | :prompt: 98 | :shell: 99 | :returncode: 0 100 | ``` 101 | 102 | \#\#\# See existing usages of advanced Wavefront functions. 103 | 104 | Some advanced functions in \[Wavefront query 105 | language\]\(https://docs.wavefront.com/query_language_reference.html\) are not 106 | the easiest to learn. It is always helpful to see existing 107 | usages of a Wavefront function by your colleagues before writing your own. Take 108 | the \[taggify\]\(https://docs.wavefront.com/ts_taggify.html\) as an example. 109 | 110 | ```eval_rst 111 | .. program-output:: wavectl show -o json dashboard | grep taggify 112 | :prompt: 113 | :shell: 114 | :returncode: 0 115 | ``` 116 | 117 | \> After textually inspecting the alert, dashboard state you may want to jump to 118 | the Wavefront gui and see the time series there. For that you can use the wavectl 119 | \[browser integration\]\(BrowserIntegration.md\). 120 | 121 | 122 | \#\#\# See all sections in all your dashboards. 123 | 124 | 125 | ```eval_rst 126 | .. program-output:: wavectl show -o json dashboard | jq '{name: .name, sections: [.sections[].name]}' 127 | :prompt: 128 | :shell: 129 | :returncode: 0 130 | :ellipsis: 19 131 | ``` 132 | 133 | 134 | -------------------------------------------------------------------------------- /test/test_pullIntoRepoDir.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Tests for pulling into a git repo dir. The directory hosts a git repo 4 | 5 | from __future__ import absolute_import 6 | from __future__ import print_function 7 | import os 8 | import sys 9 | # Extend that path so that we can import the module under test. 10 | sys.path.insert(0, os.path.abspath('..')) 11 | 12 | import wavectl 13 | 14 | import unittest 15 | import shutil 16 | import datetime 17 | import tempfile 18 | import time 19 | import copy 20 | import logging 21 | 22 | 23 | import git 24 | 25 | import util 26 | 27 | import wavefront_api_client 28 | 29 | 30 | class Test(util.TestPull): 31 | 32 | def existingRepoDir(self, rsrcType, rsrcs): 33 | """The pull command is passed a directory that is an already 34 | initialized empty git repo. Command impl should use that directory 35 | and add files to it.""" 36 | with util.TempDir() as td: 37 | d = td.dir() 38 | r = self.repoInit(d) 39 | 40 | self.addReadmeFileToRepo(r) 41 | self.createPullBranch(r, wavectl.PullCommand.datetimeFormat, 42 | wavectl.PullCommand.pullBranchSuffix) 43 | 44 | self.executePull(rsrcType, d, r, rsrcs, 45 | pullAdditionalParams=["--inGit"]) 46 | 47 | def test_existingRepoDir(self): 48 | self.existingRepoDir("alert", util.allAlerts) 49 | self.existingRepoDir("dashboard", util.allDashboards) 50 | 51 | def newRepoDir(self, rsrcType, rsrcs, nested=False): 52 | """The pull command is passed a path that does not exist yet. 53 | The impl should create a git repo there and check in files. If nested 54 | is true, the pulled dir has other intermediate directories that also do 55 | not exist.""" 56 | 57 | d = tempfile.mkdtemp() 58 | shutil.rmtree(d, ignore_errors=True) 59 | 60 | pulledDir = d 61 | if nested: 62 | # We want to use a nested pulledDir. Have another intermediate 63 | # directory that does not exist. 64 | pulledDir = os.path.join(d, "leafDir") 65 | 66 | self.executePull(rsrcType, pulledDir, None, rsrcs, 67 | pullAdditionalParams=["--inGit"]) 68 | 69 | r = git.Repo(pulledDir) 70 | 71 | shutil.rmtree(d, ignore_errors=True) 72 | 73 | def test_newRepoDir(self): 74 | self.newRepoDir("alert", util.allAlerts) 75 | self.newRepoDir("dashboard", util.allDashboards) 76 | 77 | def test_newNestedRepoDir(self): 78 | self.newRepoDir("alert", util.allAlerts, nested=True) 79 | self.newRepoDir("dashboard", util.allDashboards, nested=True) 80 | 81 | (existingDir, newDir) = range(2) 82 | 83 | def pullIntoSubdirInRepo(self, rsrcType, rsrcs, subdirState): 84 | """The given directory is to a subdir of a git repo. Depending on the 85 | subdirState parameter, the pull happens on an existing subdir or on a 86 | new subdir.""" 87 | 88 | with util.TempDir() as td: 89 | d = td.dir() 90 | r = self.repoInit(d) 91 | 92 | self.addReadmeFileToRepo(r) 93 | self.createPullBranch(r, wavectl.PullCommand.datetimeFormat, 94 | wavectl.PullCommand.pullBranchSuffix) 95 | 96 | subdirName = "subdir" 97 | subdir = os.path.join(d, subdirName) 98 | 99 | if subdirState == self.existingDir: 100 | os.mkdir(subdir) 101 | 102 | self.executePull(rsrcType, subdir, r, rsrcs, 103 | pullAdditionalParams=["--inGit"]) 104 | 105 | repoInSubdir = git.Repo(subdir, search_parent_directories=True) 106 | 107 | self.assertEqual(r.working_tree_dir, repoInSubdir.working_tree_dir) 108 | 109 | def test_pullIntoSubdirInRepo(self): 110 | self.pullIntoSubdirInRepo("alert", 111 | util.allAlerts, 112 | self.existingDir) 113 | self.pullIntoSubdirInRepo("alert", 114 | util.allAlerts, 115 | self.newDir) 116 | 117 | self.pullIntoSubdirInRepo("dashboard", 118 | util.allDashboards, 119 | self.existingDir) 120 | self.pullIntoSubdirInRepo("dashboard", 121 | util.allDashboards, 122 | self.newDir) 123 | 124 | 125 | if __name__ == '__main__': 126 | # util.initLog() 127 | util.unittestMain() 128 | -------------------------------------------------------------------------------- /doc/sphinx/README.md: -------------------------------------------------------------------------------- 1 | 2 | \# wavectl 3 | 4 | \[!\[CircleCI\]\(https://circleci.com/gh/box/wavectl.svg?style=svg\)\]\(https://circleci.com/gh/box/wavectl\) 5 | \[!\[Project Status\]\(http://opensource.box.com/badges/active.svg\)\]\(http://opensource.box.com/badges\) 6 | 7 | A command line client for \[Wavefront\]\(https://www.wavefront.com\) inspired by 8 | \[kubectl\]\(https://kubernetes.io/docs/reference/kubectl/overview/\) and 9 | \[git\]\(https://git-scm.com/docs\) command line tools. 10 | 11 | 12 | \#\# Example Commands 13 | 14 | A short list of common usages. For more details \[Use Cases\]\(#example-use-cases\) section. 15 | 16 | 17 | \#\#\# Show one line summaries for Wavefront alerts 18 | 19 | 20 | ```eval_rst 21 | .. program-output:: wavectl show alert 22 | :prompt: 23 | :returncode: 0 24 | :ellipsis: 4 25 | ``` 26 | 27 | \#\#\# Show json state of alerts 28 | 29 | 30 | ```eval_rst 31 | .. program-output:: wavectl show -o json alert 32 | :prompt: 33 | :returncode: 0 34 | :ellipsis: 19 35 | ``` 36 | 37 | 38 | 39 | \#\#\# Modify a dashboard's json and write it back to Wavefront 40 | 41 | 42 | 43 | ``` 44 | $> vim ./metadata-dashboard.json # Modify the json state of a dashboard 45 | $> wavectl push ./metadata-dashboard.json dashboard # Write the new version to Wavefront 46 | ``` 47 | 48 | ```eval_rst 49 | .. program-output:: mutateDashboards.py 50 | :returncode: 0 51 | ``` 52 | 53 | 54 | \#\# Example Use Cases 55 | 56 | \- \[Command line operations on your alerts, dashboards\]\(doc/CommandLine.md\) 57 | 58 | \- \[Advanced grep in your alerts and dashboards\]\(doc/AdvancedGrep.md\) 59 | 60 | \- \[Launch Wavefront GUI via \`wavectl\`\]\(doc/BrowserIntegration.md\) 61 | 62 | \- \[Repetitive editing of alerts, dashboards\]\(doc/RepetitiveEditing.md\) 63 | 64 | \- \[Simple templating of alerts, dashboards with \`wavectl\`\]\(doc/Templating.md\) 65 | 66 | \- \[Git integration\]\(doc/GitIntegration.md\) 67 | 68 | \- \[Easy configuration of \`wavectl\`\]\(doc/WavectlConfig.md\) 69 | 70 | 71 | 72 | \#\# \[Command Reference\]\(doc/CommandReference.md\) 73 | 74 | 75 | \#\# Installation 76 | 77 | To install the latest release: 78 | 79 | ``` 80 | pip install wavectl 81 | ``` 82 | 83 | To install from the master branch in github: 84 | 85 | ``` 86 | pip install git+https://github.com/box/wavectl.git 87 | ``` 88 | 89 | The master branch may contain unreleased features or bug fixes. The master branch 90 | \*should\* always stay stable. 91 | 92 | \#\# A note about Performance 93 | 94 | \`wavectl\`'s execution time depends on the number of alerts or dashboards you 95 | have in Wavefront. All 96 | \[resource filtering\]\(doc/CommandReference.md#resource-options\) except the 97 | \`--customerTag, -t\` option is done on the client side. This enables the 98 | powerful regular expression matching on your results. But if your organization 99 | has thousands of alerts and dashboards, the data size may overwhelm the 100 | \`wavectl\` execution time. 101 | 102 | If your organization has a lot of alerts and dashboards in Wavefront we 103 | strongly recommend to use \`--customerTag\` option in your commands. The 104 | filtering based on customerTag is done on the Wavefront server side. With 105 | \`--customerTags\` option, wavectl client will only receive data about 106 | alerts/dashboards if they are tagged with all of the specified tags. This 107 | reduces the data size processed by wavectl and results in faster execution. 108 | 109 | \#\# Notes 110 | 111 | If you could not find what you were looking for please consider 112 | \[contributing\]\(CONTRIBUTING.md\). You could also take a look at 113 | \[another\]\(https://github.com/wavefrontHQ/ruby-client/blob/master/README-cli.md\) 114 | CLI implementation for Wavefront. That one is written by Wavefront and mirrors their 115 | web api more closely. This \`wavectl\` CLI has evolved from our use cases. 116 | 117 | \`wavectl\` is designed to add automation, command line access to Wavefront 118 | data that is \*\*human generated\*\*. Initial examples are alerts and 119 | dashboards. We see those as more permanent, slow changing state in Wavefront. 120 | \`wavectl\` is not optimized to read, write time series data to Wavefront or 121 | any other data that is ingested by Wavefront at real time production workload 122 | scale. 123 | 124 | \#\# Support 125 | 126 | Need to contact us directly? Email oss@box.com and be sure to include the name 127 | of this project in the subject. 128 | 129 | \#\# Copyright and License 130 | 131 | 132 | Copyright 2018 Box, Inc. All rights reserved. 133 | 134 | Licensed under the Apache License, Version 2.0 (the "License"); 135 | you may not use this file except in compliance with the License. 136 | You may obtain a copy of the License at 137 | 138 | ``` 139 | http://www.apache.org/licenses/LICENSE-2.0 140 | ``` 141 | 142 | Unless required by applicable law or agreed to in writing, software 143 | distributed under the License is distributed on an "AS IS" BASIS, 144 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 145 | See the License for the specific language governing permissions and 146 | limitations under the License. 147 | -------------------------------------------------------------------------------- /wavectl/wavectl.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import absolute_import 3 | from __future__ import print_function 4 | import argparse 5 | import sys 6 | import logging 7 | import http.client 8 | 9 | import argcomplete 10 | 11 | from .Config import ConfigCommand 12 | from .Show import ShowCommand 13 | from .Pull import PullCommand 14 | from .Push import PushCommand 15 | from .Create import CreateCommand 16 | 17 | from .Resource import Resource 18 | 19 | import wavefront_api_client 20 | 21 | 22 | class Wavectl(object): 23 | 24 | @staticmethod 25 | def enableChildLogger(loggerName, fmtString, level): 26 | """Get the logger interface by name and enable it with the given level""" 27 | 28 | childLogger = logging.getLogger(loggerName) 29 | 30 | # Add a handler so we have the output formatter to our liking. 31 | handler = logging.StreamHandler() # By default the output goes to stderr 32 | handler.setFormatter(logging.Formatter(fmtString)) 33 | childLogger.addHandler(handler) 34 | 35 | childLogger.setLevel(level) 36 | # The child logger has an handler already. No need to propagate further 37 | # to parent loggers. 38 | childLogger.propagate = False 39 | 40 | @staticmethod 41 | def initLog(level): 42 | """Init the basic logging""" 43 | 44 | # TODO: We should instantiate a logger here and set its parameters. 45 | fmtString = "%(asctime)s %(process)d %(threadName)s %(name)s " \ 46 | + "%(levelname)s %(message)s" 47 | logging.basicConfig( 48 | format=fmtString, 49 | stream=sys.stderr, 50 | level=level) 51 | 52 | # TODO: wavefront_api_client.Configuration() constructor clears the 53 | # HTTPConnection.debuglevel flag. This may turned off later on. 54 | if level <= logging.DEBUG: 55 | http.client.HTTPConnection.debuglevel = 1 56 | 57 | Wavectl.enableChildLogger("wavefront_api_client", fmtString, level) 58 | Wavectl.enableChildLogger("urllib3", fmtString, level) 59 | Wavectl.enableChildLogger( 60 | "wavefront_api_client.rest", fmtString, level) 61 | 62 | def __init__( 63 | self, 64 | designForTestArgv=None, 65 | designForTestRsrcs=None, 66 | designForTestConfigDir=None): 67 | """ 68 | designForTestArgv: In unit tests, one can overwrite the command 69 | line arguments for better testability. 70 | designForTestRsrcs: One can make the Wavefront class bypass reaching out 71 | the wavefront api server and directly use the given alert list 72 | """ 73 | 74 | parser = argparse.ArgumentParser( 75 | prog="wavectl", 76 | description="A command line tool to programmatically interact " 77 | + "with wavefront") 78 | 79 | parser.add_argument( 80 | "--log", 81 | default="WARNING", 82 | choices=[ 83 | "DEBUG", 84 | "INFO", 85 | "WARNING", 86 | "ERROR", 87 | "CRITICAL"], 88 | help="""Set the logging level for the command""") 89 | 90 | subParsers = parser.add_subparsers( 91 | title="subcommands", 92 | description="Choose a subcommand to execute", 93 | dest="subcommands") 94 | subParsers.required = True 95 | 96 | # For each new command, add a new call here 97 | # TODO: Is there a more automatic/pythonic way of doing this ? 98 | self.config = ConfigCommand( 99 | designForTestConfigDir=designForTestConfigDir) 100 | self.config.addCmd(subParsers) 101 | 102 | self.show = ShowCommand( 103 | designForTestRsrcs=designForTestRsrcs, 104 | designForTestConfigDir=designForTestConfigDir) 105 | self.show.addCmd(subParsers) 106 | 107 | self.pull = PullCommand( 108 | designForTestRsrcs=designForTestRsrcs, 109 | designForTestConfigDir=designForTestConfigDir) 110 | self.pull.addCmd(subParsers) 111 | 112 | self.push = PushCommand(designForTestConfigDir=designForTestConfigDir) 113 | self.push.addCmd(subParsers) 114 | 115 | self.create = CreateCommand( 116 | designForTestConfigDir=designForTestConfigDir) 117 | self.create.addCmd(subParsers) 118 | 119 | # You need to execute 120 | # eval "$(register-python-argcomplete wavectl)" 121 | # for the autocompletion to effect. 122 | argcomplete.autocomplete(parser) 123 | 124 | self.args = parser.parse_args(args=designForTestArgv) 125 | 126 | Wavectl.initLog(getattr(logging, self.args.log)) 127 | 128 | def runCmd(self): 129 | """ Executes the function that handles the operation described by the 130 | command line arguments""" 131 | 132 | # TODO: This is a design for testability addition. Normally the static list 133 | # in the Resource type will always be empty at the start. 134 | # However during tests, we re-use the same instance to execute multiple 135 | # test scenarios. This cleanup serves as deleting the state from the 136 | # previous test run. 137 | Resource.allRsrcs = [] 138 | Resource._summaryTableFormat = { 139 | True: None, 140 | False: None, 141 | } 142 | 143 | self.args.wavefrontConfigFuncToCall(self.args) 144 | -------------------------------------------------------------------------------- /wavectl/BaseCommand.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import absolute_import 3 | from __future__ import print_function 4 | import json 5 | import os 6 | import threading 7 | import errno 8 | 9 | 10 | class ConfigError(Exception): 11 | pass 12 | 13 | 14 | class BaseCommand(object): 15 | """ A base class for all commands in wavectl""" 16 | 17 | # A global lock used for critical section protection 18 | 19 | _lock = threading.Lock() 20 | 21 | class LockAnchor(object): 22 | """ And Anchor type (RAII pattern) that acquires a lock at the 23 | context creation time and releases it at the exit time""" 24 | 25 | def __init__(self): 26 | pass 27 | 28 | def __enter__(self): 29 | BaseCommand._lock.acquire() 30 | 31 | def __exit__(self, type, value, traceback): 32 | BaseCommand._lock.release() 33 | 34 | # The directory and name of the config file. By default the config file is 35 | # located at ~/.wavectl/config. 36 | configDirPath = os.path.expanduser("~/.wavectl") 37 | configFileName = "config" 38 | 39 | # The key names used in the config dictionary. 40 | wavefrontHostKey = "wavefrontHost" 41 | apiTokenKey = "apiToken" 42 | 43 | wavefrontHostOptionName = "--" + wavefrontHostKey 44 | apiTokenOptionName = "--" + apiTokenKey 45 | 46 | @staticmethod 47 | def makeDirsIgnoreExisting(p): 48 | """unix mkdir -p functionality. Creates the directory given in the path 49 | p. Also creates the intermediate directories. Does not complain if the 50 | root directory exists already.""" 51 | try: 52 | os.makedirs(p) 53 | except OSError as e: 54 | if e.errno == errno.EEXIST and os.path.isdir(p): 55 | pass 56 | else: 57 | raise 58 | 59 | def _checkConfig(self): 60 | """Verify that the final compiled config has all the compulsory fields 61 | speficied. A config must have a wavefrontHost and an apiToken raise 62 | an exception if none of them exist""" 63 | 64 | if self.config.get(self.wavefrontHostKey) is None: 65 | raise ConfigError( 66 | ("The wavefront host url is not known. Either execute " 67 | "`wavectl config ...` or pass {0} command line option").format( 68 | self.wavefrontHostOptionName)) 69 | if self.config.get(self.apiTokenKey) is None: 70 | raise ConfigError( 71 | ("The wavefront api token is not known. Either execute " 72 | "`wavectl config ...` or pass {0} command line option").format( 73 | self.apiTokenOptionName)) 74 | 75 | def _getConfig(self): 76 | """ Read the config file (if exists) and use the command line flags to 77 | populate the self.config if it is not known already. If the required 78 | information is not known, raise an exception.""" 79 | 80 | # This function may be called from multiple threads at once. 81 | # So make sure only one instance is running at a time 82 | # This function modifies state in the 83 | with BaseCommand.LockAnchor(): 84 | if self.config is not None: 85 | # Config file has been read already 86 | return 87 | 88 | newConfig = {} 89 | fp = os.path.join(self.configDirPath, self.configFileName) 90 | if os.path.isfile(fp): 91 | with open(fp) as f: 92 | newConfig = json.load(f) 93 | 94 | newConfig.update(self.configFromCommandLine) 95 | 96 | self.config = newConfig 97 | 98 | self._checkConfig() 99 | 100 | def handleCmd(self, args): 101 | """ Extact the relevant commands from the command line and save in 102 | static variables. They will be used and evaluated later""" 103 | if args.wavefrontHost: 104 | self.configFromCommandLine[self.wavefrontHostKey] = args.wavefrontHost 105 | if args.apiToken: 106 | self.configFromCommandLine[self.apiTokenKey] = args.apiToken 107 | 108 | def addCmd(self, parser): 109 | """ Adds the command line parameters for specifying the wavefront host 110 | and the apiToken from the command line. The options from the command line 111 | take precedence over the ones in the config file""" 112 | 113 | parser.add_argument( 114 | self.wavefrontHostOptionName, 115 | help="""Speficy the url of the wavefront host. If specified, this 116 | takes precedence over the config file entry.""") 117 | parser.add_argument( 118 | self.apiTokenOptionName, 119 | help="""Speficy the api token to use while communicating with the 120 | wavefront host. If specified, this takes precedence over the config 121 | file entry.""") 122 | 123 | def __init__(self, designForTestConfigDir=None): 124 | # The config for the wavectl command. Either read from the 125 | # config file on demand or set by the user with `wavectl config` 126 | # command 127 | 128 | if designForTestConfigDir: 129 | # Design for testability. We may want to change the 130 | # config dir location to have better test visibility. 131 | self.configDirPath = designForTestConfigDir 132 | 133 | self.config = None 134 | # The config values if speficied from the command line. 135 | self.configFromCommandLine = {} 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | # wavectl 8 | 9 | [![CircleCI](https://circleci.com/gh/box/wavectl.svg?style=svg)](https://circleci.com/gh/box/wavectl) [![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges) 10 | 11 | A command line client for [Wavefront](https://www.wavefront.com) inspired by [kubectl](https://kubernetes.io/docs/reference/kubectl/overview/) and [git](https://git-scm.com/docs) command line tools. 12 | 13 | ## Example Commands 14 | 15 | A short list of common usages. For more details [Use Cases](#example-use-cases) section. 16 | 17 | ### Show one line summaries for Wavefront alerts 18 | 19 | ``` 20 | $ wavectl show alert 21 | ID NAME STATUS SEVERITY 22 | 1530723441304 Kubernetes - Node Network Utilization - HIGH (Prod) CHECKING WARN 23 | 1530723441442 Kubernetes - Node Cpu Utilization - HIGH (Prod) CHECKING WARN 24 | 1530723441589 Kubernetes - Node Memory Swap Utilization - HIGH (Prod) SNOOZED WARN 25 | ... 26 | ``` 27 | 28 | ### Show json state of alerts 29 | 30 | ``` 31 | $ wavectl show -o json alert 32 | { 33 | "additionalInformation": "This alert tracks the used network bandwidth percentage for all the compute-* (compute-master and compute-node) machines. If the cpu utilization exceeds 80%, this alert fires.", 34 | "condition": "ts(proc.net.percent,server_type=\"compute-*\" and env=\"live\") > 80", 35 | "displayExpression": "ts(proc.net.percent,server_type=\"compute-*\" and env=\"live\")", 36 | "id": "1530723441304", 37 | "minutes": 2, 38 | "name": "Kubernetes - Node Network Utilization - HIGH (Prod)", 39 | "resolveAfterMinutes": 2, 40 | "severity": "WARN", 41 | "tags": { 42 | "customerTags": [ 43 | "kubernetes", 44 | "skynet" 45 | ] 46 | }, 47 | "target": "pd: 07fe9ebacf8c44e881ea2f6e44dbf2d2" 48 | } 49 | { 50 | "additionalInformation": "This alert tracks the used cpu percentage for all the compute-* (compute-master and compute-node) machines. If the cpu utilization exceeds 80%, this alert fires.", 51 | ... 52 | ``` 53 | 54 | ### Modify a dashboard's json and write it back to Wavefront 55 | 56 | ``` 57 | $> vim ./metadata-dashboard.json # Modify the json state of a dashboard 58 | $> wavectl push ./metadata-dashboard.json dashboard # Write the new version to Wavefront 59 | 60 | Replaced dashboard(s): 61 | ID NAME DESCRIPTION 62 | metadata-php Metadata PHP Monitors for Metadata in the PHP webapp 63 | ``` 64 | 65 | ## Example Use Cases 66 | 67 | - [Command line operations on your alerts, dashboards](doc/CommandLine.md) 68 | 69 | - [Advanced grep in your alerts and dashboards](doc/AdvancedGrep.md) 70 | 71 | - [Launch Wavefront GUI via `wavectl`](doc/BrowserIntegration.md) 72 | 73 | - [Repetitive editing of alerts, dashboards](doc/RepetitiveEditing.md) 74 | 75 | - [Simple templating of alerts, dashboards with `wavectl`](doc/Templating.md) 76 | 77 | - [Git integration](doc/GitIntegration.md) 78 | 79 | - [Easy configuration of `wavectl`](doc/WavectlConfig.md) 80 | 81 | ## [Command Reference](doc/CommandReference.md) 82 | 83 | ## Installation 84 | 85 | To install the latest release: 86 | 87 | ``` 88 | pip install wavectl 89 | ``` 90 | 91 | To install from the master branch in github: 92 | 93 | ``` 94 | pip install git+https://github.com/box/wavectl.git 95 | ``` 96 | 97 | The master branch may contain unreleased features or bug fixes. The master branch *should* always stay stable. 98 | 99 | ## A note about Performance 100 | 101 | `wavectl`'s execution time depends on the number of alerts or dashboards you have in Wavefront. All [resource filtering](doc/CommandReference.md#resource-options) except the `--customerTag, -t` option is done on the client side. This enables the powerful regular expression matching on your results. But if your organization has thousands of alerts and dashboards, the data size may overwhelm the `wavectl` execution time. 102 | 103 | If your organization has a lot of alerts and dashboards in Wavefront we strongly recommend to use `--customerTag` option in your commands. The filtering based on customerTag is done on the Wavefront server side. With `--customerTags` option, wavectl client will only receive data about alerts/dashboards if they are tagged with all of the specified tags. This reduces the data size processed by wavectl and results in faster execution. 104 | 105 | ## Notes 106 | 107 | If you could not find what you were looking for please consider [contributing](CONTRIBUTING.md). You could also take a look at [another](https://github.com/wavefrontHQ/ruby-client/blob/master/README-cli.md) CLI implementation for Wavefront. That one is written by Wavefront and mirrors their web api more closely. This `wavectl` CLI has evolved from our use cases. 108 | 109 | `wavectl` is designed to add automation, command line access to Wavefront data that is **human generated**. Initial examples are alerts and dashboards. We see those as more permanent, slow changing state in Wavefront. `wavectl` is not optimized to read, write time series data to Wavefront or any other data that is ingested by Wavefront at real time production workload scale. 110 | 111 | ## Support 112 | 113 | Need to contact us directly? Email oss@box.com and be sure to include the name of this project in the subject. 114 | 115 | ## Copyright and License 116 | 117 | Copyright 2018 Box, Inc. All rights reserved. 118 | 119 | Licensed 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 120 | 121 | ``` 122 | http://www.apache.org/licenses/LICENSE-2.0 123 | ``` 124 | 125 | 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. 126 | -------------------------------------------------------------------------------- /wavectl/Show.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import absolute_import 3 | from __future__ import print_function 4 | import sys 5 | import os 6 | import json 7 | import webbrowser 8 | 9 | # External dependencies: 10 | import termcolor 11 | 12 | from .BaseWavefrontCommand import BaseWavefrontCommand 13 | from .Resource import Resource 14 | from .ResourceFactory import ResourceFactory 15 | 16 | class ShowError(Exception): 17 | pass 18 | 19 | 20 | class ShowCommand(BaseWavefrontCommand): 21 | """The implementation of the `show` command """ 22 | 23 | _maxBrowserTabsEnvVarName = "MAX_BROWSER_TABS" 24 | 25 | @staticmethod 26 | def _maxBrowserTabs(): 27 | """ Return the maximum number of browser tabs to spawn""" 28 | maxTabsStr = os.environ.get( 29 | ShowCommand._maxBrowserTabsEnvVarName, "10") 30 | try: 31 | rv = int(maxTabsStr) 32 | except ValueError: 33 | rv = 10 34 | 35 | return rv 36 | 37 | @staticmethod 38 | def _isTermColoringEnabled(color): 39 | """ Determines whether we will do any type of terminal coloring""" 40 | return color == ShowCommand.autoColor \ 41 | and BaseWavefrontCommand.doesTermSupportColor() 42 | 43 | summaryOutput, jsonOutput = ["summary", "json"] 44 | 45 | @staticmethod 46 | def _addOutputOption(parser): 47 | """ -o --output, summary, json options.""" 48 | parser.add_argument( 49 | "--output", 50 | "-o", 51 | choices=[ 52 | ShowCommand.summaryOutput, 53 | ShowCommand.jsonOutput], 54 | default=ShowCommand.summaryOutput, 55 | help="""Output fromat. summary output is default where one line is 56 | printed for each resource. Json prints out the full state of each 57 | resource in json form.""") 58 | 59 | def printSummaryTable(self, rsrcType, rsrcs, color, printHeader): 60 | """Print a nicely formatted summary table for all the resources. 61 | The summary table contains a single line summary for each resource.""" 62 | 63 | # Print the header using the entryFormat 64 | if printHeader: 65 | print(rsrcType.summaryTableHeader()) 66 | 67 | enableColor = ShowCommand._isTermColoringEnabled(color) 68 | for rsrc in rsrcs: 69 | print(rsrc.summaryTableRow(enableColor)) 70 | 71 | def showResourcesInBrowser(self, rsrcs): 72 | """ Depending on the command line arguments and the number of selected 73 | resources , display the resources in a web browser. The user must 74 | specify --in-browser in the command line for us to launch a browser and 75 | the number of resources to display should be less than _maxBrowserTabs. 76 | We do not want to create 100's of tabs programmatically by accident""" 77 | 78 | if len(rsrcs) == 0: 79 | return 80 | 81 | if len(rsrcs) > ShowCommand._maxBrowserTabs(): 82 | raise ShowError( 83 | "Too many resources to display in browser. Selected " + 84 | "resrouces: {0}, maximum supported {1}" 85 | "".format( 86 | len(rsrcs), 87 | ShowCommand._maxBrowserTabs)) 88 | 89 | # Open the first resource in a new window. 90 | # Then open the rest of the resources in tabs. 91 | # TODO: The python documentation does not guarantee the new window 92 | # and tab usage. All new pages can be opened in new tabs. 93 | rsrc = rsrcs[0] 94 | urlPrefix = self.getWavefrontHost() 95 | webbrowser.open_new(urlPrefix + rsrc.browserUrlSuffix()) 96 | for rsrc in rsrcs[1:]: 97 | webbrowser.open_new_tab(urlPrefix + rsrc.browserUrlSuffix()) 98 | 99 | def handleCmd(self, args): 100 | """ Handles the show [...] commands.""" 101 | super(ShowCommand, self).handleCmd(args) 102 | 103 | rsrcType = args.rsrcType 104 | rt = ResourceFactory.resourceTypeFromString(rsrcType) 105 | 106 | rsrcs = self.getRsrcsViaWavefrontApiClient(rt, args.customerTag, args) 107 | 108 | if args.output == ShowCommand.summaryOutput: 109 | self.printSummaryTable(rt, rsrcs, args.color, not args.noHeader) 110 | elif args.output == ShowCommand.jsonOutput: 111 | for r in rsrcs: 112 | print(r.jsonStr()) 113 | else: 114 | assert not "Unexpected output option in args." 115 | 116 | if args.inBrowser: 117 | self.showResourcesInBrowser(rsrcs) 118 | 119 | autoColor, neverColor = ("auto", "never") 120 | 121 | def _addColorOption(self, parser): 122 | parser.add_argument( 123 | "--color", 124 | "-l", 125 | choices=[ 126 | self.autoColor, 127 | self.neverColor], 128 | default="auto", 129 | help="""Enable/Disable colored output in the summary table. 130 | If set to auto the output will be colored if the terminal supports it. 131 | If set to never, ouput will not be colored""") 132 | 133 | def _addInBrowserOption(self, parser): 134 | parser.add_argument( 135 | "--in-browser", 136 | "-b", 137 | dest="inBrowser", 138 | action="store_true", 139 | help="""Open the selected resources in the default browser in new 140 | tabs. At most {0} new tabs are supported. If the selected number 141 | of resources are more than {0}, an exception is thrown. The 142 | supported max can be changed by setting the {1} env 143 | var""".format( 144 | ShowCommand._maxBrowserTabs(), 145 | ShowCommand._maxBrowserTabsEnvVarName)) 146 | 147 | def _addNoHeaderOption(self, parser): 148 | parser.add_argument( 149 | "--no-header", 150 | dest="noHeader", 151 | action="store_true", 152 | help="""If set, the show command does not print the table header in 153 | summary mode""") 154 | 155 | def addCmd(self, subParsers): 156 | p = subParsers.add_parser( 157 | "show", help="""show prints Wavefront rsrcs in 158 | various formats """) 159 | p.set_defaults(wavefrontConfigFuncToCall=self.handleCmd) 160 | 161 | ShowCommand._addOutputOption(p) 162 | self._addColorOption(p) 163 | self._addInBrowserOption(p) 164 | self._addNoHeaderOption(p) 165 | 166 | self._addRsrcTypeSubParsers(p) 167 | 168 | super(ShowCommand, self).addCmd(p) 169 | 170 | def __init__(self, *args, **kwargs): 171 | super(ShowCommand, self).__init__(*args, **kwargs) 172 | -------------------------------------------------------------------------------- /wavectl/Mutator.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import absolute_import 3 | from __future__ import print_function 4 | import argparse 5 | import os 6 | import json 7 | import time 8 | import datetime 9 | import sys 10 | import tempfile 11 | import shutil 12 | import logging 13 | import threading 14 | import multiprocessing 15 | import types 16 | import urllib3 17 | import http.client 18 | 19 | # External dependencies: 20 | import git 21 | import wavefront_api_client 22 | 23 | # TODO: The name is not correct anymore. It should be like 24 | # WavefrontCmdImpl or something like that. 25 | from .BaseWavefrontCommand import BaseWavefrontCommand 26 | from .GitUtils import GitUtils 27 | from .ResourceFactory import ResourceFactory 28 | 29 | 30 | class MutatorError(Exception): 31 | pass 32 | 33 | 34 | class Mutator(BaseWavefrontCommand): 35 | """ The implementation of the common functions in the Push and Create 36 | commands.""" 37 | 38 | @staticmethod 39 | def _addQuietOption(parser): 40 | parser.add_argument( 41 | "--quiet", 42 | "-q", 43 | action="store_true", 44 | help="""Supress printed output about mutated resrouces.""") 45 | 46 | @staticmethod 47 | def _addTargetOption(parser): 48 | parser.add_argument("target", help="""Wavefront resource data from this 49 | target will be written to the wavefront server. target can be a path to 50 | a directory or a file. For directories all resource files in that 51 | directory will be considered. For files, on the that file is considered 52 | for a push""") 53 | 54 | @staticmethod 55 | def withApiExceptionRetry(wavefrontFunction): 56 | """A decorator to call the given function in a retry loop. If the function 57 | raises a wavefront_api_client.ApiException, the exception gets logged 58 | and the function is retried""" 59 | 60 | def rv(*args, **kwargs): 61 | retries = 3 62 | while True: 63 | try: 64 | res = wavefrontFunction(*args, **kwargs) 65 | except (wavefront_api_client.rest.ApiException, 66 | http.client.BadStatusLine, 67 | http.client.HTTPException, 68 | urllib3.exceptions.ProtocolError, 69 | Exception) as e: 70 | 71 | logging.debug( 72 | ("Received exception with type {} from api call:" 73 | "{}Retries: {}").format( 74 | type(e), e, retries)) 75 | retries = retries - 1 76 | 77 | # In case we receive bunch of consecutive exceptions, 78 | # raise the exception after exhaustion of retires. 79 | if retries == 0: 80 | raise 81 | 82 | # If the Api server has returned with some error, give some time 83 | # before retrying. 84 | time.sleep(0.1) 85 | else: 86 | return res 87 | 88 | return rv 89 | 90 | def checkCleanDirInRepo(self, r, d): 91 | """ Check that the given dir in the git repo is "clean". In other words, 92 | there are no staged or modified files in the given directory.""" 93 | if r.is_dirty(path=d): 94 | raise MutatorError( 95 | ("The path at {0} is dirty. Please commit your outstanding changes " 96 | "and try again... \n{1}").format( 97 | r.working_tree_dir, r.git.status(d))) 98 | 99 | def filterRsrcsWrtTag(self, rsrcs, customerTag): 100 | """We filter the resoruces according to their customerTag field. For the 101 | filtering of resources received from the wavefront server, we did not 102 | process the customerTags locally. Because the server already does the 103 | customerTag filtering. For the resources that are read from a local 104 | directory, we additionally need to filter them using the customerTag 105 | filed.""" 106 | 107 | if customerTag is None: 108 | # The user has not specified any customerTag. So all rsrcs are 109 | # matching 110 | return rsrcs 111 | 112 | assert(len(customerTag) > 0 113 | and "Expected valid entries in the customerTag parameter") 114 | 115 | rv = [r for r in rsrcs if r.doesContainAllTags(customerTag)] 116 | return rv 117 | 118 | def getRsrcFromFile(self, rsrcType, fn): 119 | """Given the full file path, return the rsrc object for it. 120 | The return value can be Alert or Dashboard etc.""" 121 | 122 | with open(fn) as f: 123 | r = rsrcType.fromDict(json.load(f)) 124 | return r 125 | 126 | def getRsrcsFromPath(self, rsrcType, p, customerTag, args): 127 | """Parse all the resource files in the given dir or parse the file at 128 | the given path , filter them according to the user's command line 129 | params and return a list of them""" 130 | 131 | # The passed parameter can be a directory or a file. 132 | # In case of a directory all resource files in it will be parsed. 133 | # In case of a file, only that file will be parsed. 134 | if os.path.isdir(p): 135 | files = BaseWavefrontCommand.getResourceFiles(rsrcType, p) 136 | rsrcs = [] 137 | for fn in files: 138 | r = self.getRsrcFromFile(rsrcType, fn) 139 | rsrcs.append(r) 140 | else: 141 | rsrc = self.getRsrcFromFile(rsrcType, p) 142 | rsrcs = [rsrc] 143 | 144 | remRsrcs = self.filterRsrcsWrtTag(rsrcs, customerTag) 145 | remRsrcs = self.filterResources(rsrcType, remRsrcs, args) 146 | return remRsrcs 147 | 148 | def doChecksGetRsrcsFromTarget(self, args): 149 | """Execute necessary checks and get the resources from the args.target. 150 | Do necessary filtering as specified by the user.""" 151 | 152 | p = self._getRealPath(args.target) 153 | if not os.path.exists(p): 154 | # For create and push commands (mutators) the passed target 155 | # should exist in the file system. 156 | raise MutatorError("The given path: {} does not exist".format(p)) 157 | 158 | rsrcType = args.rsrcType 159 | rt = ResourceFactory.resourceTypeFromString(rsrcType) 160 | if args.inGit: 161 | r = GitUtils.getExistingRepo(p) 162 | self.checkCleanDirInRepo(r, p) 163 | 164 | rsrcs = self.getRsrcsFromPath(rt, p, args.customerTag, args) 165 | 166 | logging.info("Using the resources: [{0}] from path: {1}".format( 167 | " ".join([str(r.uniqueId()) + rt.fileExtension() for r in rsrcs]), p)) 168 | 169 | return rsrcs 170 | 171 | def __init__(self, *args, **kwargs): 172 | super(Mutator, self).__init__(*args, **kwargs) 173 | -------------------------------------------------------------------------------- /wavectl/Alert.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import termcolor 4 | import collections 5 | import copy 6 | import abc 7 | from six import with_metaclass 8 | 9 | import wavefront_api_client 10 | 11 | 12 | from .Resource import Resource 13 | 14 | # To print a human readable string for the class type name 15 | 16 | 17 | class Meta(abc.ABCMeta): 18 | def __str__(self): 19 | return "alert" 20 | 21 | 22 | class Alert(with_metaclass(Meta, Resource)): 23 | """ A class representing an Alert""" 24 | 25 | def __str__(self): 26 | s = self.summaryTableRow(False) 27 | return s 28 | 29 | __repr__ = __str__ 30 | 31 | # Some fields in alerts are not interesting. Omit displaying and saving 32 | # them. 33 | _omittedFields = set([ 34 | "activeMaintenanceWindows", 35 | "alertsLastDay", 36 | "alertsLastMonth", 37 | "alertsLastWeek", 38 | "conditionQBEnabled", 39 | "created", 40 | "createdEpochMillis", 41 | "createUserId", 42 | "creatorId", 43 | "deleted", 44 | "displayExpressionQBEnabled", 45 | "endTime", 46 | "event", 47 | "failingHostLabelPairs", 48 | "hostsUsed", 49 | "includeObsoleteMetrics", 50 | "inMaintenanceHostLabelPairs", 51 | "inTrash", 52 | "lastEventTime", 53 | "lastNotificationMillis", 54 | "lastProcessedMillis", 55 | "metricsUsed", 56 | "notificants", 57 | "pointsScannedAtLastQuery", 58 | "prefiringHostLabelPairs", 59 | "processRateMinutes", 60 | "queryFailing", 61 | "runningState", 62 | "runninState", 63 | "sortAttr", 64 | "startTime", 65 | "status", 66 | "targetInfo", 67 | "updated", 68 | "updatedEpochMillis", 69 | "updaterId", 70 | "updateUserId", 71 | ]) 72 | 73 | # The command line options used to narrow down Alert resources. 74 | _supportedFilters = { 75 | "id": "i", 76 | "name": "n", 77 | "condition": "d", 78 | "displayExpression": "x", 79 | "additionalInformation": "f", 80 | "status": "a", 81 | "severity": "e", 82 | } 83 | 84 | # TODO : Should we add SNOOZED Until ? 85 | # This will make it more similar to the wavefront web page. 86 | _summaryTableKeys = ["id", "name", "status", "severity"] 87 | 88 | _uniqueKey = "id" 89 | 90 | @staticmethod 91 | def _colorStatus(v): 92 | """ The value is for the status field in alert data. Return its 93 | tty colored string accordingly""" 94 | 95 | if v == "CHECKING": 96 | return termcolor.colored(v, color="green") 97 | elif v == "ACTIVE" or v == "FIRING": 98 | return termcolor.colored(v, color="red") 99 | elif v == "SNOOZED": 100 | return termcolor.colored(v, color="grey") 101 | else: 102 | return v 103 | 104 | @staticmethod 105 | def _colorSeverity(v): 106 | """ The value is for the severity a field in alert data. Return its 107 | tty colored string accordingly""" 108 | if v == "SEVERE": 109 | return termcolor.colored(v, color="red") 110 | elif v == "WARN": 111 | return termcolor.colored(v, color="yellow") 112 | elif v == "INFO": 113 | return termcolor.colored(v, color="blue") 114 | elif v == "SMOKE": 115 | return termcolor.colored(v, color="grey") 116 | else: 117 | return v 118 | 119 | _coloredKeys = ["status", "severity"] 120 | 121 | @staticmethod 122 | def _maybeColor(k, v, enableColor): 123 | """ Depending on the key and the color setting from the command line 124 | we may want to color the output. This function returns the colored version 125 | of v if desired or if possible""" 126 | 127 | if not enableColor: 128 | return v 129 | 130 | # We only color-code alert states and severity information. Nothing else. 131 | # This is similar to the wavefront webpage interface. 132 | if k == "status": 133 | return Alert._colorStatus(v) 134 | elif k == "severity": 135 | return Alert._colorSeverity(v) 136 | else: 137 | return v 138 | 139 | @staticmethod 140 | def _formatValueForSummaryTable(k, v, enableColor): 141 | """Format the given value for a nice print in the summary table """ 142 | if isinstance(v, collections.MutableSequence): 143 | return " ".join([Alert._maybeColor(k, x, enableColor) for x in v]) 144 | else: 145 | return Alert._maybeColor(k, v, enableColor) 146 | 147 | @staticmethod 148 | def getFunction(wavefrontClient): 149 | """Given a wavefrontClient object return the function to call to get all 150 | alerts from the wavefront Api server""" 151 | api = wavefront_api_client.SearchApi(wavefrontClient) 152 | f = api.search_alert_entities 153 | return f 154 | 155 | @staticmethod 156 | def getDeletedFunction(wavefrontClient): 157 | """Given a wavefrontClient object return the function to call to get all 158 | deleted alerts from the wavefront Api server""" 159 | api = wavefront_api_client.SearchApi(wavefrontClient) 160 | f = api.search_alert_deleted_entities 161 | return f 162 | 163 | @staticmethod 164 | def updateFunction(wavefrontClient): 165 | """Given a wavefrontClient object return the function to call to update 166 | one alert in wavefront api server""" 167 | api = wavefront_api_client.AlertApi(wavefrontClient) 168 | f = api.update_alert 169 | return f 170 | 171 | @staticmethod 172 | def createFunction(wavefrontClient): 173 | """Given a wavefrontClient object return the function to call to create 174 | one new alert in wavefront api server""" 175 | api = wavefront_api_client.AlertApi(wavefrontClient) 176 | f = api.create_alert 177 | return f 178 | 179 | @staticmethod 180 | def fileExtension(): 181 | """Return the extension used for the file on disc representing this 182 | resource""" 183 | return ".alert" 184 | 185 | def __init__(self, state): 186 | super(Alert, self).__init__(state) 187 | 188 | # Save a reference of self to the static global list. 189 | self.allRsrcs.append(self) 190 | 191 | @classmethod 192 | def fromDict(cls, dict): 193 | """Create a Dashboard object from the given dict dictionary""" 194 | return cls(dict) 195 | 196 | def uniqueId(self): 197 | """Return a unique id representing this Alert. 198 | It is the id field assigned by the wavefront api server""" 199 | return self._state[Alert._uniqueKey] 200 | 201 | def browserUrlSuffix(self): 202 | return "/alert/" + str(self.uniqueId()) 203 | 204 | def summaryTableRow(self, enableColor): 205 | # Return the one line summary string. This string becomes the row in the 206 | # summary table representing this alert. 207 | 208 | fmt = self._summaryTableRowFormat(enableColor) 209 | row = fmt.format( 210 | **{k: Alert._formatValueForSummaryTable(k, self._state.get(k, ""), 211 | enableColor) for k in Alert._summaryTableKeys}) 212 | return row 213 | -------------------------------------------------------------------------------- /doc/sphinx/GitIntegration.md: -------------------------------------------------------------------------------- 1 | 2 | \# Using wavectl with git 3 | 4 | One of the advanced features of \`wavectl\` is its git integration. It can be 5 | enabled using the \`--inGit,-g\` parameter to 6 | \[pull\]\(CommandReference.md#pull-options\), 7 | \[push\]\(CommandReference.md#push-options\) and 8 | \[create\]\(CommandReference.md#create-options\) commands. \`wavectl\` gets 9 | several benefits via git integration that are not immediately obvious. Some 10 | effective use cases are as follows: 11 | 12 | \#\# Build a git history of alerts, dashboards with 13 | \[\`pull\`\]\(CommandReference.md#pull-options\) 14 | 15 | Wavefront already keeps the change history of 16 | \[alerts\]\(https://docs.wavefront.com/alerts.html#viewing-alerts-and-alert-history\) 17 | and 18 | \[dashboards\]\(https://docs.wavefront.com/dashboards_managing.html#managing-dashboard-versions\). 19 | Those can be accessed via the browser gui. The powerful Wavefront v2 api also 20 | has endpoints to access the histories of 21 | \[alerts\]\(https://github.com/wavefrontHQ/python-client/blob/355698be1b53a32e81348f92549707dbcb4f6413/wavefront_api_client/api/alert_api.py#L428\) 22 | and 23 | \[dashboards\](https://github.com/wavefrontHQ/python-client/blob/355698be1b53a32e81348f92549707dbcb4f6413/wavefront_api_client/api/dashboard_api.py#L523\). 24 | With git integration in \`wavectl\` we do not add a brand new feature on top 25 | of those. Instead, we attempt to make the retrieval of the histories easier via 26 | the command line mostly using existing mechanisms in git. 27 | 28 | Passing \`--inGit\` to \`pull\` command initalizes a git repository in the 29 | pulled directory, if the directory gets created with the \`pull\` command. 30 | 31 | 32 | 33 | 34 | ```eval_rst 35 | .. program-output:: rm -rf /tmp/GitIntegrationPull 36 | :returncode: 0 37 | ``` 38 | 39 | ```eval_rst 40 | .. program-output:: wavectl pull --inGit /tmp/GitIntegrationPull/alerts alert 41 | :returncode: 0 42 | :prompt: 43 | ``` 44 | 45 | A git repo is created in the newly created /tmp/GitIntegrationPull/alerts directory. 46 | 47 | 48 | ```eval_rst 49 | .. program-output:: git -C /tmp/GitIntegrationPull/alerts status 50 | :returncode: 0 51 | :prompt: 52 | ``` 53 | 54 | Each \`pull\` command creates a new commit with the changes pulled from the 55 | Wavefront server at that time. 56 | 57 | 58 | ```eval_rst 59 | .. program-output:: git -C /tmp/GitIntegrationPull/alerts log --oneline 60 | :returncode: 0 61 | :prompt: 62 | ``` 63 | 64 | If you execute a long running daemon executing periodic pulls from Wavefront, an 65 | extensive git history can be built. The git history will correspond to users' 66 | edits to alerts and dashboards. 67 | 68 | 69 | while true; do 70 | wavectl pull alert 71 | wavectl pull dashboard 72 | sleep 300 73 | done 74 | 75 | 76 | Then, later on, if someone is interested understanding the changes to some alerts 77 | or dashboards, the investigation can be done with git show and log commands, which are widely 78 | known by programmers already. 79 | 80 | For example: 81 | 82 | \#\#\# When was an a particular alert created ? 83 | 84 | ```eval_rst 85 | .. program-output:: git -C /tmp/GitIntegrationPull/alerts log $(ls /tmp/GitIntegrationPull/alerts | sort | head -n 1) 86 | :returncode: 0 87 | :shell: 88 | :prompt: 89 | ``` 90 | 91 | \#\#\# When were each alert snoozed ? 92 | 93 | ```eval_rst 94 | .. program-output:: git -C /tmp/GitIntegrationPull/alerts log -S snoozed 95 | :returncode: 0 96 | :shell: 97 | :prompt: 98 | ``` 99 | 100 | 101 | \> NOTE: The updater id and the actual update time for each alert and dashboard 102 | can be retrieved using the history endpoing in \[Wavefront 103 | API\]\(https://docs.wavefront.com/wavefront_api.html\). However, in 104 | the current \`wavectl\` implementation, the git commit messages 105 | do not contain either of them. In future \`wavectl\` releases we plan to improve 106 | the git integration of pull command to include the updater id and the update time. 107 | 108 | 109 | 110 | \#\# Using \[git-diff\]\(https://git-scm.com/docs/git-diff\) to make safe local 111 | modifications to your alerts, dashboards with 112 | \[\`push\`\]\(CommandReference.md#push-options\) 113 | 114 | In the \[repetitive editing doc\]\(RepetitiveEditing.md\) we have 115 | demonstrated an example using the 116 | \[\`sed\`\]\(https://www.gnu.org/software/sed/manual/sed.html\) command to 117 | search and replace strings on local files. After the modifications, the alert, 118 | dashboard json files were written to Wavefront. 119 | 120 | It is always good practice to inspect the changes to alerts and dashboards 121 | before writing them back to Wavefront with the 122 | \[\`push\`\]\(CommandReference.md#push-options\) command. The git integration 123 | for the push command can provide a diff of local modifications for the user to 124 | verify. Since the git integration will work on a local git repo, 125 | \[git-diff\]\(https://git-scm.com/docs/git-diff\) can be used for the diff 126 | generation. 127 | 128 | 129 | 130 | ```eval_rst 131 | .. program-output:: rm -rf /tmp/GitIntegrationPush 132 | :returncode: 0 133 | ``` 134 | 135 | Using the same example from the \[repetitive editing doc\]\(RepetitiveEditing.md\), we 136 | first pull the alerts matching a regular expression. But this time we use the 137 | git integration command line parameter \`--inGit,-g\` 138 | 139 | ```eval_rst 140 | .. program-output:: wavectl pull --inGit /tmp/GitIntegrationPush/alerts alert --match "proc\." 141 | :returncode: 0 142 | :prompt: 143 | ``` 144 | 145 | Then the local modifications are executed: 146 | 147 | ```eval_rst 148 | .. program-output:: find /tmp/GitIntegrationPush -type f | xargs sed -i -e 's/proc\./host.proc./g' 149 | :returncode: 0 150 | :prompt: 151 | :shell: 152 | ``` 153 | 154 | See the modifications to alerts that are going to be pushed to Wavefront: 155 | 156 | ```eval_rst 157 | .. program-output:: git -C /tmp/GitIntegrationPush/alerts diff HEAD 158 | :returncode: 0 159 | :prompt: 160 | :ellipsis: 32 161 | ``` 162 | 163 | Submit your changes to the local repo: 164 | 165 | ```eval_rst 166 | .. program-output:: git -C /tmp/GitIntegrationPush/alerts commit -a -m "proc. is replaced with host.proc." 167 | :returncode: 0 168 | :shell: 169 | :prompt: 170 | ``` 171 | 172 | \> NOTE: If you are using git integration, \`wavectl\` will not let you push 173 | unless you have committed your changes to the repo. This behavior is like a 174 | safeguard to ensure that the user is fully aware of what he is writing to 175 | Wavefont via \`wavectl push\`. Asking the user to commit his local changes, 176 | serves to ensure that she has inspected the diff and is OK with the 177 | modifications. 178 | 179 | 180 | Lastly, push your local modifications to Wavefront. 181 | 182 | ```eval_rst 183 | .. program-output:: wavectl push --inGit /tmp/GitIntegrationPush/alerts alert 184 | :returncode: 0 185 | :prompt: 186 | ``` 187 | 188 | 189 | \> NOTE: Even with all the safeguards, if you have made a mistake and pushed to 190 | Wavefront, you can roll back the git repo to the previous commit. A command 191 | like \`git checkout HEAD^\` will remove your most recent changes from local 192 | files. After that, you can re-execute the push command. That will update all 193 | alerts one more time and will set them to the previous state. 194 | 195 | 196 | -------------------------------------------------------------------------------- /doc/Templating.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | # Simple templating of alerts, dashboards 8 | 9 | Wavefront alerts and dashboards are very powerful tools at developers' hands. With Wavefront's monitoring capabilities, programmers can gain great insights about of all sorts of applications. With that great versatility, we have seen the alert and dashboard arsenals of our application owners grow organically in an ad-hoc way. 10 | 11 | Different teams have re-discovered almost identical best practices. Various different ways of formulating the same query have independently spread in the organization. For example, we have seen different teams use slightly different metrics and conditions for their high CPU consumption alerts. Sometimes we discover a more robust, improved way of building a query. That knowledge does not spread across teams fast enough to quickly benefit the entire organization. With these concerns, we thought to add more structure to our Wavefront state and experimented with templating using `wavectl.` In this document we give a brief introduction how `wavectl` can be used for simple templating. 12 | 13 | Templated alerts, dashboards can be very a comprehensive feature and its implementation could become very complex. At the start with `wavectl`, we kept the templating capabilities limited and simple. Lets go over an example and later on we discuss the limitations and future work. The following sections focus on only templated alerts for brevity reasons. The commands and capabilities easily expand to dashboards too. 14 | 15 | ## Generate the template files first time. 16 | 17 | ### Choose which alerts to templetize. 18 | 19 | Some problematic observations are common in various applications. For example high cpu, memory... consumption. With containers and [Kubernetes](https://kubernetes.io/) orchestration framework, we get some more additional common observations like: 20 | 21 | - Too many container restarts. 22 | 23 | - Too many Pod restarts. 24 | 25 | - Too many Pods stuck at an unhealthy state. 26 | 27 | - Pods getting to close to their ResourceQuota usage. 28 | 29 | - ... 30 | 31 | In your organization, first, you need to identify some common observations that would add value to numerous teams. The purpose of an alert templating exercise should be to simplify and improve many teams' monitoring capabilities. Once you select a common observation, you may discover distinct Wavefront alerts that detect the same problem. They may be implemented differently or may not maintained similarly. Those would be good candidates for templetizing. 32 | 33 | ### Download json files of alerts from Wavefront. 34 | 35 | Wavefront GUI has really powerful user experience. 36 | 37 | - [Charts](https://docs.wavefront.com/charts.html) react instantly to changing queries. 38 | 39 | - Alerts have a [backtesting](https://docs.wavefront.com/alerts.html#backtesting) mode helping to avoid false positives and negatives. 40 | 41 | - Autocomplete [dropdown](https://docs.wavefront.com/metrics_managing.html) menus for metric name completion. 42 | 43 | Because of these and many other features, we strongly believe the Wavefront GUI is the right medium for developing new alerts and dashboards. If there is a need to templetize an alert, an initial version of that alert would have been implemented via the Wavefront GUI already. 44 | 45 | With that observation, the first step to generate the alert templates is to download the json representation of the alerts from Wavefront. In this example, a service called collections-service has some good alerts. We want to build templates from them so that they can be easily used by other teams too. The [`pull`](CommandReference.md#pull-options) command is used to download the alerts. 46 | 47 | ``` 48 | $ wavectl pull /tmp/Templating/alerts alert -t collections-service 49 | ``` 50 | 51 | After pulling the alerts you can see them in the target directory. 52 | 53 | ``` 54 | $ ls /tmp/Templating/alerts 55 | 1530723442872.alert 56 | 1530723443002.alert 57 | 1530723443146.alert 58 | ``` 59 | 60 | They are in json format: 61 | 62 | ``` 63 | $ cat /tmp/Templating/alerts/* 64 | { 65 | "condition": "sum(ts(kube.metrics.deployment_status_replicas_available, namespace=collections-service-dev)) < sum(ts(kube.metrics.deployment_status_replicas, namespace=collections-service-dev))", 66 | "displayExpression": "sum(ts(kube.metrics.deployment_status_replicas_available, namespace=collections-service-dev))", 67 | "id": "1530723442872", 68 | "minutes": 10, 69 | "name": "Collections Dev Pod Count Low", 70 | "severity": "SEVERE", 71 | "tags": { 72 | "customerTags": [ 73 | "collections-service", 74 | "core-frameworks" 75 | ] 76 | }, 77 | "target": "pd: c6cce4d0d93345a6ab0b76ce1a3b1498" 78 | }{ 79 | ... 80 | ``` 81 | 82 | > NOTE: In this example there are a small number of alerts. All the concepts and commands discussed here easily expand to many more alerts and dashboards. We kept the data small to have a easy to follow example. 83 | 84 | ### Convert the json files of alerts into a templating languge. 85 | 86 | Once we have the json files downloaded, we need to decide on a templating tool. Since we are working on json files, in this example, we have choosen [jsonnet](https://jsonnet.org/). jsonnet is a very powerful json templating tool and it is used in various other places in kubernetes community. You could use your own favorite templating tool chain, the commands will be different but the main idea stays the same. [Mustache](https://mustache.github.io/) or [go](https://golang.org/pkg/text/template/) templates or even [sed](https://www.gnu.org/software/sed/) commands are suitable for this task. 87 | 88 | Converting json files into jsonnet templating language is a one time setup step for templates. It requires jsonnet lanuage know-how. In this example, the before and after json -\> jsonnet files would looke like this: 89 | 90 | #### Alert's json file: 91 | 92 | ``` 93 | { 94 | "condition": "sum(ts(kube.metrics.deployment_status_replicas_available, namespace=collections-service-dev)) < sum(ts(kube.metrics.deployment_status_replicas, namespace=collections-service-dev))", 95 | "displayExpression": "sum(ts(kube.metrics.deployment_status_replicas_available, namespace=collections-service-dev))", 96 | "id": "1530723442872", 97 | "minutes": 10, 98 | "name": "Collections Dev Pod Count Low", 99 | "severity": "SEVERE", 100 | "tags": { 101 | "customerTags": [ 102 | "collections-service", 103 | "core-frameworks" 104 | ] 105 | }, 106 | "target": "pd: c6cce4d0d93345a6ab0b76ce1a3b1498" 107 | } 108 | ``` 109 | 110 | #### The corresponding jsonnet template: 111 | 112 | We have replaced the hardcoded values for a specific service, with variables that can be overwritten at template complie time. For example, namespace, tag and pagerDuty key. 113 | 114 | ``` 115 | { 116 | condition: 'sum(ts(kube.metrics.deployment_status_replicas_available, namespace=' + std.extVar('namespace') + ')) < sum(ts(kube.metrics.deployment_status_replicas, namespace=' + std.extVar('namespace') + '))', 117 | displayExpression: 'sum(ts(kube.metrics.deployment_status_replicas_available, namespace=' + std.extVar('namespace') + '))', 118 | id: '1530723442872', 119 | minutes: 10, 120 | name: '' + std.extVar('namespace') + ' Pod Count Low', 121 | severity: 'SEVERE', 122 | tags: { 123 | customerTags: [ 124 | std.extVar('teamTag'), 125 | std.extVar('parentTeamTag'), 126 | ], 127 | }, 128 | target: std.extVar('pagerDutyKey'), 129 | } 130 | ``` 131 | 132 | Once this transformation is done, you get the template files. They can be reused to generate Wavefront alerts later on. 133 | 134 | ## Generate a new set of alerts from templates. 135 | 136 | TODO: Add this section 137 | 138 | ## Future Work. 139 | 140 | TODO: Add this section 141 | -------------------------------------------------------------------------------- /doc/sphinx/Templating.md: -------------------------------------------------------------------------------- 1 | 2 | \# Simple templating of alerts, dashboards 3 | 4 | Wavefront alerts and dashboards are very powerful tools at developers' hands. 5 | With Wavefront's monitoring capabilities, programmers can gain great insights 6 | about of all sorts of applications. With that great versatility, we have seen 7 | the alert and dashboard arsenals of our application owners grow organically in 8 | an ad-hoc way. 9 | 10 | Different teams have re-discovered almost identical best practices. Various 11 | different ways of formulating the same query have independently spread in 12 | the organization. For example, we have seen different teams use slightly 13 | different metrics and conditions for their high CPU consumption alerts. 14 | Sometimes we discover a more robust, improved way of building a query. That 15 | knowledge does not spread across teams fast enough to quickly benefit the 16 | entire organization. With these concerns, we thought to add more structure to 17 | our Wavefront state and experimented with templating using \`wavectl.\` In this 18 | document we give a brief introduction how \`wavectl\` can be used for simple 19 | templating. 20 | 21 | Templated alerts, dashboards can be very a comprehensive feature and its 22 | implementation could become very complex. At the start with \`wavectl\`, we 23 | kept the templating capabilities limited and simple. Lets go over an example 24 | and later on we discuss the limitations and future work. The following sections 25 | focus on only templated alerts for brevity reasons. The commands and capabilities 26 | easily expand to dashboards too. 27 | 28 | \#\# Generate the template files first time. 29 | 30 | \#\#\# Choose which alerts to templetize. 31 | 32 | Some problematic observations are common in various applications. For example 33 | high cpu, memory... consumption. With containers and 34 | \[Kubernetes\]\(https://kubernetes.io/\) orchestration framework, we get some 35 | more additional common observations like: 36 | 37 | \- Too many container restarts. 38 | 39 | \- Too many Pod restarts. 40 | 41 | \- Too many Pods stuck at an unhealthy state. 42 | 43 | \- Pods getting to close to their ResourceQuota usage. 44 | 45 | \- ... 46 | 47 | In your organization, first, you need to identify some common observations that 48 | would add value to numerous teams. The purpose of an alert templating exercise 49 | should be to simplify and improve many teams' monitoring capabilities. Once you 50 | select a common observation, you may discover distinct Wavefront alerts that 51 | detect the same problem. They may be implemented differently or may not 52 | maintained similarly. Those would be good candidates for templetizing. 53 | 54 | 55 | \#\#\# Download json files of alerts from Wavefront. 56 | 57 | Wavefront GUI has really powerful user experience. 58 | 59 | \- \[Charts\]\(https://docs.wavefront.com/charts.html\) react instantly to 60 | changing queries. 61 | 62 | \- Alerts have a 63 | \[backtesting\]\(https://docs.wavefront.com/alerts.html#backtesting\) mode 64 | helping to avoid false positives and negatives. 65 | 66 | \- Autocomplete 67 | \[dropdown\]\(https://docs.wavefront.com/metrics_managing.html\) menus for 68 | metric name completion. 69 | 70 | Because of these and many other features, we strongly believe the Wavefront GUI 71 | is the right medium for developing new alerts and dashboards. If there is a 72 | need to templetize an alert, an initial version of that alert would have been 73 | implemented via the Wavefront GUI already. 74 | 75 | With that observation, the first step to generate the alert templates is to 76 | download the json representation of the alerts from Wavefront. In this 77 | example, a service called collections-service has some good alerts. We want to 78 | build templates from them so that they can be easily used by other teams too. 79 | The \[\`pull\`\]\(CommandReference.md#pull-options\) command is used to 80 | download the alerts. 81 | 82 | 83 | 84 | 85 | ```eval_rst 86 | .. program-output:: rm -rf /tmp/Templating 87 | :returncode: 0 88 | ``` 89 | 90 | ```eval_rst 91 | .. program-output:: wavectl pull /tmp/Templating/alerts alert -t collections-service 92 | :returncode: 0 93 | :prompt: 94 | ``` 95 | 96 | After pulling the alerts you can see them in the target directory. 97 | 98 | ```eval_rst 99 | .. program-output:: ls /tmp/Templating/alerts 100 | :returncode: 0 101 | :prompt: 102 | ``` 103 | 104 | They are in json format: 105 | 106 | ```eval_rst 107 | .. program-output:: cat /tmp/Templating/alerts/* 108 | :returncode: 0 109 | :shell: 110 | :prompt: 111 | :ellipsis: 15 112 | ``` 113 | 114 | \> NOTE: In this example there are a small number of alerts. All the concepts 115 | and commands discussed here easily expand to many more alerts and dashboards. 116 | We kept the data small to have a easy to follow example. 117 | 118 | \#\#\# Convert the json files of alerts into a templating languge. 119 | 120 | Once we have the json files downloaded, we need to decide on a templating tool. 121 | Since we are working on json files, in this example, we have choosen 122 | \[jsonnet\]\(https://jsonnet.org/\). jsonnet is a very powerful json templating 123 | tool and it is used in various other places in kubernetes community. You could 124 | use your own favorite templating tool chain, the commands will be different but 125 | the main idea stays the same. \[Mustache\]\(https://mustache.github.io/\) or 126 | \[go\]\(https://golang.org/pkg/text/template/\) templates or even 127 | \[sed\]\(https://www.gnu.org/software/sed/\) commands are suitable for this 128 | task. 129 | 130 | Converting json files into jsonnet templating language is a one time setup step 131 | for templates. It requires jsonnet lanuage know-how. In this example, the 132 | before and after json -> jsonnet files would looke like this: 133 | 134 | ```eval_rst 135 | .. program-output:: alertToTemplate.sh >/dev/null 2>&1 136 | :returncode: 0 137 | :shell: 138 | ``` 139 | 140 | \#\#\#\# Alert's json file: 141 | 142 | ```eval_rst 143 | .. program-output:: ls /tmp/Templating/alerts/*.alert| head -n 1 | xargs cat 144 | :returncode: 0 145 | :shell: 146 | ``` 147 | 148 | \#\#\#\# The corresponding jsonnet template: 149 | 150 | We have replaced the hardcoded values for a specific service, with variables 151 | that can be overwritten at template complie time. For example, namespace, tag 152 | and pagerDuty key. 153 | 154 | ```eval_rst 155 | .. program-output:: ls /tmp/Templating/alertTemplates/*.jsonnet | head -n 1 | xargs cat 156 | :returncode: 0 157 | :shell: 158 | ``` 159 | 160 | Once this transformation is done, you get the template files. They can be reused 161 | to generate Wavefront alerts later on. 162 | 163 | 164 | \#\# Generate a new set of alerts from templates. 165 | 166 | TODO: Add this section 167 | 168 | 169 | \#\# Future Work. 170 | 171 | TODO: Add this section 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /wavectl/Resource.py: -------------------------------------------------------------------------------- 1 | 2 | import argparse 3 | import json 4 | import abc 5 | import six 6 | 7 | 8 | class Resource(six.with_metaclass(abc.ABCMeta, object)): 9 | """A base class that has the common function for wavefront api items, 10 | building blocks.""" 11 | 12 | # Keep a reference to all resources. 13 | allRsrcs = [] 14 | 15 | # A dictionary used as a cache to avoid repeated calculations of format 16 | # strings in summary tables. 17 | _summaryTableFormat = { 18 | True: None, 19 | False: None, 20 | } 21 | 22 | @staticmethod 23 | def _getMaxPrintWidth(rsrcs, key, enableColor): 24 | """ Accesses all entries in rsrcs and looks at the key in each entry. 25 | Evaluates the string for each value and returns the longest string 26 | amongst all values. We use this function for tabular print. Say if we 27 | want to print all "name" entires of all resources. This function 28 | returns the necessary max width to allocate""" 29 | 30 | coloringOffset = 0 31 | # If there is going to be any terminal coloring, allocate some offset 32 | # for the ansi codes of terminal colors 33 | if enableColor: 34 | coloringOffset = 9 35 | 36 | # The [0] addition is to avoid a ValueError: max() arg is an empty sequence 37 | # error in case no resource can be found. 38 | width = max([0] + [coloringOffset + len(str(rsrc._state.get(key, ""))) 39 | for rsrc in rsrcs]) 40 | 41 | return width 42 | 43 | @staticmethod 44 | def _addCustomerTagOption(parser): 45 | """Adds the --customerTag option to the given parser 46 | The --customerTag is used by multiple commands so this common function 47 | can be used to add that command line option""" 48 | parser.add_argument( 49 | "--customerTag", 50 | "-t", 51 | action="append", 52 | help="""Narrow down the matching resources by their tags. Multiple 53 | customer tags can be passed. Resources that contain all tags will 54 | be returned.""") 55 | 56 | @staticmethod 57 | def _addMatchOption(parser): 58 | parser.add_argument("--match", "-m", metavar="REGEX", help="""specify 59 | a regular expression to further narrow down on matches in any field in 60 | the resource. If this regex mathes in the resource's representation, 61 | then the resource will be included in the processing""") 62 | 63 | @classmethod 64 | def _addSupportedFilterOptions(cls, parser): 65 | """ Add all command line parameters for regular expression specification 66 | for individual fields of a resource""" 67 | clsName = str(cls) 68 | for l, s in cls._supportedFilters.items(): 69 | parser.add_argument( 70 | "--" + 71 | l, 72 | "-" + 73 | s, 74 | metavar="REGEX", 75 | help="""specify 76 | a regular expression to further narrow down on matches in the """ + 77 | l + 78 | " field in a " + 79 | clsName) 80 | 81 | @classmethod 82 | def addSubparser(cls, rsrcsParser): 83 | """Add resource related command line options to the argparse subparser""" 84 | clsName = str(cls) 85 | p = rsrcsParser.add_parser( 86 | clsName, help="Specify to use {} resources".format(clsName)) 87 | 88 | Resource._addCustomerTagOption(p) 89 | Resource._addMatchOption(p) 90 | cls._addSupportedFilterOptions(p) 91 | 92 | @classmethod 93 | def _isColoredKey(cls, key): 94 | return key in cls._coloredKeys 95 | 96 | @classmethod 97 | def _summaryTableRowFormat(cls, enableColor): 98 | """Return the format strings of summary table rows. The summary table 99 | print uses the format string syntax to have reasonable spacing between 100 | the columns. Depending on the enableColor option the format strings are 101 | returned for colored summary table or not""" 102 | 103 | assert enableColor in cls._summaryTableFormat 104 | 105 | cached = cls._summaryTableFormat.get(enableColor) 106 | if cached: 107 | return cached 108 | 109 | delimiter = " " 110 | fmt = "" 111 | for stk in cls._summaryTableKeys: 112 | enCol = enableColor and cls._isColoredKey(stk) 113 | w = cls._getMaxPrintWidth(cls.allRsrcs, stk, enCol) 114 | # This is the string format specificatoin 115 | fmt = fmt + "{" + stk + ":<" + str(w) + "}" + delimiter 116 | 117 | cls._summaryTableFormat[enableColor] = fmt 118 | return fmt 119 | 120 | @classmethod 121 | def summaryTableHeader(cls): 122 | """Return the header of the summary table with correct delimiters""" 123 | 124 | # The header formatting is never colored. 125 | fmt = cls._summaryTableRowFormat(False) 126 | h = fmt.format(**{k: k.upper() for k in cls._summaryTableKeys}) 127 | return h 128 | 129 | def __init__(self, state): 130 | 131 | # Make sure all strings can be encoded with ascii. Ignore the unicode 132 | # characters 133 | # Without this python2 complains with 134 | # UnicodeEncodeError: 'ascii' codec can't encode character u'\u2013' in 135 | # position 24: ordinal not in range(128) 136 | # Maybe wavefront api server has started to send back unicode characters. 137 | # TODO: This would be a big performance hit. Attempting to stringify 138 | # every entry in every resoure. 139 | for k, v in state.items(): 140 | if isinstance(v, six.string_types): 141 | try: 142 | str(v) 143 | except UnicodeEncodeError as e: 144 | state[k] = v.encode("ascii", "ignore") 145 | 146 | self._state = state 147 | 148 | def doAllRegexesMatch(self, regexes, match): 149 | # Make sure all regular expressions match in resource state's 150 | # corresponding key 151 | rv = all([regex.search(str(self._state.get(key, ""))) is not None 152 | for (key, regex) in regexes.items()]) 153 | 154 | if not rv: 155 | # No need to check further 156 | return False 157 | 158 | if match is None: 159 | # No other regex to check 160 | return rv 161 | 162 | # a global match is also specified. That for that regex in the stringified 163 | # version of the alert 164 | rv = match.search(json.dumps(self._state)) 165 | return rv is not None 166 | 167 | def jsonStr(self): 168 | """ Return the json representation of the state in the resource""" 169 | # Purge the omitted fields 170 | d = {i: self._state[i] 171 | for i in self._state if i not in self._omittedFields} 172 | 173 | # The separators avoid the trailing whitespace. 174 | return json.dumps(d, indent=4, sort_keys=True, separators=(',', ': ')) 175 | 176 | def doesContainAllTags(self, tags): 177 | """Return true if the alert has one of the tags listed in tags set. 178 | tags: a set of tag strings""" 179 | 180 | if len(tags) == 0: 181 | # There is no tags to match 182 | return True 183 | 184 | tagDict = self._state.get("tags") 185 | if not tagDict: 186 | # For some reason this alert does not have any tags information. 187 | return False 188 | 189 | customerTagsList = tagDict.get("customerTags") 190 | if customerTagsList is None: 191 | # The Resource does not have any customerTags 192 | return False 193 | 194 | customerTagsSet = set(customerTagsList) 195 | # Return true if all given tags exist in the resource. 196 | return all([(t in customerTagsSet) for t in tags]) 197 | 198 | @abc.abstractmethod 199 | def uniqueId(self): 200 | raise NotImplementedError 201 | 202 | @abc.abstractmethod 203 | def browserUrlSuffix(self): 204 | raise NotImplementedError 205 | 206 | @abc.abstractmethod 207 | def summaryTableRow(self, enableColor): 208 | raise NotImplementedError 209 | -------------------------------------------------------------------------------- /doc/CommandLine.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | # Command line operations on your alerts, dashboards 8 | 9 | In this document we describe general tasks using the command line that you can accomplish with `wavectl` using your alerts and dashboards. 10 | 11 | Many of these examples can be accomplished by using the Wavefront gui too. `wavectl` enables accomplishing these tasks in a command line interface and in conjunction with powerful tools like `grep`, `awk`, `sed`, [`jq`](https://stedolan.github.io/jq/), or similar 12 | 13 | ## Print one line summaries of your alerts, dashboards. 14 | 15 | `wavectl` can be used to list all alerts in a one line summary form. 16 | 17 | For example: 18 | 19 | ``` 20 | $ wavectl show alert 21 | ID NAME STATUS SEVERITY 22 | 1530723441304 Kubernetes - Node Network Utilization - HIGH (Prod) CHECKING WARN 23 | 1530723441442 Kubernetes - Node Cpu Utilization - HIGH (Prod) CHECKING WARN 24 | 1530723441589 Kubernetes - Node Memory Swap Utilization - HIGH (Prod) SNOOZED WARN 25 | 1530723441737 Wavefront Freshness CHECKING WARN 26 | ... 27 | ``` 28 | 29 | The short summary form contains the [alert state](https://docs.wavefront.com/alerts_states_lifecycle.html#alert-states) and the severity in addition to the name and unique id of the alert. Once you have a structured columned print of your alerts, you can do all sorts of processing with them. 30 | 31 | For example: 32 | 33 | ### Find the alerts firing right now. 34 | 35 | ``` 36 | $ wavectl show alert | grep FIRING 37 | 1530723442180 Orion Response time more than 2 seconds FIRING INFO 38 | ``` 39 | 40 | This could be used from a script too. For example, an operator may want to ensure no alerts from "kubernetes" are firing before executing a script that is going to downtime one of the kubernetes control plane hosts. 41 | 42 | ### Count the total number of alerts in your organization. 43 | 44 | ``` 45 | $ wavectl show --no-header alert | wc -l 46 | 14 47 | ``` 48 | 49 | ## Inspect all attributes of alerts, dashboards. 50 | 51 | In addition to printing one line summaries the `show` command can also print detailed state of your alerts in json form: 52 | 53 | ``` 54 | $ wavectl show -o json alert 55 | { 56 | "additionalInformation": "This alert tracks the used network bandwidth percentage for all the compute-* (compute-master and compute-node) machines. If the cpu utilization exceeds 80%, this alert fires.", 57 | "condition": "ts(proc.net.percent,server_type=\"compute-*\" and env=\"live\") > 80", 58 | "displayExpression": "ts(proc.net.percent,server_type=\"compute-*\" and env=\"live\")", 59 | "id": "1530723441304", 60 | "minutes": 2, 61 | "name": "Kubernetes - Node Network Utilization - HIGH (Prod)", 62 | "resolveAfterMinutes": 2, 63 | "severity": "WARN", 64 | "tags": { 65 | "customerTags": [ 66 | "kubernetes", 67 | "skynet" 68 | ] 69 | }, 70 | "target": "pd: 07fe9ebacf8c44e881ea2f6e44dbf2d2" 71 | } 72 | { 73 | "additionalInformation": "This alert tracks the used cpu percentage for all the compute-* (compute-master and compute-node) machines. If the cpu utilization exceeds 80%, this alert fires.", 74 | ... 75 | ``` 76 | 77 | One you have an easy way to retrieve the json representation of alerts, dashboards, this can lead to various powerful use cases with using text processing tools like [`jq`](https://stedolan.github.io/jq/) or grep. For example: 78 | 79 | ### Print the name and the [condition](https://docs.wavefront.com/alerts_states_lifecycle.html#alert-conditions) for each alert. 80 | 81 | ``` 82 | $ wavectl show -o json alert | jq '{name,condition}' 83 | { 84 | "name": "Kubernetes - Node Network Utilization - HIGH (Prod)", 85 | "condition": "ts(proc.net.percent,server_type=\"compute-*\" and env=\"live\") > 80" 86 | } 87 | { 88 | "name": "Kubernetes - Node Cpu Utilization - HIGH (Prod)", 89 | "condition": "ts(proc.stat.cpu.percentage_used,server_type=\"compute-*\" and env=\"live\") > 80" 90 | } 91 | { 92 | "name": "Kubernetes - Node Memory Swap Utilization - HIGH (Prod)", 93 | "condition": "ts(proc.meminfo.percentage_swapused,server_type=\"compute-*\" and env=\"live\") > 10" 94 | ... 95 | ``` 96 | 97 | ### See the existing usages of a particular metric. 98 | 99 | You may want to see a metric's usages in all dashboard queries. You may be unsure about the semantics of a metric and seeing its correct usages definitely helps. 100 | 101 | Dashboards' json state can be inspected similarly to alerts. Seeing all dashboard queries regarding haproxy backends: 102 | 103 | ``` 104 | $ wavectl show -o json dashboard | grep haproxy_backend 105 | "query": "ts(octoproxy.haproxy_backend_connections_total, ${dev})", 106 | "query": "rate(ts(octoproxy.haproxy_backend_connections_total, ${dev}))", 107 | "query": "ts(octoproxy.haproxy_backend_response_errors_total, ${dev})", 108 | "query": "ts(octoproxy.haproxy_backend_retry_warnings_total, ${dev})", 109 | "query": "ts(octoproxy.haproxy_backend_up, ${dev})", 110 | "query": "ts(octoproxy.haproxy_backend_connections_total, env=live)", 111 | "query": "rate(ts(octoproxy.haproxy_backend_connections_total, env=live))", 112 | "query": "ts(octoproxy.haproxy_backend_retry_warnings_total, env=live)", 113 | "query": "ts(octoproxy.haproxy_backend_response_errors_total, env=live)", 114 | "query": "ts(octoproxy.haproxy_backend_up, env=live)", 115 | ``` 116 | 117 | ### See existing usages of advanced Wavefront functions. 118 | 119 | Some advanced functions in [Wavefront query language](https://docs.wavefront.com/query_language_reference.html) are not the easiest to learn. It is always helpful to see existing usages of a Wavefront function by your colleagues before writing your own. Take the [taggify](https://docs.wavefront.com/ts_taggify.html) as an example. 120 | 121 | ``` 122 | $ wavectl show -o json dashboard | grep taggify 123 | "query": "rawsum(aliasMetric(taggify(${RdyCalicUncrdndNdsWithPod},metric,pod,0),tagk,node,\"(.*)\",\"$1\"),pod,dc,metrics)", 124 | "query": "rawsum(taggify(${NotReadyCalicoPodsWithNodeInMetrics},metric,node,0),node,dc)", 125 | "query": "taggify(${RdyCalicUncrdndNds},tagk,node,node,0)", 126 | "query": "rawsum(taggify(${BirdUp},source,node,0),node,dc)", 127 | "query": "taggify(${RdyCalicUncrdndNds},tagk,node,node,0)", 128 | "query": "rawsum(taggify(${BgpState},source,node,0),node,dc)", 129 | "query": "taggify(${RdyCalicUncrdndNds},tagk,node,node,0)", 130 | ``` 131 | 132 | > After textually inspecting the alert, dashboard state you may want to jump to the Wavefront gui and see the time series there. For that you can use the wavectl [browser integration](BrowserIntegration.md). 133 | 134 | ### See all sections in all your dashboards. 135 | 136 | ``` 137 | $ wavectl show -o json dashboard | jq '{name: .name, sections: [.sections[].name]}' 138 | { 139 | "name": "Skynet Octoproxy Dev", 140 | "sections": [ 141 | "Vitals", 142 | "getEndpoints", 143 | "lbRestart", 144 | "getServices", 145 | "HA Proxy Backend Metrics", 146 | "HAProxy Frontend Metrics" 147 | ] 148 | } 149 | { 150 | "name": "Data Retention", 151 | "sections": [ 152 | "Retention", 153 | "Disposition", 154 | "Disposition Notifications Runmode - Sundays" 155 | ] 156 | } 157 | ... 158 | ``` 159 | -------------------------------------------------------------------------------- /test/test_show.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # In this file we have positive tests of the `show ... ` cmd 4 | 5 | from __future__ import absolute_import 6 | from __future__ import print_function 7 | import os 8 | import sys 9 | # Extend that path so that we can import the module under test. 10 | sys.path.insert(0, os.path.abspath('..')) 11 | 12 | import wavectl 13 | import unittest 14 | import json 15 | import copy 16 | import re 17 | import time 18 | import six 19 | import logging 20 | 21 | import util 22 | 23 | 24 | class Test(util.Test): 25 | 26 | def addCommasToDistionaryList(self, s): 27 | """ The string may be a back to back printed distionaries.For example: 28 | {a:b} {c:d} {e:f}. Python wants to have commas between the entries in a list. 29 | So in this function we add commas into the input string between 30 | disctionary entries {a:b}, {c:d}, {e:f}""" 31 | return re.sub(r"}\s*{", r"},\n{", s) 32 | 33 | def purgeOmittedFields(self, rsrcType, rsrcs): 34 | """A resource representation has some un-interesting fields. 35 | They are listed in the self.omittedFileds attribute. This function purges 36 | those key value pairs from each resource state. Operates on the given rsrcs 37 | parameter""" 38 | myRsrcs = copy.deepcopy(rsrcs) 39 | [[a.pop(k, None) for k in util.resourceTypeFromString( 40 | rsrcType)._omittedFields] for a in myRsrcs] 41 | return myRsrcs 42 | 43 | def executeShow(self, 44 | rsrcType, 45 | showAdditionalParams=[], 46 | rsrcAdditionalParams=[]): 47 | """Execute a resource show command and return the printed std out.""" 48 | 49 | args = ["show", 50 | "--wavefrontHost", util.wavefrontHostName, 51 | "--apiToken", util.wavefrontApiToken] \ 52 | + showAdditionalParams \ 53 | + [rsrcType] \ 54 | + rsrcAdditionalParams 55 | wc = wavectl.Wavectl(designForTestArgv=args) 56 | 57 | with util.StdoutCapture() as captOut: 58 | wc.runCmd() 59 | 60 | return captOut.str() 61 | 62 | def fullRsrcsWithMatch(self, rsrcType, rsrcs): 63 | """ A basic test case for show it expects to print all data in all of the 64 | resources.""" 65 | 66 | out = self.executeShow(rsrcType, 67 | showAdditionalParams=["--output=json"]) 68 | 69 | receivedRsrcs = json.loads( 70 | "[" + self.addCommasToDistionaryList(out) + "]") 71 | 72 | self.assertEqual(len(receivedRsrcs), len(rsrcs)) 73 | 74 | # Pick some key name that exists in all rsrcTypes 75 | def getCommonKey(x): return x.get("name") 76 | actRsrcs = sorted( 77 | self.purgeOmittedFields( 78 | rsrcType, 79 | rsrcs), 80 | key=getCommonKey) 81 | receivedRsrcs = sorted(receivedRsrcs, key=getCommonKey) 82 | # TODO: Here we only compare a small subset of keys of resources. This 83 | # made sense when the wavectl implementation was using wavefront v1 api 84 | # and tests were using v2 api. Recently all code has migrated to v2 api. 85 | # Ideally we should be comparing a lot more keys in resources. 86 | compareKeys = self.getCompareKeys(rsrcType) 87 | self.assertTrue(all([self.compareRsrcs(r1, r2, compareKeys) 88 | for (r1, r2) in zip(actRsrcs, receivedRsrcs)])) 89 | 90 | def test_fullRsrcsWithMatch(self): 91 | self.fullRsrcsWithMatch("alert", util.allAlerts) 92 | self.fullRsrcsWithMatch("dashboard", util.allDashboards) 93 | 94 | def fullRsrcsNoMatch(self, rsrcType, rsrcs): 95 | """ `show ` attempts to print the full state of resources but 96 | there are no matching resources""" 97 | 98 | out = self.executeShow( 99 | rsrcType, 100 | showAdditionalParams=["--output=json"], 101 | rsrcAdditionalParams=[ 102 | "--match", 103 | "unlikely_string_that_does_not_match_34kjh234"]) 104 | 105 | self.assertEqual(out.strip(), "") 106 | 107 | def test_fullRsrcsNoMatch(self): 108 | self.fullRsrcsNoMatch("alert", util.allAlerts) 109 | self.fullRsrcsNoMatch("dashboard", util.allDashboards) 110 | 111 | def summaryRsrcs( 112 | self, 113 | rsrcType, 114 | rsrcs, 115 | expectedOutRegex, 116 | rsrcAdditionalParams=[]): 117 | """ Only print the resource summaries. 118 | 119 | Gets the output lines from the stdout of the command. 120 | Compare it line by line with the given expectedOutRegex list. One regex 121 | per line""" 122 | 123 | out = self.executeShow(rsrcType, 124 | rsrcAdditionalParams=rsrcAdditionalParams) 125 | 126 | actualOut = out.strip().split("\n") 127 | util.SummaryLineProcessor.compareExpectedActualLineByLine( 128 | self, expectedOutRegex, actualOut) 129 | 130 | def test_summaryRsrcsWithMatch(self): 131 | expectedOut = [r"ID\s*NAME\s*STATUS\s*SEVERITY"] 132 | for r in util.allAlerts: 133 | expectedOut.append( 134 | util.SummaryLineProcessor.expectedAlertSummaryLineRegex(r)) 135 | self.summaryRsrcs("alert", util.allAlerts, expectedOut) 136 | 137 | expectedOut = [r"ID\s*NAME\s*DESCRIPTION"] 138 | for r in util.allDashboards: 139 | expectedOut.append( 140 | util.SummaryLineProcessor.expectedDashboardSummaryLineRegex(r)) 141 | self.summaryRsrcs("dashboard", util.allDashboards, expectedOut) 142 | 143 | def test_summaryRsrcsNoMatch(self): 144 | """ Attempt to print resource summaries but no reseource matches the given 145 | regexes""" 146 | expectedOut = [r"ID\s*NAME\s*STATUS\s*SEVERITY"] 147 | self.summaryRsrcs("alert", util.allAlerts, expectedOut, rsrcAdditionalParams=[ 148 | "--match", "unlikely_string_that_does_not_match_34kjh234"]) 149 | 150 | expectedOut = [r"ID\s*NAME\s*DESCRIPTION"] 151 | # The summary should print the created and the name for now. 152 | self.summaryRsrcs( 153 | "dashboard", 154 | util.allDashboards, 155 | expectedOut, 156 | rsrcAdditionalParams=[ 157 | "--match", 158 | "unlikely_string_that_does_not_match_34kjh234"]) 159 | 160 | def summaryRsrcsNoHeader(self, rsrcType): 161 | """Test the --no-header parameter. If --no--header is specified the 162 | summary table will not print an header""" 163 | 164 | out = self.executeShow( 165 | rsrcType, 166 | showAdditionalParams=["--no-header"], 167 | rsrcAdditionalParams=[ 168 | "--match", 169 | "unlikely_string_that_does_not_match_34kjh234"]) 170 | 171 | # With --no-header the output of an empty match should only me an empty 172 | # string 173 | self.assertEqual(out.strip(), "") 174 | 175 | def test_summaryRsrcsNoHeader(self): 176 | """Test the --no-header parameter. If --no--header is specified the 177 | summary table will not print an header""" 178 | self.summaryRsrcsNoHeader("alert") 179 | self.summaryRsrcsNoHeader("dashboard") 180 | 181 | def test_showWithCustomerTag(self): 182 | """The user passed two customerTags. In this test we first write all 183 | resources to the wavefront server. Then do a show with two customerTags. 184 | Only resources that container BOTH tags are expected to show""" 185 | expectedOut = [r"ID\s*NAME\s*STATUS\s*SEVERITY"] 186 | allAlerts = util.allAlerts 187 | # Only this alert has both tags. 188 | expectedAlerts = [util.Alert.kubernetesSkynetTag] 189 | for r in expectedAlerts: 190 | expectedOut.append( 191 | util.SummaryLineProcessor.expectedAlertSummaryLineRegex(r)) 192 | self.summaryRsrcs("alert", 193 | allAlerts, 194 | expectedOut, 195 | rsrcAdditionalParams=[ 196 | "--customerTag", "kubernetes", 197 | "--customerTag", "skynet"]) 198 | 199 | expectedOut = [r"ID\s*NAME\s*DESCRIPTION"] 200 | allDashboards = util.allDashboards 201 | # Only this dashboard has both tags. 202 | expectedDashboards = [util.Dashboard.skynetApplier] 203 | for r in expectedDashboards: 204 | expectedOut.append( 205 | util.SummaryLineProcessor.expectedDashboardSummaryLineRegex(r)) 206 | self.summaryRsrcs("dashboard", 207 | allDashboards, 208 | expectedOut, 209 | rsrcAdditionalParams=[ 210 | "--customerTag", "kubernetes", 211 | "--customerTag", "skynet"]) 212 | 213 | # TODO: 214 | # 1) Add a show in browser test with probably some mocking. 215 | # 216 | 217 | 218 | if __name__ == '__main__': 219 | # util.initLog() 220 | util.unittestMain() 221 | -------------------------------------------------------------------------------- /doc/GitIntegration.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | # Using wavectl with git 8 | 9 | One of the advanced features of `wavectl` is its git integration. It can be enabled using the `--inGit,-g` parameter to [pull](CommandReference.md#pull-options), [push](CommandReference.md#push-options) and [create](CommandReference.md#create-options) commands. `wavectl` gets several benefits via git integration that are not immediately obvious. Some effective use cases are as follows: 10 | 11 | ## Build a git history of alerts, dashboards with [`pull`](CommandReference.md#pull-options) 12 | 13 | Wavefront already keeps the change history of [alerts](https://docs.wavefront.com/alerts.html#viewing-alerts-and-alert-history) and [dashboards](https://docs.wavefront.com/dashboards_managing.html#managing-dashboard-versions). Those can be accessed via the browser gui. The powerful Wavefront v2 api also has endpoints to access the histories of [alerts](https://github.com/wavefrontHQ/python-client/blob/355698be1b53a32e81348f92549707dbcb4f6413/wavefront_api_client/api/alert_api.py#L428) and [dashboards](https://github.com/wavefrontHQ/python-client/blob/355698be1b53a32e81348f92549707dbcb4f6413/wavefront_api_client/api/dashboard_api.py#L523). With git integration in `wavectl` we do not add a brand new feature on top of those. Instead, we attempt to make the retrieval of the histories easier via the command line mostly using existing mechanisms in git. 14 | 15 | Passing `--inGit` to `pull` command initalizes a git repository in the pulled directory, if the directory gets created with the `pull` command. 16 | 17 | ``` 18 | $ wavectl pull --inGit /tmp/GitIntegrationPull/alerts alert 19 | ``` 20 | 21 | A git repo is created in the newly created /tmp/GitIntegrationPull/alerts directory. 22 | 23 | ``` 24 | $ git -C /tmp/GitIntegrationPull/alerts status 25 | On branch master 26 | nothing to commit, working tree clean 27 | ``` 28 | 29 | Each `pull` command creates a new commit with the changes pulled from the Wavefront server at that time. 30 | 31 | ``` 32 | $ git -C /tmp/GitIntegrationPull/alerts log --oneline 33 | 7c5147c Added files due to pull cmd:/Users/hbaba/box/src/skynet/wavectl/doc/bin/wavectl pull --inGit /tmp/GitIntegrationPull/alerts alert 34 | 385967c Initial commit with the README.md file 35 | ``` 36 | 37 | If you execute a long running daemon executing periodic pulls from Wavefront, an extensive git history can be built. The git history will correspond to users' edits to alerts and dashboards. 38 | 39 | ``` 40 | while true; do 41 | wavectl pull alert 42 | wavectl pull dashboard 43 | sleep 300 44 | done 45 | ``` 46 | 47 | Then, later on, if someone is interested understanding the changes to some alerts or dashboards, the investigation can be done with git show and log commands, which are widely known by programmers already. 48 | 49 | For example: 50 | 51 | ### When was an a particular alert created ? 52 | 53 | ``` 54 | $ git -C /tmp/GitIntegrationPull/alerts log $(ls /tmp/GitIntegrationPull/alerts | sort | head -n 1) 55 | commit 7c5147c408a0117b66ef74a2ac8edf44a69e9685 56 | Author: Hakan Baba 57 | Date: Fri Feb 8 21:37:16 2019 -0800 58 | 59 | Added files due to pull cmd:/Users/hbaba/box/src/skynet/wavectl/doc/bin/wavectl pull --inGit /tmp/GitIntegrationPull/alerts alert 60 | ``` 61 | 62 | ### When were each alert snoozed ? 63 | 64 | ``` 65 | $ git -C /tmp/GitIntegrationPull/alerts log -S snoozed 66 | commit 7c5147c408a0117b66ef74a2ac8edf44a69e9685 67 | Author: Hakan Baba 68 | Date: Fri Feb 8 21:37:16 2019 -0800 69 | 70 | Added files due to pull cmd:/Users/hbaba/box/src/skynet/wavectl/doc/bin/wavectl pull --inGit /tmp/GitIntegrationPull/alerts alert 71 | ``` 72 | 73 | > NOTE: The updater id and the actual update time for each alert and dashboard can be retrieved using the history endpoing in [Wavefront API](https://docs.wavefront.com/wavefront_api.html). However, in the current `wavectl` implementation, the git commit messages do not contain either of them. In future `wavectl` releases we plan to improve the git integration of pull command to include the updater id and the update time. 74 | 75 | ## Using [git-diff](https://git-scm.com/docs/git-diff) to make safe local modifications to your alerts, dashboards with [`push`](CommandReference.md#push-options) 76 | 77 | In the [repetitive editing doc](RepetitiveEditing.md) we have demonstrated an example using the [`sed`](https://www.gnu.org/software/sed/manual/sed.html) command to search and replace strings on local files. After the modifications, the alert, dashboard json files were written to Wavefront. 78 | 79 | It is always good practice to inspect the changes to alerts and dashboards before writing them back to Wavefront with the [`push`](CommandReference.md#push-options) command. The git integration for the push command can provide a diff of local modifications for the user to verify. Since the git integration will work on a local git repo, [git-diff](https://git-scm.com/docs/git-diff) can be used for the diff generation. 80 | 81 | Using the same example from the [repetitive editing doc](RepetitiveEditing.md), we first pull the alerts matching a regular expression. But this time we use the git integration command line parameter `--inGit,-g` 82 | 83 | ``` 84 | $ wavectl pull --inGit /tmp/GitIntegrationPush/alerts alert --match "proc\." 85 | ``` 86 | 87 | Then the local modifications are executed: 88 | 89 | ``` 90 | $ find /tmp/GitIntegrationPush -type f | xargs sed -i -e 's/proc\./host.proc./g' 91 | ``` 92 | 93 | See the modifications to alerts that are going to be pushed to Wavefront: 94 | 95 | ``` 96 | $ git -C /tmp/GitIntegrationPush/alerts diff HEAD 97 | diff --git a/1530723441304.alert b/1530723441304.alert 98 | index a1bb891..ec02df7 100644 99 | --- a/1530723441304.alert 100 | +++ b/1530723441304.alert 101 | @@ -1,7 +1,7 @@ 102 | { 103 | "additionalInformation": "This alert tracks the used network bandwidth percentage for all the compute-* (compute-master and compute-node) machines. If the cpu utilization exceeds 80%, this alert fires.", 104 | - "condition": "ts(proc.net.percent,server_type=\"compute-*\" and env=\"live\") > 80", 105 | - "displayExpression": "ts(proc.net.percent,server_type=\"compute-*\" and env=\"live\")", 106 | + "condition": "ts(host.proc.net.percent,server_type=\"compute-*\" and env=\"live\") > 80", 107 | + "displayExpression": "ts(host.proc.net.percent,server_type=\"compute-*\" and env=\"live\")", 108 | "id": "1530723441304", 109 | "minutes": 2, 110 | "name": "Kubernetes - Node Network Utilization - HIGH (Prod)", 111 | diff --git a/1530723441442.alert b/1530723441442.alert 112 | index ad9e8ef..bb0bf57 100644 113 | --- a/1530723441442.alert 114 | +++ b/1530723441442.alert 115 | @@ -1,7 +1,7 @@ 116 | { 117 | "additionalInformation": "This alert tracks the used cpu percentage for all the compute-* (compute-master and compute-node) machines. If the cpu utilization exceeds 80%, this alert fires.", 118 | - "condition": "ts(proc.stat.cpu.percentage_used,server_type=\"compute-*\" and env=\"live\") > 80", 119 | - "displayExpression": "ts(proc.stat.cpu.percentage_used,server_type=\"compute-*\" and env=\"dev\")", 120 | + "condition": "ts(host.proc.stat.cpu.percentage_used,server_type=\"compute-*\" and env=\"live\") > 80", 121 | + "displayExpression": "ts(host.proc.stat.cpu.percentage_used,server_type=\"compute-*\" and env=\"dev\")", 122 | "id": "1530723441442", 123 | "minutes": 2, 124 | "name": "Kubernetes - Node Cpu Utilization - HIGH (Prod)", 125 | diff --git a/1530723441589.alert b/1530723441589.alert 126 | index e7a0d7f..830d19f 100644 127 | --- a/1530723441589.alert 128 | +++ b/1530723441589.alert 129 | ... 130 | ``` 131 | 132 | Submit your changes to the local repo: 133 | 134 | ``` 135 | $ git -C /tmp/GitIntegrationPush/alerts commit -a -m "proc. is replaced with host.proc." 136 | [master f33e513] proc. is replaced with host.proc. 137 | 4 files changed, 21 insertions(+), 21 deletions(-) 138 | rewrite 1530723443146.alert (67%) 139 | ``` 140 | 141 | > NOTE: If you are using git integration, `wavectl` will not let you push unless you have committed your changes to the repo. This behavior is like a safeguard to ensure that the user is fully aware of what he is writing to Wavefont via `wavectl push`. Asking the user to commit his local changes, serves to ensure that she has inspected the diff and is OK with the modifications. 142 | 143 | Lastly, push your local modifications to Wavefront. 144 | 145 | ``` 146 | $ wavectl push --inGit /tmp/GitIntegrationPush/alerts alert 147 | Replaced alert(s): 148 | ID NAME STATUS SEVERITY 149 | 1530723441304 Kubernetes - Node Network Utilization - HIGH (Prod) WARN 150 | 1530723441442 Kubernetes - Node Cpu Utilization - HIGH (Prod) WARN 151 | 1530723441589 Kubernetes - Node Memory Swap Utilization - HIGH (Prod) WARN 152 | 1530723443146 Collections Dev High CPU WARN 153 | ``` 154 | 155 | > NOTE: Even with all the safeguards, if you have made a mistake and pushed to Wavefront, you can roll back the git repo to the previous commit. A command like `git checkout HEAD^` will remove your most recent changes from local files. After that, you can re-execute the push command. That will update all alerts one more time and will set them to the previous state. 156 | -------------------------------------------------------------------------------- /doc/RepetitiveEditing.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | # Repetitive editing of alerts, dashboards 8 | 9 | At times one needs to change multiple alerts or various queries in several dashboards at once. A change in a metric name can cause something like this. For example the metric generator in kubernetes, [kube-state-metrics](https://github.com/kubernetes/kube-state-metrics), occasionally changes a metric's name. 10 | 11 | After such a change, the platform owner needs to update all alerts and dashboards with that metric. Searching for that in the Wavefront GUI and updating the queries manually can be very labor intensive, error prone and time consuming. `wavectl` can help with automating such a global change. 12 | 13 | For the sake of an example, let's say because of an upsteam change, all metrics that started with `proc.` have been renamed to start with `host.proc.`. Once this upstream change gets deployed, numerous alerts and dashboards will be broken. They will try to display the old metric name and will not show data. In order to quickly fix this problem via `wavectl` we first [`pull`](CommandReference.md#pull-options) all alerts and resources that match the `proc\.` regular expression. The [`--match`](CommandReference#resource-options) option can be used to narrow down the returned set via a regular expression search. 14 | 15 | ``` 16 | $ wavectl pull /tmp/RepetitiveEditing/alerts alert --match "proc\." 17 | 18 | $ wavectl pull /tmp/RepetitiveEditing/dashboards dashboard --match "proc\." 19 | ``` 20 | 21 | See the pulled alerts, dashboards. 22 | 23 | ``` 24 | $ find /tmp/RepetitiveEditing -type f 25 | /tmp/RepetitiveEditing/alerts/1530723441304.alert 26 | /tmp/RepetitiveEditing/alerts/1530723441442.alert 27 | /tmp/RepetitiveEditing/alerts/1530723441589.alert 28 | /tmp/RepetitiveEditing/alerts/1530723443146.alert 29 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard 30 | /tmp/RepetitiveEditing/dashboards/octoproxy.dashboard 31 | ``` 32 | 33 | See the usage of the metrics starting with `proc.` in pulled alerts, dashboards. 34 | 35 | ``` 36 | $ find /tmp/RepetitiveEditing -type f | xargs grep "proc." 37 | /tmp/RepetitiveEditing/alerts/1530723441304.alert: "condition": "ts(proc.net.percent,server_type=\"compute-*\" and env=\"live\") > 80", 38 | /tmp/RepetitiveEditing/alerts/1530723441304.alert: "displayExpression": "ts(proc.net.percent,server_type=\"compute-*\" and env=\"live\")", 39 | /tmp/RepetitiveEditing/alerts/1530723441442.alert: "condition": "ts(proc.stat.cpu.percentage_used,server_type=\"compute-*\" and env=\"live\") > 80", 40 | /tmp/RepetitiveEditing/alerts/1530723441442.alert: "displayExpression": "ts(proc.stat.cpu.percentage_used,server_type=\"compute-*\" and env=\"dev\")", 41 | /tmp/RepetitiveEditing/alerts/1530723441589.alert: "condition": "ts(proc.meminfo.percentage_swapused,server_type=\"compute-*\" and env=\"live\") > 10", 42 | /tmp/RepetitiveEditing/alerts/1530723441589.alert: "displayExpression": "ts(proc.meminfo.percentage_swapused,server_type=\"compute-*\" and env=\"live\")", 43 | /tmp/RepetitiveEditing/alerts/1530723443146.alert: "condition": "max(((sum(rate(ts(proc.stat.cpu, namespace=\"collections-service-dev\" and type=used)), pod_name) /100) / (sum(taggify(ts(kube.metrics.pod_container_resource_requests_cpu_cores, namespace=\"collections-service-dev\"), tagk, pod, pod_name, \"\", \"\"), pod_name)) * 100)) > 70", 44 | /tmp/RepetitiveEditing/alerts/1530723443146.alert: "displayExpression": "(sum(rate(ts(proc.stat.cpu, namespace=\"collections-service-dev\" and type=used)), pod_name) /100) / (sum(taggify(ts(kube.metrics.pod_container_resource_requests_cpu_cores, namespace=\"collections-service-dev\" ), tagk, pod, pod_name, \"\", \"\"), pod_name)) * 100", 45 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(\"proc.kernel.entropy_avail\", host=${metadata_server})", 46 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(proc.stat.cpu.percentage_used, ${PerfPod} and host=${metadata_server})", 47 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(proc.stat.cpu.percentage_used, ${PerfPod} and host=${metadata_db02})", 48 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(proc.stat.cpu.percentage_used, ${PerfPod} and host=${metadata_database})", 49 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(proc.stat.cpu.percentage_used, ${PerfPod} and host=${eng01})", 50 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(proc.stat.cpu.percentage_used, ${PerfPod} and host=${content01})", 51 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(proc.stat.cpu.percentage_iowait, ${PerfPod} and host=${metadata_server})", 52 | ... 53 | ``` 54 | 55 | Then using [`sed`](https://www.gnu.org/software/sed/manual/sed.html) replace all occurances of `proc.` with `host.proc.` 56 | 57 | ``` 58 | $ find /tmp/RepetitiveEditing -type f | xargs sed -i -e 's/proc\./host.proc./g' 59 | ``` 60 | 61 | Check the changes you have make 62 | 63 | ``` 64 | $ find /tmp/RepetitiveEditing -type f | xargs grep "host.proc." 65 | /tmp/RepetitiveEditing/alerts/1530723441304.alert: "condition": "ts(host.proc.net.percent,server_type=\"compute-*\" and env=\"live\") > 80", 66 | /tmp/RepetitiveEditing/alerts/1530723441304.alert: "displayExpression": "ts(host.proc.net.percent,server_type=\"compute-*\" and env=\"live\")", 67 | /tmp/RepetitiveEditing/alerts/1530723441442.alert: "condition": "ts(host.proc.stat.cpu.percentage_used,server_type=\"compute-*\" and env=\"live\") > 80", 68 | /tmp/RepetitiveEditing/alerts/1530723441442.alert: "displayExpression": "ts(host.proc.stat.cpu.percentage_used,server_type=\"compute-*\" and env=\"dev\")", 69 | /tmp/RepetitiveEditing/alerts/1530723441589.alert: "condition": "ts(host.proc.meminfo.percentage_swapused,server_type=\"compute-*\" and env=\"live\") > 10", 70 | /tmp/RepetitiveEditing/alerts/1530723441589.alert: "displayExpression": "ts(host.proc.meminfo.percentage_swapused,server_type=\"compute-*\" and env=\"live\")", 71 | /tmp/RepetitiveEditing/alerts/1530723443146.alert: "condition": "max(((sum(rate(ts(host.proc.stat.cpu, namespace=\"collections-service-dev\" and type=used)), pod_name) /100) / (sum(taggify(ts(kube.metrics.pod_container_resource_requests_cpu_cores, namespace=\"collections-service-dev\"), tagk, pod, pod_name, \"\", \"\"), pod_name)) * 100)) > 70", 72 | /tmp/RepetitiveEditing/alerts/1530723443146.alert: "displayExpression": "(sum(rate(ts(host.proc.stat.cpu, namespace=\"collections-service-dev\" and type=used)), pod_name) /100) / (sum(taggify(ts(kube.metrics.pod_container_resource_requests_cpu_cores, namespace=\"collections-service-dev\" ), tagk, pod, pod_name, \"\", \"\"), pod_name)) * 100", 73 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(\"host.proc.kernel.entropy_avail\", host=${metadata_server})", 74 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(host.proc.stat.cpu.percentage_used, ${PerfPod} and host=${metadata_server})", 75 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(host.proc.stat.cpu.percentage_used, ${PerfPod} and host=${metadata_db02})", 76 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(host.proc.stat.cpu.percentage_used, ${PerfPod} and host=${metadata_database})", 77 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(host.proc.stat.cpu.percentage_used, ${PerfPod} and host=${eng01})", 78 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(host.proc.stat.cpu.percentage_used, ${PerfPod} and host=${content01})", 79 | /tmp/RepetitiveEditing/dashboards/metadata-perfpod.dashboard: "query": "ts(host.proc.stat.cpu.percentage_iowait, ${PerfPod} and host=${metadata_server})", 80 | ... 81 | ``` 82 | 83 | Replace the Wavefront alerts and dashboards using [`wavectl push`](CommandReference.md#push-options) 84 | 85 | ``` 86 | $ wavectl push /tmp/RepetitiveEditing/alerts alert 87 | Replaced alert(s): 88 | ID NAME STATUS SEVERITY 89 | 1530723441304 Kubernetes - Node Network Utilization - HIGH (Prod) WARN 90 | 1530723441442 Kubernetes - Node Cpu Utilization - HIGH (Prod) WARN 91 | 1530723441589 Kubernetes - Node Memory Swap Utilization - HIGH (Prod) WARN 92 | 1530723443146 Collections Dev High CPU WARN 93 | 94 | $ wavectl push /tmp/RepetitiveEditing/dashboards dashboard 95 | Replaced dashboard(s): 96 | ID NAME DESCRIPTION 97 | metadata-perfpod Metadata PerfPod Monitors for testing Metadata in the PerfPods 98 | octoproxy Skynet Octoproxy One look summary about the load balancer 99 | ``` 100 | 101 | After these steps all your alerts and dashboards in Wavefront will use the new metric names. 102 | 103 | > NOTE: Doing local modifications via `sed` like commands and writing the resulting files to Wavefront may be risky and dangerous. Some unintended changes may be written to Wavefront by mistake. If you want to execute safer local modifications, where you have a better handle on the resulting diff, take a look at the [git integration to push command](GitIntegration.md) section. 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | "License" shall mean the terms and conditions for use, reproduction, 9 | and distribution as defined by Sections 1 through 9 of this document. 10 | "Licensor" shall mean the copyright owner or entity authorized by 11 | the copyright owner that is granting the License. 12 | "Legal Entity" shall mean the union of the acting entity and all 13 | other entities that control, are controlled by, or are under common 14 | control with that entity. For the purposes of this definition, 15 | "control" means (i) the power, direct or indirect, to cause the 16 | direction or management of such entity, whether by contract or 17 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 18 | outstanding shares, or (iii) beneficial ownership of such entity. 19 | "You" (or "Your") shall mean an individual or Legal Entity 20 | exercising permissions granted by this License. 21 | "Source" form shall mean the preferred form for making modifications, 22 | including but not limited to software source code, documentation 23 | source, and configuration files. 24 | "Object" form shall mean any form resulting from mechanical 25 | transformation or translation of a Source form, including but 26 | not limited to compiled object code, generated documentation, 27 | and conversions to other media types. 28 | "Work" shall mean the work of authorship, whether in Source or 29 | Object form, made available under the License, as indicated by a 30 | copyright notice that is included in or attached to the work 31 | (an example is provided in the Appendix below). 32 | "Derivative Works" shall mean any work, whether in Source or Object 33 | form, that is based on (or derived from) the Work and for which the 34 | editorial revisions, annotations, elaborations, or other modifications 35 | represent, as a whole, an original work of authorship. For the purposes 36 | of this License, Derivative Works shall not include works that remain 37 | separable from, or merely link (or bind by name) to the interfaces of, 38 | the Work and Derivative Works thereof. 39 | "Contribution" shall mean any work of authorship, including 40 | the original version of the Work and any modifications or additions 41 | to that Work or Derivative Works thereof, that is intentionally 42 | submitted to Licensor for inclusion in the Work by the copyright owner 43 | or by an individual or Legal Entity authorized to submit on behalf of 44 | the copyright owner. For the purposes of this definition, "submitted" 45 | means any form of electronic, verbal, or written communication sent 46 | to the Licensor or its representatives, including but not limited to 47 | communication on electronic mailing lists, source code control systems, 48 | and issue tracking systems that are managed by, or on behalf of, the 49 | Licensor for the purpose of discussing and improving the Work, but 50 | excluding communication that is conspicuously marked or otherwise 51 | designated in writing by the copyright owner as "Not a Contribution." 52 | "Contributor" shall mean Licensor and any individual or Legal Entity 53 | on behalf of whom a Contribution has been received by Licensor and 54 | subsequently incorporated within the Work. 55 | 56 | 2. Grant of Copyright License. Subject to the terms and conditions of 57 | this License, each Contributor hereby grants to You a perpetual, 58 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 59 | copyright license to reproduce, prepare Derivative Works of, 60 | publicly display, publicly perform, sublicense, and distribute the 61 | Work and such Derivative Works in Source or Object form. 62 | 63 | 3. Grant of Patent License. Subject to the terms and conditions of 64 | this License, each Contributor hereby grants to You a perpetual, 65 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 66 | (except as stated in this section) patent license to make, have made, 67 | use, offer to sell, sell, import, and otherwise transfer the Work, 68 | where such license applies only to those patent claims licensable 69 | by such Contributor that are necessarily infringed by their 70 | Contribution(s) alone or by combination of their Contribution(s) 71 | with the Work to which such Contribution(s) was submitted. If You 72 | institute patent litigation against any entity (including a 73 | cross-claim or counterclaim in a lawsuit) alleging that the Work 74 | or a Contribution incorporated within the Work constitutes direct 75 | or contributory patent infringement, then any patent licenses 76 | granted to You under this License for that Work shall terminate 77 | as of the date such litigation is filed. 78 | 79 | 4. Redistribution. You may reproduce and distribute copies of the 80 | Work or Derivative Works thereof in any medium, with or without 81 | modifications, and in Source or Object form, provided that You 82 | meet the following conditions: 83 | 84 | (a) You must give any other recipients of the Work or 85 | Derivative Works a copy of this License; and 86 | 87 | (b) You must cause any modified files to carry prominent notices 88 | stating that You changed the files; and 89 | 90 | (c) You must retain, in the Source form of any Derivative Works 91 | that You distribute, all copyright, patent, trademark, and 92 | attribution notices from the Source form of the Work, 93 | excluding those notices that do not pertain to any part of 94 | the Derivative Works; and 95 | 96 | (d) If the Work includes a "NOTICE" text file as part of its 97 | distribution, then any Derivative Works that You distribute must 98 | include a readable copy of the attribution notices contained 99 | within such NOTICE file, excluding those notices that do not 100 | pertain to any part of the Derivative Works, in at least one 101 | of the following places: within a NOTICE text file distributed 102 | as part of the Derivative Works; within the Source form or 103 | documentation, if provided along with the Derivative Works; or, 104 | within a display generated by the Derivative Works, if and 105 | wherever such third-party notices normally appear. The contents 106 | of the NOTICE file are for informational purposes only and 107 | do not modify the License. You may add Your own attribution 108 | notices within Derivative Works that You distribute, alongside 109 | or as an addendum to the NOTICE text from the Work, provided 110 | that such additional attribution notices cannot be construed 111 | as modifying the License. 112 | 113 | You may add Your own copyright statement to Your modifications and 114 | may provide additional or different license terms and conditions 115 | for use, reproduction, or distribution of Your modifications, or 116 | for any such Derivative Works as a whole, provided Your use, 117 | reproduction, and distribution of the Work otherwise complies with 118 | the conditions stated in this License. 119 | 120 | 5. Submission of Contributions. Unless You explicitly state otherwise, 121 | any Contribution intentionally submitted for inclusion in the Work 122 | by You to the Licensor shall be under the terms and conditions of 123 | this License, without any additional terms or conditions. 124 | Notwithstanding the above, nothing herein shall supersede or modify 125 | the terms of any separate license agreement you may have executed 126 | with Licensor regarding such Contributions. 127 | 128 | 6. Trademarks. This License does not grant permission to use the trade 129 | names, trademarks, service marks, or product names of the Licensor, 130 | except as required for reasonable and customary use in describing the 131 | origin of the Work and reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. Unless required by applicable law or 134 | agreed to in writing, Licensor provides the Work (and each 135 | Contributor provides its Contributions) on an "AS IS" BASIS, 136 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 137 | implied, including, without limitation, any warranties or conditions 138 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 139 | PARTICULAR PURPOSE. You are solely responsible for determining the 140 | appropriateness of using or redistributing the Work and assume any 141 | risks associated with Your exercise of permissions under this License. 142 | 143 | 8. Limitation of Liability. In no event and under no legal theory, 144 | whether in tort (including negligence), contract, or otherwise, 145 | unless required by applicable law (such as deliberate and grossly 146 | negligent acts) or agreed to in writing, shall any Contributor be 147 | liable to You for damages, including any direct, indirect, special, 148 | incidental, or consequential damages of any character arising as a 149 | result of this License or out of the use or inability to use the 150 | Work (including but not limited to damages for loss of goodwill, 151 | work stoppage, computer failure or malfunction, or any and all 152 | other commercial damages or losses), even if such Contributor 153 | has been advised of the possibility of such damages. 154 | 155 | 9. Accepting Warranty or Additional Liability. While redistributing 156 | the Work or Derivative Works thereof, You may choose to offer, 157 | and charge a fee for, acceptance of support, warranty, indemnity, 158 | or other liability obligations and/or rights consistent with this 159 | License. However, in accepting such obligations, You may act only 160 | on Your own behalf and on Your sole responsibility, not on behalf 161 | of any other Contributor, and only if You agree to indemnify, 162 | defend, and hold each Contributor harmless for any liability 163 | incurred by, or claims asserted against, such Contributor by reason 164 | of your accepting any such warranty or additional liability. 165 | 166 | END OF TERMS AND CONDITIONS 167 | --------------------------------------------------------------------------------