Skip to content
This repository was archived by the owner on Oct 23, 2025. It is now read-only.

Commit 5ff035b

Browse files
authored
Merge pull request #327 from zalando-stups/sentry
Sentry
2 parents 45d4a2b + 3bcdcb8 commit 5ff035b

File tree

15 files changed

+386
-68
lines changed

15 files changed

+386
-68
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ stups-pierone>=1.0.34
66
boto3>=1.3.0
77
botocore>=1.4.10
88
pytest>=2.7.3
9+
raven
910
typing

senza/arguments.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import click
21
import re
32

3+
import click
4+
45
from .error_handling import HandleExceptions
56

67
REGION_PATTERN = re.compile(r'^[a-z]{2}-[a-z]+-[0-9]$')
@@ -54,3 +55,4 @@ def set_stacktrace_visible(ctx, param, value):
5455
type=click.IntRange(1, 300),
5556
metavar='SECS',
5657
help='Auto update the screen every X seconds')
58+
GLOBAL_OPTIONS = {}

senza/cli.py

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,13 @@
1818
import boto3
1919
import click
2020
import requests
21-
import senza
2221
import yaml
2322
from botocore.exceptions import ClientError
24-
from clickclick import (Action, AliasedGroup, FloatRange, OutputFormat, choice,
25-
error, fatal_error, info, ok)
23+
from clickclick import (Action, FloatRange, OutputFormat, choice, error,
24+
fatal_error, info, ok)
2625
from clickclick.console import print_table
2726

28-
from .arguments import (json_output_option, output_option,
27+
from .arguments import (GLOBAL_OPTIONS, json_output_option, output_option,
2928
parameter_file_option, region_option,
3029
stacktrace_visible_option, watch_option,
3130
watchrefresh_option)
@@ -43,15 +42,15 @@
4342
from .patch import patch_auto_scaling_group
4443
from .respawn import get_auto_scaling_group, respawn_auto_scaling_group
4544
from .stups.piu import Piu
45+
from .subcommands.config import cmd_config
46+
from .subcommands.root import cli
4647
from .templates import get_template_description, get_templates
4748
from .templates._helper import get_mint_bucket_name
4849
from .traffic import (change_version_traffic, get_records,
4950
print_version_traffic, resolve_to_ip_addresses)
5051
from .utils import (camel_case_to_underscore, ensure_keys, named_value,
5152
pystache_render)
5253

53-
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
54-
5554
STYLES = {
5655
'RUNNING': {'fg': 'green'},
5756
'TERMINATED': {'fg': 'red'},
@@ -117,8 +116,6 @@
117116
'ResourceStatusReason': 50
118117
}
119118

120-
GLOBAL_OPTIONS = {}
121-
122119

123120
def print_json(data, output=None):
124121
if output == 'yaml':
@@ -205,13 +202,6 @@ def watching(w: bool, watch: int):
205202
}
206203

207204

208-
def print_version(ctx, param, value):
209-
if not value or ctx.resilient_parsing:
210-
return
211-
click.echo('Senza {}'.format(senza.__version__))
212-
ctx.exit()
213-
214-
215205
def evaluate(definition, args, account_info, force: bool):
216206
# extract Senza* meta information
217207
info = definition.pop("SenzaInfo")
@@ -255,14 +245,6 @@ def evaluate(definition, args, account_info, force: bool):
255245
return definition
256246

257247

258-
@click.group(cls=AliasedGroup, context_settings=CONTEXT_SETTINGS)
259-
@click.option('-V', '--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True,
260-
help='Print the current version number and exit.')
261-
@region_option
262-
def cli(region):
263-
GLOBAL_OPTIONS['region'] = region
264-
265-
266248
class TemplateArguments:
267249
def __init__(self, **kwargs):
268250
for key, val in kwargs.items():
@@ -1634,6 +1616,8 @@ def wait(stack_ref, region, deletion, timeout, interval):
16341616
return
16351617
raise click.Abort()
16361618

1619+
cli.add_command(cmd_config)
1620+
16371621

16381622
def main():
16391623
HandleExceptions(cli)()

senza/configuration.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from collections.abc import MutableMapping
2+
from pathlib import Path
3+
from typing import Dict, Tuple
4+
5+
import yaml
6+
from click import get_app_dir
7+
8+
from .exceptions import InvalidConfigKey
9+
10+
11+
class Configuration(MutableMapping):
12+
13+
def __init__(self, path: Path):
14+
self.config_path = path
15+
16+
def __iter__(self):
17+
yield from self.raw_dict
18+
19+
def __len__(self):
20+
return len(self.raw_dict)
21+
22+
def __getitem__(self, key: str) -> str:
23+
section, sub_key = self.__split_key(key)
24+
return self.raw_dict[section][sub_key]
25+
26+
def __setitem__(self, key: str, value):
27+
section, sub_key = self.__split_key(key)
28+
configuration = self.raw_dict
29+
30+
if section not in configuration:
31+
configuration[section] = {}
32+
configuration[section][sub_key] = str(value)
33+
self.__save(configuration)
34+
35+
def __delitem__(self, key):
36+
section, sub_key = self.__split_key(key)
37+
cfg = self.raw_dict
38+
del cfg[section][sub_key]
39+
self.__save(cfg)
40+
41+
@staticmethod
42+
def __split_key(key: str) -> Tuple[str, str]:
43+
"""
44+
Splits the full key in section and subkey
45+
"""
46+
try:
47+
section, sub_key = key.split('.', 1)
48+
except ValueError:
49+
# error message inspired by git config
50+
raise InvalidConfigKey('key does not contain '
51+
'a section: {}'.format(key))
52+
return section, sub_key
53+
54+
def __save(self, cfg):
55+
"""
56+
Saves the configuration in the configuration path, creating the
57+
directory if necessary.
58+
"""
59+
try:
60+
self.config_path.parent.mkdir(parents=True)
61+
except FileExistsError:
62+
# this try...except can be replaced with exist_ok=True when
63+
# we drop python3.4 support
64+
pass
65+
with self.config_path.open('w+') as config_file:
66+
yaml.safe_dump(cfg, config_file,
67+
default_flow_style=False)
68+
69+
@property
70+
def raw_dict(self) -> Dict[str, Dict[str, str]]:
71+
"""
72+
Returns a dict with the configuration data as stored in config.yaml
73+
"""
74+
try:
75+
with self.config_path.open() as config_file:
76+
cfg = yaml.safe_load(config_file)
77+
except FileNotFoundError:
78+
cfg = {}
79+
return cfg
80+
81+
configuration = Configuration(Path(get_app_dir('senza')) / "config.yaml")

