Skip to content

Commit ce6999d

Browse files
committed
Allow dashboards created with API v1 to be uploaded to API v2
Merge from #86
2 parents df8aabb + f98d049 commit ce6999d

File tree

4 files changed

+350
-9
lines changed

4 files changed

+350
-9
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env python
2+
#
3+
# Save the first user dashboard to file and then use create_dashboard_from_file()
4+
# to apply the stored dasboard again with a different filter.
5+
#
6+
import os
7+
import sys
8+
import json
9+
sys.path.insert(
10+
0, os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), '..'))
11+
from sdcclient import SdMonitorClient
12+
from sdcclient import SdMonitorClientV1
13+
14+
#
15+
# Parse arguments
16+
#
17+
if len(sys.argv) != 5:
18+
print(
19+
'usage: %s <sysdig-v1-url> <sysdig-v1-token> <sysdig-v2-url> <sysdig-v2-token>'
20+
% sys.argv[0])
21+
print(
22+
'You can find your token at https://app.sysdigcloud.com/#/settings/user'
23+
)
24+
sys.exit(1)
25+
26+
sdc_v1_url = sys.argv[1]
27+
sdc_v1_token = sys.argv[2]
28+
sdc_v2_url = sys.argv[3]
29+
sdc_v2_token = sys.argv[4]
30+
31+
#
32+
# Instantiate the SDC client
33+
#
34+
sdclient_v2 = SdMonitorClient(sdc_v2_token, sdc_url=sdc_v2_url)
35+
sdclient_v1 = SdMonitorClientV1(sdc_v1_token, sdc_url=sdc_v1_url)
36+
37+
#
38+
# Serialize the first user dashboard to disk
39+
#
40+
ok, res = sdclient_v1.get_dashboards()
41+
42+
if not ok:
43+
print(res)
44+
sys.exit(1)
45+
46+
for dashboard in res['dashboards']:
47+
file_name = '{}.json'.format(dashboard['id'])
48+
print('Saving v1 dashboard {} to file {}...'.format(
49+
dashboard['name'], file_name))
50+
sdclient_v1.save_dashboard_to_file(dashboard, file_name)
51+
52+
print('Importing dashboard to v2...')
53+
ok, res = sdclient_v2.create_dashboard_from_file(
54+
u'import of {}'.format(dashboard['name']),
55+
file_name,
56+
None,
57+
shared=dashboard['isShared'],
58+
public=dashboard['isPublic'])
59+
60+
if ok:
61+
print('Dashboard {} imported!'.format(dashboard['name']))
62+
sdclient_v2.delete_dashboard(res['dashboard'])
63+
else:
64+
print('Dashboard {} import failed:'.format(dashboard['name']))
65+
print(res)
66+
67+
print('\n')

sdcclient/_common.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,18 @@ def _checkResponse(self, res):
3838
return False
3939

4040
if 'errors' in j:
41-
if 'message' in j['errors'][0]:
42-
self.lasterr = j['errors'][0]['message']
41+
error_msgs = []
42+
for error in j['errors']:
43+
error_msg = []
44+
if 'message' in error:
45+
error_msg.append(error['message'])
4346

44-
if 'reason' in j['errors'][0]:
45-
if self.lasterr is not None:
46-
self.lasterr += ' '
47-
else:
48-
self.lasrerr = ''
47+
if 'reason' in error:
48+
error_msg.append(error['reason'])
4949

50-
self.lasterr += j['errors'][0]['reason']
50+
error_msgs.append(': '.join(error_msg))
51+
52+
self.lasterr = '\n'.join(error_msgs)
5153
elif 'message' in j:
5254
self.lasterr = j['message']
5355
else:

sdcclient/_monitor.py

Lines changed: 266 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,10 @@ def create_dashboard_from_template(self, dashboard_name, template, scope, shared
576576
scopeExpression = self.convert_scope_string_to_expression(scope)
577577
if scopeExpression[0] == False:
578578
return scopeExpression
579-
template['scopeExpressionList'] = map(lambda ex: {'operand':ex['operand'], 'operator':ex['operator'],'value':ex['value'],'displayName':'', 'variable':False}, scopeExpression[1])
579+
if scopeExpression[1]:
580+
template['scopeExpressionList'] = map(lambda ex: {'operand': ex['operand'], 'operator': ex['operator'], 'value': ex['value'], 'displayName': '', 'variable': False}, scopeExpression[1])
581+
else:
582+
template['scopeExpressionList'] = None
580583

581584
# NOTE: Individual panels might override the dashboard scope, the override will NOT be reset
582585
if 'widgets' in template and template['widgets'] is not None:
@@ -596,6 +599,7 @@ def create_dashboard_from_template(self, dashboard_name, template, scope, shared
596599
# Create the new dashboard
597600
#
598601
res = requests.post(self.url + self._dashboards_api_endpoint, headers=self.hdrs, data=json.dumps({'dashboard': template}), verify=self.ssl_verify)
602+
599603
return self._request_result(res)
600604

601605
def create_dashboard_from_view(self, newdashname, viewname, filter, shared=False, public=False):
@@ -718,6 +722,15 @@ def create_dashboard_from_file(self, dashboard_name, filename, filter, shared=Fa
718722

719723
dashboard = loaded_object['dashboard']
720724

725+
if loaded_object['version'] != self._dashboards_api_version:
726+
#
727+
# Convert the dashboard (if possible)
728+
#
729+
conversion_result, dashboard = self._convert_dashboard_to_current_version(dashboard, loaded_object['version'])
730+
731+
if conversion_result == False:
732+
return conversion_result, dashboard
733+
721734
#
722735
# Create the new dashboard
723736
#
@@ -838,6 +851,258 @@ def convert_scope_string_to_expression(scope):
838851

839852
return [True, expressions]
840853

854+
def _get_dashboard_converters(self):
855+
return {
856+
'v2': {
857+
'v1': self._convert_dashboard_v1_to_v2
858+
}
859+
}
860+
861+
def _convert_dashboard_to_current_version(self, dashboard, version):
862+
converters_to = self._get_dashboard_converters().get(self._dashboards_api_version, None)
863+
if converters_to == None:
864+
return False, 'unexpected error: no dashboard converters from version {} are supported'.format(self._dashboards_api_version)
865+
866+
converter = converters_to.get(version, None)
867+
868+
if converter == None:
869+
return False, 'dashboard version {} cannot be converted to {}'.format(version, self._dashboards_api_version)
870+
871+
try:
872+
return converter(dashboard)
873+
except Exception as err:
874+
return False, str(err)
875+
876+
def _convert_dashboard_v1_to_v2(self, dashboard):
877+
#
878+
# Migrations
879+
#
880+
# Each converter function will take:
881+
# 1. name of the v1 dashboard property
882+
# 2. v1 dashboard configuration
883+
# 3. v2 dashboard configuration
884+
#
885+
# Each converter will apply changes to v2 dashboard configuration according to v1
886+
#
887+
def when_set(converter):
888+
def fn(prop_name, old_obj, new_obj):
889+
if prop_name in old_obj and old_obj[prop_name] is not None:
890+
converter(prop_name, old_obj, new_obj)
891+
892+
return fn
893+
894+
def with_default(converter, default=None):
895+
def fn(prop_name, old_obj, new_obj):
896+
if prop_name not in old_obj:
897+
old_obj[prop_name] = default
898+
899+
converter(prop_name, old_obj, new_obj)
900+
901+
return fn
902+
903+
def keep_as_is(prop_name, old_obj, new_obj):
904+
new_obj[prop_name] = old_obj[prop_name]
905+
906+
def drop_it(prop_name = None, old_obj = None, new_obj = None):
907+
pass
908+
909+
def ignore(prop_name = None, old_obj = None, new_obj = None):
910+
pass
911+
912+
def rename_to(new_prop_name):
913+
def rename(prop_name, old_obj, new_obj):
914+
new_obj[new_prop_name] = old_obj[prop_name]
915+
916+
return rename
917+
918+
def convert_schema(prop_name, old_dashboard, new_dashboard):
919+
new_dashboard[prop_name] = 2
920+
921+
def convert_scope(prop_name, old_dashboard, new_dashboard):
922+
# # TODO!
923+
924+
scope = old_dashboard[prop_name]
925+
scope_conversion = self.convert_scope_string_to_expression(scope)
926+
927+
if scope_conversion[0]:
928+
if scope_conversion[1]:
929+
new_dashboard['scopeExpressionList'] = scope_conversion[1]
930+
else:
931+
# the property can be either `null` or a non-empty array
932+
new_dashboard['scopeExpressionList'] = None
933+
else:
934+
raise SyntaxError('scope not supported by the current grammar')
935+
936+
def convert_events_filter(prop_name, old_dashboard, new_dashboard):
937+
rename_to('eventsOverlaySettings')(prop_name, old_dashboard, new_dashboard)
938+
939+
if 'showNotificationsDoNotFilterSameMetrics' in new_dashboard['eventsOverlaySettings']:
940+
del new_dashboard['eventsOverlaySettings']['showNotificationsDoNotFilterSameMetrics']
941+
if 'showNotificationsDoNotFilterSameScope' in new_dashboard['eventsOverlaySettings']:
942+
del new_dashboard['eventsOverlaySettings']['showNotificationsDoNotFilterSameScope']
943+
944+
def convert_items(prop_name, old_dashboard, new_dashboard):
945+
def convert_color_coding(prop_name, old_widget, new_widget):
946+
best_value = None
947+
worst_value= None
948+
for item in old_widget[prop_name]['thresholds']:
949+
if item['color'] == 'best':
950+
best_value = item['max'] if not item['max'] else item['min']
951+
elif item['color'] == 'worst':
952+
worst_value = item['min'] if not item['min'] else item['max']
953+
954+
if best_value is not None and worst_value is not None:
955+
new_widget[prop_name] = {
956+
'best': best_value,
957+
'worst': worst_value
958+
}
959+
960+
def convert_display_options(prop_name, old_widget, new_widget):
961+
keep_as_is(prop_name, old_widget, new_widget)
962+
963+
if 'yAxisScaleFactor' in new_widget[prop_name]:
964+
del new_widget[prop_name]['yAxisScaleFactor']
965+
966+
def convert_group(prop_name, old_widget, new_widget):
967+
group_by_metrics = old_widget[prop_name]['configuration']['groups'][0]['groupBy']
968+
969+
migrated = []
970+
for metric in group_by_metrics:
971+
migrated.append({ 'labelId': metric['metric'] })
972+
973+
new_widget['groupingLabelsIds'] = migrated
974+
975+
def convert_override_filter(prop_name, old_widget, new_widget):
976+
if old_widget['showAs'] == 'map':
977+
# override scope always true if scope is set
978+
new_widget['overrideScope'] = True
979+
else:
980+
new_widget['overrideScope'] = old_widget[prop_name]
981+
982+
983+
def convert_name(prop_name, old_widget, new_widget):
984+
#
985+
# enforce unique name (on old dashboard, before migration)
986+
#
987+
unique_id = 1
988+
name = old_widget[prop_name]
989+
990+
for widget in old_dashboard['items']:
991+
if widget == old_widget:
992+
break
993+
994+
if old_widget[prop_name] == widget[prop_name]:
995+
old_widget[prop_name] = '{} ({})'.format(name, unique_id)
996+
unique_id += 1
997+
998+
keep_as_is(prop_name, old_widget, new_widget)
999+
1000+
def convert_metrics(prop_name, old_widget, new_widget):
1001+
def convert_property_name(prop_name, old_metric, new_metric):
1002+
keep_as_is(prop_name, old_metric, new_metric)
1003+
1004+
if old_metric['metricId'] == 'timestamp':
1005+
return 'k0'
1006+
1007+
metric_migrations = {
1008+
'metricId': rename_to('id'),
1009+
'aggregation': rename_to('timeAggregation'),
1010+
'groupAggregation': rename_to('groupAggregation'),
1011+
'propertyName': convert_property_name
1012+
}
1013+
1014+
migrated_metrics = []
1015+
for old_metric in old_widget[prop_name]:
1016+
migrated_metric = {}
1017+
1018+
for key in metric_migrations.keys():
1019+
if key in old_metric:
1020+
metric_migrations[key](key, old_metric, migrated_metric)
1021+
1022+
migrated_metrics.append(migrated_metric)
1023+
1024+
# Property name convention:
1025+
# timestamp: k0 (if present)
1026+
# other keys: k* (from 0 or 1, depending on timestamp)
1027+
# values: v* (from 0)
1028+
timestamp_key = filter(lambda m: m['id'] == 'timestamp' and not ('timeAggregation' in m and m['timeAggregation'] is not None), migrated_metrics)
1029+
no_timestamp_keys = filter(lambda m: m['id'] != 'timestamp' and not ('timeAggregation' in m and m['timeAggregation'] is not None), migrated_metrics)
1030+
values = filter(lambda m: 'timeAggregation' in m and m['timeAggregation'] is not None, migrated_metrics)
1031+
if timestamp_key:
1032+
timestamp_key[0]['propertyName'] = 'k0'
1033+
k_offset = 1 if timestamp_key else 0
1034+
for i in range(0, len(no_timestamp_keys)):
1035+
no_timestamp_keys[i]['propertyName'] = 'k{}'.format(i + k_offset)
1036+
for i in range(0, len(values)):
1037+
values[i]['propertyName'] = 'v{}'.format(i)
1038+
1039+
new_widget['metrics'] = migrated_metrics
1040+
1041+
widget_migrations = {
1042+
'colorCoding': when_set(convert_color_coding),
1043+
'compareToConfig': when_set(keep_as_is),
1044+
'customDisplayOptions': with_default(convert_display_options, {}),
1045+
'gridConfiguration': keep_as_is,
1046+
'group': when_set(convert_group),
1047+
'hasTransparentBackground': when_set(rename_to('transparentBackground')),
1048+
'limitToScope': when_set(keep_as_is),
1049+
'isPanelTitleVisible': when_set(rename_to('panelTitleVisible')),
1050+
'markdownSource': when_set(keep_as_is),
1051+
'metrics': with_default(convert_metrics, []),
1052+
'name': with_default(convert_name, 'Panel'),
1053+
'overrideFilter': convert_override_filter,
1054+
'paging': drop_it,
1055+
'scope': with_default(keep_as_is, None),
1056+
'showAs': keep_as_is,
1057+
'showAsType': drop_it,
1058+
'sorting': drop_it,
1059+
'textpanelTooltip': when_set(keep_as_is),
1060+
}
1061+
1062+
migrated_widgets = []
1063+
for old_widget in old_dashboard[prop_name]:
1064+
migrated_widget = {}
1065+
1066+
for key in widget_migrations.keys():
1067+
widget_migrations[key](key, old_widget, migrated_widget)
1068+
1069+
migrated_widgets.append(migrated_widget)
1070+
1071+
new_dashboard['widgets'] = migrated_widgets
1072+
1073+
return migrated
1074+
1075+
migrations = {
1076+
'autoCreated': keep_as_is,
1077+
'createdOn': keep_as_is,
1078+
'eventsFilter': with_default(convert_events_filter, {
1079+
'filterNotificationsUserInputFilter': ''
1080+
}),
1081+
'filterExpression': convert_scope,
1082+
'scopeExpressionList': ignore, # scope will be generated from 'filterExpression'
1083+
'id': keep_as_is,
1084+
'isPublic': rename_to('public'),
1085+
'isShared': rename_to('shared'),
1086+
'items': convert_items,
1087+
'layout': drop_it,
1088+
'modifiedOn': keep_as_is,
1089+
'name': keep_as_is,
1090+
'publicToken': drop_it,
1091+
'schema': convert_schema,
1092+
'teamId': keep_as_is,
1093+
'username': keep_as_is,
1094+
'version': keep_as_is,
1095+
}
1096+
1097+
#
1098+
# Apply migrations
1099+
#
1100+
migrated = {}
1101+
for key in migrations.keys():
1102+
migrations[key](key, copy.deepcopy(dashboard), migrated)
1103+
1104+
return True, migrated
1105+
8411106

8421107
# For backwards compatibility
8431108
SdcClient = SdMonitorClient

0 commit comments

Comments
 (0)