senza/error_handling.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
from typing import Dict, Any # noqa: F401
21
import sys
32
from tempfile import NamedTemporaryFile
43
from traceback import format_exception
4+
from typing import Any, Dict, Optional # noqa: F401
55

6+
import senza
67
import yaml.constructor
78
from botocore.exceptions import ClientError, NoCredentialsError
89
from clickclick import fatal_error
10+
from raven import Client
911

10-
from .exceptions import PiuNotFound, InvalidDefinition
12+
from .configuration import configuration
13+
from .exceptions import InvalidDefinition, PiuNotFound
1114
from .manaus.exceptions import (ELBNotFound, HostedZoneNotFound, InvalidState,
1215
RecordNotFound)
1316

@@ -59,12 +62,20 @@ def __init__(self, function):
5962
self.function = function
6063

6164
def die_unknown_error(self, e: Exception):
62-
if not self.stacktrace_visible:
65+
if sentry:
66+
# The exception should always be sent to sentry if sentry is
67+
# configured
68+
sentry.captureException()
69+
if self.stacktrace_visible:
70+
raise e
71+
elif sentry:
72+
die_fatal_error("Unknown Error: {e}.\n"
73+
"This error will be pushed to sentry ".format(e=e))
74+
elif not sentry:
6375
file_name = store_exception(e)
6476
die_fatal_error('Unknown Error: {e}.\n'
6577
'Please create an issue with the '
6678
'content of {fn}'.format(e=e, fn=file_name))
67-
raise e
6879

6980
def __call__(self, *args, **kwargs):
7081
try:
@@ -108,3 +119,19 @@ def __call__(self, *args, **kwargs):
108119
except Exception as e:
109120
# Catch All
110121
self.die_unknown_error(e)
122+
123+
124+
def setup_sentry(sentry_endpoint: Optional[str]):
125+
"""
126+
This function setups sentry, this exists mostly to make sentry integration
127+
easier to test
128+
"""
129+
if sentry_endpoint is not None:
130+
sentry = Client(sentry_endpoint,
131+
release=senza.__version__)
132+
else:
133+
sentry = None
134+
135+
return sentry
136+
137+
sentry = setup_sentry(configuration.get('sentry.endpoint'))

senza/exceptions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ def __init__(self):
2020
super().__init__('Command not found: piu')
2121

2222

23+
class InvalidConfigKey(SenzaException, ValueError):
24+
"""
25+
Error raised when trying to use an Invalid Config Key
26+
"""
27+
28+
def __init__(self, message: str):
29+
super().__init__(message)
30+
31+
2332
class InvalidDefinition(SenzaException):
2433
"""
2534
Exception raised when trying to parse and invalid senza definition

senza/subcommands/__init__.py

Whitespace-only changes.

senza/subcommands/config.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import Optional
2+
3+
from click import argument, command
4+
from click.exceptions import BadArgumentUsage
5+
6+
from ..configuration import configuration
7+
from ..exceptions import InvalidConfigKey
8+
9+
10+
@command('config')
11+
@argument('key')
12+
@argument('value', required=False)
13+
def cmd_config(key: str, value: Optional[str]):
14+
"""
15+
Get and set senza options.
16+
"""
17+
if value is None:
18+
try:
19+
value = configuration[key]
20+
print(value)
21+
except InvalidConfigKey as e:
22+
raise BadArgumentUsage(e)
23+
except KeyError:
24+
exit(1)
25+
else:
26+
try:
27+
configuration[key] = value
28+
except InvalidConfigKey as e:
29+
raise BadArgumentUsage(e)

senza/subcommands/root.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import click
2+
import senza
3+
from clickclick import AliasedGroup
4+
5+
from ..arguments import GLOBAL_OPTIONS, region_option
6+
7+
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
8+
9+
10+
def print_version(ctx, param, value):
11+
if not value or ctx.resilient_parsing:
12+
return
13+
click.echo('Senza {}'.format(senza.__version__))
14+
ctx.exit()
15+
16+
17+
@click.group(cls=AliasedGroup, context_settings=CONTEXT_SETTINGS)
18+
@click.option('-V', '--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True,
19+
help='Print the current version number and exit.')
20+
@region_option
21+
def cli(region):
22+
GLOBAL_OPTIONS['region'] = region

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def setup_package():
130130
install_requires=install_reqs,
131131
setup_requires=['flake8'],
132132
cmdclass=cmdclass,
133-
tests_require=['pytest-cov', 'pytest'],
133+
tests_require=['pytest-cov', 'pytest', 'mock'],
134134
command_options=command_options,
135135
entry_points={'console_scripts': CONSOLE_SCRIPTS,
136136
'senza.templates': ['bgapp = senza.templates.bgapp',

0 commit comments

Comments
 (0)