diff --git a/examples/create_dashboard.py b/examples/create_dashboard.py index 7c65ec9a..097e472d 100755 --- a/examples/create_dashboard.py +++ b/examples/create_dashboard.py @@ -10,7 +10,7 @@ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), '..')) -from sdcclient import SdcClient +from sdcclient import SdMonitorClient # @@ -42,7 +42,7 @@ def usage(): # # Instantiate the SDC client # -sdclient = SdcClient(sdc_token) +sdclient = SdMonitorClient(sdc_token) # # Create the new dashboard, applying to cassandra in production @@ -55,7 +55,7 @@ def usage(): # in Sysdig Cloud Explore page. # You can also refer to AWS tags by using "cloudProvider.tag.*" metadata or # agent tags by using "agent.tag.*" metadata -dashboardFilter = "kubernetes.namespace.name = prod" +dashboardFilter = 'proc.name = "cassandra"' print('Creating dashboard from view') ok, res = sdclient.create_dashboard_from_view(dashboardName, viewName, dashboardFilter) # @@ -75,7 +75,7 @@ def usage(): # Name of the dashboard to copy dashboardCopy = "Copy of {}".format(dashboardName) # Filter to apply to the new dashboard. Same as above. -dashboardFilter = "kubernetes.namespace.name != prod" +dashboardFilter = 'proc.name != "cassandra"' print('Creating dashboard from dashboard') ok, res = sdclient.create_dashboard_from_dashboard(dashboardCopy, dashboardName, dashboardFilter) diff --git a/examples/dashboard.py b/examples/dashboard.py index 79c5eb08..79adc255 100755 --- a/examples/dashboard.py +++ b/examples/dashboard.py @@ -8,7 +8,7 @@ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), '..')) -from sdcclient import SdcClient +from sdcclient import SdMonitorClient # @@ -39,7 +39,7 @@ def usage(): # # Instantiate the SDC client # -sdclient = SdcClient(sdc_token) +sdclient = SdMonitorClient(sdc_token) # @@ -77,10 +77,10 @@ def usage(): panel_name = 'CPU Over Time' panel_type = 'timeSeries' metrics = [ - {'id': 'kubernetes.pod.name'}, + {'id': 'proc.name'}, {'id': 'cpu.used.percent', 'aggregations': {'time': 'avg', 'group': 'avg'}} ] -scope = 'kubernetes.namespace.name = "dev" and kubernetes.replicationController.name = "cassandra"' +scope = 'proc.name = "cassandra"' ok, res = sdclient.add_dashboard_panel(dashboard_configuration, panel_name, panel_type, metrics, scope=scope) # Check the result @@ -101,9 +101,9 @@ def usage(): {'id': 'host.hostName'}, {'id': 'cpu.used.percent', 'aggregations': {'time': 'avg', 'group': 'avg'}} ] -sort_by = {'metric': 'cpu.used.percent', 'mode': 'desc'} +sort_direction = 'desc' limit = 10 -ok, res = sdclient.add_dashboard_panel(dashboard_configuration, panel_name, panel_type, metrics, sort_by=sort_by, limit=limit) +ok, res = sdclient.add_dashboard_panel(dashboard_configuration, panel_name, panel_type, metrics, sort_direction=sort_direction, limit=limit) # Check the result if ok: @@ -137,7 +137,7 @@ def usage(): # # Remove a panel # -ok, res = sdclient.remove_dashboard_panel(dashboard_configuration, 'CPU') +ok, res = sdclient.remove_dashboard_panel(dashboard_configuration, 'CPU Over Time') # Check the result if ok: diff --git a/examples/dashboard_backup_v1_restore_v2.py b/examples/dashboard_backup_v1_restore_v2.py new file mode 100644 index 00000000..6bb3e6b3 --- /dev/null +++ b/examples/dashboard_backup_v1_restore_v2.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# +# Save the first user dashboard to file and then use create_dashboard_from_file() +# to apply the stored dasboard again with a different filter. +# +import os +import sys +import json +sys.path.insert( + 0, os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), '..')) +from sdcclient import SdMonitorClient +from sdcclient import SdMonitorClientV1 + +# +# Parse arguments +# +if len(sys.argv) != 5: + print( + 'usage: %s ' + % sys.argv[0]) + print( + 'You can find your token at https://app.sysdigcloud.com/#/settings/user' + ) + sys.exit(1) + +sdc_v1_url = sys.argv[1] +sdc_v1_token = sys.argv[2] +sdc_v2_url = sys.argv[3] +sdc_v2_token = sys.argv[4] + +# +# Instantiate the SDC client +# +sdclient_v2 = SdMonitorClient(sdc_v2_token, sdc_url=sdc_v2_url) +sdclient_v1 = SdMonitorClientV1(sdc_v1_token, sdc_url=sdc_v1_url) + +# +# Serialize the first user dashboard to disk +# +ok, res = sdclient_v1.get_dashboards() + +if not ok: + print(res) + sys.exit(1) + +for dashboard in res['dashboards']: + file_name = '{}.json'.format(dashboard['id']) + print('Saving v1 dashboard {} to file {}...'.format( + dashboard['name'], file_name)) + sdclient_v1.save_dashboard_to_file(dashboard, file_name) + + print('Importing dashboard to v2...') + ok, res = sdclient_v2.create_dashboard_from_file( + u'import of {}'.format(dashboard['name']), + file_name, + None, + shared=dashboard['isShared'], + public=dashboard['isPublic']) + + if ok: + print('Dashboard {} imported!'.format(dashboard['name'])) + sdclient_v2.delete_dashboard(res['dashboard']) + else: + print('Dashboard {} import failed:'.format(dashboard['name'])) + print(res) + + print('\n') diff --git a/examples/dashboard_save_load.py b/examples/dashboard_save_load.py index b37c1f37..2b39ee61 100644 --- a/examples/dashboard_save_load.py +++ b/examples/dashboard_save_load.py @@ -7,7 +7,7 @@ import sys import json sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), '..')) -from sdcclient import SdcClient +from sdcclient import SdMonitorClient # # Parse arguments @@ -22,7 +22,7 @@ # # Instantiate the SDC client # -sdclient = SdcClient(sdc_token) +sdclient = SdMonitorClient(sdc_token) # # Serialize the first user dashboard to disk @@ -34,8 +34,7 @@ sys.exit(1) if len(res[u'dashboards']) > 0: - with open('dashboard.json', 'w') as outf: - json.dump(res[u'dashboards'][0], outf) + sdclient.save_dashboard_to_file(res[u'dashboards'][0], 'dashboard.json') else: print('the user has no dashboards. Exiting.') sys.exit(0) @@ -44,7 +43,7 @@ # Now create the dashboard from the file. We use a filter for the Cassandra process # as an example. # -dashboardFilter = "proc.name = cassandra" +dashboardFilter = 'proc.name = "cassandra"' ok, res = sdclient.create_dashboard_from_file('test dasboard from file', 'dashboard.json', dashboardFilter) diff --git a/examples/delete_dashboard.py b/examples/delete_dashboard.py index 6347f280..a5bbbedd 100755 --- a/examples/delete_dashboard.py +++ b/examples/delete_dashboard.py @@ -7,7 +7,7 @@ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), '..')) -from sdcclient import SdcClient +from sdcclient import SdMonitorClient # @@ -38,7 +38,7 @@ def usage(): # # Instantiate the SDC client # -sdclient = SdcClient(sdc_token) +sdclient = SdMonitorClient(sdc_token) # # List the dashboards diff --git a/examples/download_dashboards.py b/examples/download_dashboards.py index 1fa23c19..a205320b 100644 --- a/examples/download_dashboards.py +++ b/examples/download_dashboards.py @@ -8,7 +8,7 @@ import zipfile import json sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), '..')) -from sdcclient import SdcClient +from sdcclient import SdMonitorClient def zipdir(path, ziph): @@ -52,7 +52,7 @@ def cleanup_dir(path): # # Instantiate the SDC client # -sdclient = SdcClient(sdc_token, sdc_url='https://app.sysdigcloud.com') +sdclient = SdMonitorClient(sdc_token) # # Fire the request. @@ -76,11 +76,9 @@ def cleanup_dir(path): for db in res['dashboards']: - file_path = os.path.join(sysdig_dashboard_dir, str(db['id'])) - f = open(file_path, 'w') - f.write(json.dumps(db)) - print("Name: %s, # Charts: %d" % (db['name'], len(db['items']))) - f.close() + sdclient.save_dashboard_to_file(db, os.path.join(sysdig_dashboard_dir, str(db['id']))) + + print("Name: %s, # Charts: %d" % (db['name'], len(db['widgets']))) zipf = zipfile.ZipFile(dashboard_state_file, 'w', zipfile.ZIP_DEFLATED) zipdir(sysdig_dashboard_dir, zipf) diff --git a/examples/list_dashboards.py b/examples/list_dashboards.py index c9c3a65b..b958b538 100755 --- a/examples/list_dashboards.py +++ b/examples/list_dashboards.py @@ -6,7 +6,7 @@ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), '..')) -from sdcclient import SdcClient +from sdcclient import SdMonitorClient # # Parse arguments @@ -21,7 +21,7 @@ # # Instantiate the SDC client # -sdclient = SdcClient(sdc_token) +sdclient = SdMonitorClient(sdc_token) # # Fire the request. @@ -36,4 +36,4 @@ sys.exit(1) for db in res['dashboards']: - print("Name: %s, # Charts: %d" % (db['name'], len(db['items'] if 'items' in db else []))) + print("Name: %s, # Charts: %d" % (db['name'], len(db['widgets'] if 'widgets' in db else []))) diff --git a/examples/restore_dashboards.py b/examples/restore_dashboards.py index 4d201487..8ff55c22 100644 --- a/examples/restore_dashboards.py +++ b/examples/restore_dashboards.py @@ -8,7 +8,7 @@ import zipfile import json sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), '..')) -from sdcclient import SdcClient +from sdcclient import SdMonitorClient # # Parse arguments @@ -24,32 +24,25 @@ # # Instantiate the SDC client # -sdclient = SdcClient(sdc_token) +sdclient = SdMonitorClient(sdc_token) zipf = zipfile.ZipFile(dashboard_state_file, 'r') - -dashboard_conf_items = ['showAsType', 'filterRoot', 'linkMetrics', - 'singleTimeNavigation', 'gridConfiguration', 'responsive', - 'nodesNoiseFilter', 'compareWith', 'format', 'linksNoiseFilter', - 'filterProcesses', 'isLegendExpanded', 'inhertitTimeNavigation', - 'schema', 'sortAscending', 'mapDataLimit', 'metrics', 'filterExtNodes', - 'sorting', 'name', 'sourceExploreView', 'items', 'showAs', 'eventsFilter', - 'timeMode', 'isShared', 'sourceDrilldownView', 'filterExpression'] - for info in zipf.infolist(): data = zipf.read(info.filename) try: j = json.loads(data) except ValueError: - print('Non-JSON item found in ZIP: ' + info.filename + ' (skipping)') + print('Invalid JSON file found in ZIP file ' + info.filename + ': skipping') continue - k = {} - for item in j.keys(): - if item in dashboard_conf_items: - k[item] = j[item] - ok, res = sdclient.create_dashboard_with_configuration(k) + # + # Handle old files + # + if 'dashboard' in j: + j = j['dashboard'] + + ok, res = sdclient.create_dashboard_with_configuration(j) if ok: print('Restored Dashboard named: ' + j['name']) else: diff --git a/sdcclient/__init__.py b/sdcclient/__init__.py index 2c176ac9..9081724c 100644 --- a/sdcclient/__init__.py +++ b/sdcclient/__init__.py @@ -1,4 +1,5 @@ from sdcclient._monitor import SdcClient from sdcclient._monitor import SdMonitorClient +from sdcclient._monitor_v1 import SdMonitorClientV1 from sdcclient._secure import SdSecureClient from sdcclient._scanning import SdScanningClient diff --git a/sdcclient/_common.py b/sdcclient/_common.py index a69b2830..af9f7f62 100644 --- a/sdcclient/_common.py +++ b/sdcclient/_common.py @@ -38,16 +38,18 @@ def _checkResponse(self, res): return False if 'errors' in j: - if 'message' in j['errors'][0]: - self.lasterr = j['errors'][0]['message'] + error_msgs = [] + for error in j['errors']: + error_msg = [] + if 'message' in error: + error_msg.append(error['message']) - if 'reason' in j['errors'][0]: - if self.lasterr is not None: - self.lasterr += ' ' - else: - self.lasrerr = '' + if 'reason' in error: + error_msg.append(error['reason']) - self.lasterr += j['errors'][0]['reason'] + error_msgs.append(': '.join(error_msg)) + + self.lasterr = '\n'.join(error_msgs) elif 'message' in j: self.lasterr = j['message'] else: diff --git a/sdcclient/_monitor.py b/sdcclient/_monitor.py index 0c21c7a3..9de57a3c 100644 --- a/sdcclient/_monitor.py +++ b/sdcclient/_monitor.py @@ -16,6 +16,9 @@ class SdMonitorClient(_SdcCommon): def __init__(self, token="", sdc_url='https://app.sysdigcloud.com', ssl_verify=True): super(SdMonitorClient, self).__init__(token, sdc_url, ssl_verify) self.product = "SDC" + self._dashboards_api_version = 'v2' + self._dashboards_api_endpoint = '/api/{}/dashboards'.format(self._dashboards_api_version) + self._default_dashboards_api_endpoint = '/api/{}/defaultDashboards'.format(self._dashboards_api_version) def get_alerts(self): '''**Description** @@ -263,7 +266,7 @@ def set_explore_grouping_hierarchy(self, new_hierarchy): return [True, None] def get_views_list(self): - res = requests.get(self.url + '/api/defaultDashboards', headers=self.hdrs, + res = requests.get(self.url + self._default_dashboards_api_endpoint, headers=self.hdrs, verify=self.ssl_verify) if not self._checkResponse(res): return [False, self.lasterr] @@ -286,7 +289,7 @@ def get_view(self, name): if not id: return [False, 'view ' + name + ' not found'] - res = requests.get(self.url + '/api/defaultDashboards/' + id, headers=self.hdrs, + res = requests.get(self.url + self._default_dashboards_api_endpoint + '/' + id, headers=self.hdrs, verify=self.ssl_verify) return self._request_result(res) @@ -300,7 +303,7 @@ def get_dashboards(self): **Example** `examples/list_dashboards.py `_ ''' - res = requests.get(self.url + '/ui/dashboards', headers=self.hdrs, verify=self.ssl_verify) + res = requests.get(self.url + self._dashboards_api_endpoint, headers=self.hdrs, verify=self.ssl_verify) return self._request_result(res) def find_dashboard_by(self, name=None): @@ -330,7 +333,14 @@ def create_item(configuration): return [True, dashboards] def create_dashboard_with_configuration(self, configuration): - res = requests.post(self.url + '/ui/dashboards', headers=self.hdrs, data=json.dumps({'dashboard': configuration}), + # Remove id and version properties if already set + configuration_clone = copy.deepcopy(configuration) + if 'id' in configuration_clone: + del configuration_clone['id'] + if 'version' in configuration_clone: + del configuration_clone['version'] + + res = requests.post(self.url + self._dashboards_api_endpoint, headers=self.hdrs, data=json.dumps({'dashboard': configuration_clone}), verify=self.ssl_verify) return self._request_result(res) @@ -350,18 +360,18 @@ def create_dashboard(self, name): ''' dashboard_configuration = { 'name': name, - 'schema': 1, - 'items': [] + 'schema': 2, + 'widgets': [] } # # Create the new dashboard # - res = requests.post(self.url + '/ui/dashboards', headers=self.hdrs, data=json.dumps({'dashboard': dashboard_configuration}), + res = requests.post(self.url + self._dashboards_api_endpoint, headers=self.hdrs, data=json.dumps({'dashboard': dashboard_configuration}), verify=self.ssl_verify) return self._request_result(res) - def add_dashboard_panel(self, dashboard, name, panel_type, metrics, scope=None, sort_by=None, limit=None, layout=None): + def add_dashboard_panel(self, dashboard, name, panel_type, metrics, scope=None, sort_direction='desc', limit=None, layout=None): """**Description** Adds a panel to the dashboard. A panel can be a time series, or a top chart (i.e. bar chart), or a number panel. @@ -374,7 +384,7 @@ def add_dashboard_panel(self, dashboard, name, panel_type, metrics, scope=None, - ``top``: 1 or more metrics OR 1 metric + 1 grouping key - ``number``: 1 metric only - **scope**: filter to apply to the panel; must be based on metadata available in Sysdig Monitor; Example: *kubernetes.namespace.name='production' and container.image='nginx'*. - - **sort_by**: Data sorting; The parameter is optional and it's a dictionary of ``metric`` and ``mode`` (it can be ``desc`` or ``asc``) + - **sort_direction**: Data sorting; The parameter is optional and it's a string identifying the sorting direction (it can be ``desc`` or ``asc``) - **limit**: This parameter sets the limit on the number of lines/bars shown in a ``timeSeries`` or ``top`` panel. In the case of more entities being available than the limit, the top entities according to the sort will be shown. The default value is 10 for ``top`` panels (for ``timeSeries`` the default is defined by Sysdig Monitor itself). Note that increasing the limit above 10 is not officially supported and may cause performance and rendering issues - **layout**: Size and position of the panel. The dashboard layout is defined by a grid of 12 columns, each row height is equal to the column height. For example, say you want to show 2 panels at the top: one panel might be 6 x 3 (half the width, 3 rows height) located in row 1 and column 1 (top-left corner of the viewport), the second panel might be 6 x 3 located in row 1 and position 7. The location is specified by a dictionary of ``row`` (row position), ``col`` (column position), ``size_x`` (width), ``size_y`` (height). @@ -387,7 +397,6 @@ def add_dashboard_panel(self, dashboard, name, panel_type, metrics, scope=None, panel_configuration = { 'name': name, 'showAs': None, - 'showAsType': None, 'metrics': [], 'gridConfiguration': { 'col': 1, @@ -426,56 +435,60 @@ def add_dashboard_panel(self, dashboard, name, panel_type, metrics, scope=None, property_names[metric['id']] = property_name + str(i) panel_configuration['metrics'].append({ - 'metricId': metric['id'], - 'aggregation': metric['aggregations']['time'] if 'aggregations' in metric else None, + 'id': metric['id'], + 'timeAggregation': metric['aggregations']['time'] if 'aggregations' in metric else None, 'groupAggregation': metric['aggregations']['group'] if 'aggregations' in metric else None, 'propertyName': property_name + str(i) }) panel_configuration['scope'] = scope # if chart scope is equal to dashboard scope, set it as non override - panel_configuration['overrideFilter'] = ('scope' in dashboard and dashboard['scope'] != scope) or ('scope' not in dashboard and scope != None) - + panel_configuration['overrideScope'] = ('scope' in dashboard and dashboard['scope'] != scope) or ('scope' not in dashboard and scope != None) + + if 'custom_display_options' not in panel_configuration: + panel_configuration['custom_display_options'] = { + 'valueLimit': { + 'count': 10, + 'direction': 'desc' + }, + 'histogram': { + 'numberOfBuckets': 10 + }, + 'yAxisScale': 'linear', + 'yAxisLeftDomain': { + 'from': 0, + 'to': None + }, + 'yAxisRightDomain': { + 'from': 0, + 'to': None + }, + 'xAxis': { + 'from': 0, + 'to': None + } + } # # Configure panel type # if panel_type == 'timeSeries': panel_configuration['showAs'] = 'timeSeries' - panel_configuration['showAsType'] = 'line' if limit != None: - panel_configuration['paging'] = { - 'from': 0, - 'to': limit - 1 + panel_configuration['custom_display_options']['valueLimit'] = { + 'count': limit, + 'direction': 'desc' } elif panel_type == 'number': panel_configuration['showAs'] = 'summary' - panel_configuration['showAsType'] = 'summary' elif panel_type == 'top': panel_configuration['showAs'] = 'top' - panel_configuration['showAsType'] = 'bars' - if sort_by is None: - panel_configuration['sorting'] = [{ - 'id': 'v0', - 'mode': 'desc' - }] - else: - panel_configuration['sorting'] = [{ - 'id': property_names[sort_by['metric']], - 'mode': sort_by['mode'] - }] - - if limit is None: - panel_configuration['paging'] = { - 'from': 0, - 'to': 10 - } - else: - panel_configuration['paging'] = { - 'from': 0, - 'to': limit - 1 + if limit != None: + panel_configuration['custom_display_options']['valueLimit'] = { + 'count': limit, + 'direction': sort_direction } # @@ -488,17 +501,16 @@ def add_dashboard_panel(self, dashboard, name, panel_type, metrics, scope=None, # Clone existing dashboard... # dashboard_configuration = copy.deepcopy(dashboard) - dashboard_configuration['id'] = None # # ... and add the new panel # - dashboard_configuration['items'].append(panel_configuration) + dashboard_configuration['widgets'].append(panel_configuration) # # Update dashboard # - res = requests.put(self.url + '/ui/dashboards/' + str(dashboard['id']), headers=self.hdrs, data=json.dumps({'dashboard': dashboard_configuration}), + res = requests.put(self.url + self._dashboards_api_endpoint + '/' + str(dashboard['id']), headers=self.hdrs, data=json.dumps({'dashboard': dashboard_configuration}), verify=self.ssl_verify) return self._request_result(res) @@ -519,32 +531,32 @@ def remove_dashboard_panel(self, dashboard, panel_name): # Clone existing dashboard... # dashboard_configuration = copy.deepcopy(dashboard) - dashboard_configuration['id'] = None + del dashboard_configuration['id'] # # ... find the panel # def filter_fn(panel): return panel['name'] == panel_name - panels = list(filter(filter_fn, dashboard_configuration['items'])) + panels = list(filter(filter_fn, dashboard_configuration['widgets'])) if len(panels) > 0: # # ... and remove it # for panel in panels: - dashboard_configuration['items'].remove(panel) + dashboard_configuration['widgets'].remove(panel) # # Update dashboard # - res = requests.put(self.url + '/ui/dashboards/' + str(dashboard['id']), headers=self.hdrs, data=json.dumps({'dashboard': dashboard_configuration}), + res = requests.put(self.url + self._dashboards_api_endpoint + '/' + str(dashboard['id']), headers=self.hdrs, data=json.dumps({'dashboard': dashboard_configuration}), verify=self.ssl_verify) return self._request_result(res) else: return [False, 'Not found'] - def create_dashboard_from_template(self, dashboard_name, template, scope, shared=False, public=False, annotations={}): + def create_dashboard_from_template(self, dashboard_name, template, scope, shared=False, public=False): if scope is not None: if isinstance(scope, basestring) == False: return [False, 'Invalid scope format: Expected a string'] @@ -554,53 +566,54 @@ def create_dashboard_from_template(self, dashboard_name, template, scope, shared # template['id'] = None template['version'] = None - template['schema'] = 1 + template['schema'] = 2 template['name'] = dashboard_name - template['isShared'] = shared - template['isPublic'] = public + template['shared'] = shared + template['public'] = public template['publicToken'] = None + # default dashboards don't have eventsOverlaySettings property + # make sure to add the default set if the template doesn't include it + if 'eventsOverlaySettings' not in template or not template['eventsOverlaySettings']: + template['eventsOverlaySettings'] = { + 'filterNotificationsUserInputFilter': '' + } + # set dashboard scope to the specific parameter scopeExpression = self.convert_scope_string_to_expression(scope) if scopeExpression[0] == False: return scopeExpression - template['filterExpression'] = scope - template['scopeExpressionList'] = map(lambda ex: {'operand':ex['operand'], 'operator':ex['operator'],'value':ex['value'],'displayName':'', 'isVariable':False}, scopeExpression[1]) - - if 'widgets' in template and template['widgets'] is not None: - # Default dashboards (aka Explore views) specify panels with the property `widgets`, - # while custom dashboards use `items` - template['items'] = list(template['widgets']) - del template['widgets'] + if scopeExpression[1]: + template['scopeExpressionList'] = map(lambda ex: {'operand': ex['operand'], 'operator': ex['operator'], 'value': ex['value'], 'displayName': '', 'variable': False}, scopeExpression[1]) + else: + template['scopeExpressionList'] = None # NOTE: Individual panels might override the dashboard scope, the override will NOT be reset - if 'items' in template and template['items'] is not None: - for chart in template['items']: - if 'overrideFilter' not in chart: - chart['overrideFilter'] = False + if 'widgets' in template and template['widgets'] is not None: + for chart in template['widgets']: + if 'overrideScope' not in chart: + chart['overrideScope'] = False - if chart['overrideFilter'] == False: + if chart['overrideScope'] == False: # patch frontend bug to hide scope override warning even when it's not really overridden chart['scope'] = scope - # if chart scope is equal to dashboard scope, set it as non override - chart_scope = chart['scope'] if 'scope' in chart else None - chart['overrideFilter'] = chart_scope != scope - - if 'annotations' in template: - template['annotations'].update(annotations) - else: - template['annotations'] = annotations - - template['annotations']['createdByEngine'] = True + if chart['showAs'] != 'map': + # if chart scope is equal to dashboard scope, set it as non override + chart_scope = chart['scope'] if 'scope' in chart else None + chart['overrideScope'] = chart_scope != scope + else: + # topology panels must override the scope + chart['overrideScope'] = True # # Create the new dashboard # - res = requests.post(self.url + '/ui/dashboards', headers=self.hdrs, data=json.dumps({'dashboard': template}), verify=self.ssl_verify) + res = requests.post(self.url + self._dashboards_api_endpoint, headers=self.hdrs, data=json.dumps({'dashboard': template}), verify=self.ssl_verify) + return self._request_result(res) - def create_dashboard_from_view(self, newdashname, viewname, filter, shared=False, public=False, annotations={}): + def create_dashboard_from_view(self, newdashname, viewname, filter, shared=False, public=False): '''**Description** Create a new dasboard using one of the Sysdig Monitor views as a template. You will be able to define the scope of the new dashboard. @@ -610,7 +623,6 @@ def create_dashboard_from_view(self, newdashname, viewname, filter, shared=False - **filter**: a boolean expression combining Sysdig Monitor segmentation criteria that defines what the new dasboard will be applied to. For example: *kubernetes.namespace.name='production' and container.image='nginx'*. - **shared**: if set to True, the new dashboard will be a shared one. - **public**: if set to True, the new dashboard will be shared with public token. - - **annotations**: an optional dictionary of custom properties that you can associate to this dashboard for automation or management reasons **Success Return Value** A dictionary showing the details of the new dashboard. @@ -633,9 +645,9 @@ def create_dashboard_from_view(self, newdashname, viewname, filter, shared=False # # Create the new dashboard # - return self.create_dashboard_from_template(newdashname, view, filter, shared, public, annotations) + return self.create_dashboard_from_template(newdashname, view, filter, shared, public) - def create_dashboard_from_dashboard(self, newdashname, templatename, filter, shared=False, public=False, annotations={}): + def create_dashboard_from_dashboard(self, newdashname, templatename, filter, shared=False, public=False): '''**Description** Create a new dasboard using one of the existing dashboards as a template. You will be able to define the scope of the new dasboard. @@ -645,7 +657,6 @@ def create_dashboard_from_dashboard(self, newdashname, templatename, filter, sha - **filter**: a boolean expression combining Sysdig Monitor segmentation criteria defines what the new dasboard will be applied to. For example: *kubernetes.namespace.name='production' and container.image='nginx'*. - **shared**: if set to True, the new dashboard will be a shared one. - **public**: if set to True, the new dashboard will be shared with public token. - - **annotations**: an optional dictionary of custom properties that you can associate to this dashboard for automation or management reasons **Success Return Value** A dictionary showing the details of the new dashboard. @@ -656,7 +667,7 @@ def create_dashboard_from_dashboard(self, newdashname, templatename, filter, sha # # Get the list of dashboards from the server # - res = requests.get(self.url + '/ui/dashboards', headers=self.hdrs, verify=self.ssl_verify) + res = requests.get(self.url + self._dashboards_api_endpoint, headers=self.hdrs, verify=self.ssl_verify) if not self._checkResponse(res): return [False, self.lasterr] @@ -679,20 +690,25 @@ def create_dashboard_from_dashboard(self, newdashname, templatename, filter, sha # # Create the dashboard # - return self.create_dashboard_from_template(newdashname, dboard, filter, shared, public, annotations) + return self.create_dashboard_from_template(newdashname, dboard, filter, shared, public) - def create_dashboard_from_file(self, newdashname, filename, filter, shared=False, public=False, annotations={}): + def create_dashboard_from_file(self, dashboard_name, filename, filter, shared=False, public=False): ''' **Description** - Create a new dasboard using a dashboard template saved to disk. + Create a new dasboard using a dashboard template saved to disk. See :func:`~SdcClient.save_dashboard_to_file` to use the file to create a dashboard (usefl to create and restore backups). + + The file can contain the following JSON formats: + 1. dashboard object in the format of an array element returned by :func:`~SdcClient.get_dashboards` + 2. JSON object with the following properties: + * version: dashboards API version (e.g. 'v2') + * dashboard: dashboard object in the format of an array element returned by :func:`~SdcClient.get_dashboards` **Arguments** - - **newdashname**: the name of the dashboard that will be created. - - **filename**: name of a file containing a JSON object for a dashboard in the format of an array element returned by :func:`~SdcClient.get_dashboards` + - **dashboard_name**: the name of the dashboard that will be created. + - **filename**: name of a file containing a JSON object - **filter**: a boolean expression combining Sysdig Monitor segmentation criteria defines what the new dasboard will be applied to. For example: *kubernetes.namespace.name='production' and container.image='nginx'*. - **shared**: if set to True, the new dashboard will be a shared one. - **public**: if set to True, the new dashboard will be shared with public token. - - **annotations**: an optional dictionary of custom properties that you can associate to this dashboard for automation or management reasons **Success Return Value** A dictionary showing the details of the new dashboard. @@ -704,15 +720,54 @@ def create_dashboard_from_file(self, newdashname, filename, filter, shared=False # Load the Dashboard # with open(filename) as data_file: - dboard = json.load(data_file) + loaded_object = json.load(data_file) + + # + # Handle old files + # + if 'dashboard' not in loaded_object: + loaded_object = { + 'version': 'v1', + 'dashboard': loaded_object + } - dboard['timeMode'] = {'mode': 1} - dboard['time'] = {'last': 2 * 60 * 60 * 1000000, 'sampling': 2 * 60 * 60 * 1000000} + dashboard = loaded_object['dashboard'] + + if loaded_object['version'] != self._dashboards_api_version: + # + # Convert the dashboard (if possible) + # + conversion_result, dashboard = self._convert_dashboard_to_current_version(dashboard, loaded_object['version']) + + if conversion_result == False: + return conversion_result, dashboard # # Create the new dashboard # - return self.create_dashboard_from_template(newdashname, dboard, filter, shared, public, annotations) + return self.create_dashboard_from_template(dashboard_name, dashboard, filter, shared, public) + + def save_dashboard_to_file(self, dashboard, filename): + ''' + **Description** + Save a dashboard to disk. See :func:`~SdcClient.create_dashboard_from_file` to use the file to create a dashboard (usefl to create and restore backups). + + The file will contain a JSON object with the following properties: + * version: dashboards API version (e.g. 'v2') + * dashboard: dashboard object in the format of an array element returned by :func:`~SdcClient.get_dashboards` + + **Arguments** + - **dashboard**: dashboard object in the format of an array element returned by :func:`~SdcClient.get_dashboards` + - **filename**: name of a file that will contain a JSON object + + **Example** + `examples/dashboard_save_load.py `_ + ''' + with open(filename, 'w') as outf: + json.dump({ + 'version': self._dashboards_api_version, + 'dashboard': dashboard + }, outf) def delete_dashboard(self, dashboard): '''**Description** @@ -730,7 +785,7 @@ def delete_dashboard(self, dashboard): if 'id' not in dashboard: return [False, "Invalid dashboard format"] - res = requests.delete(self.url + '/ui/dashboards/' + str(dashboard['id']), headers=self.hdrs, verify=self.ssl_verify) + res = requests.delete(self.url + self._dashboards_api_endpoint + '/' + str(dashboard['id']), headers=self.hdrs, verify=self.ssl_verify) if not self._checkResponse(res): return [False, self.lasterr] @@ -807,6 +862,262 @@ def convert_scope_string_to_expression(scope): return [True, expressions] + def _get_dashboard_converters(self): + return { + 'v2': { + 'v1': self._convert_dashboard_v1_to_v2 + } + } + + def _convert_dashboard_to_current_version(self, dashboard, version): + converters_to = self._get_dashboard_converters().get(self._dashboards_api_version, None) + if converters_to == None: + return False, 'unexpected error: no dashboard converters from version {} are supported'.format(self._dashboards_api_version) + + converter = converters_to.get(version, None) + + if converter == None: + return False, 'dashboard version {} cannot be converted to {}'.format(version, self._dashboards_api_version) + + try: + return converter(dashboard) + except Exception as err: + return False, str(err) + + def _convert_dashboard_v1_to_v2(self, dashboard): + # + # Migrations + # + # Each converter function will take: + # 1. name of the v1 dashboard property + # 2. v1 dashboard configuration + # 3. v2 dashboard configuration + # + # Each converter will apply changes to v2 dashboard configuration according to v1 + # + def when_set(converter): + def fn(prop_name, old_obj, new_obj): + if prop_name in old_obj and old_obj[prop_name] is not None: + converter(prop_name, old_obj, new_obj) + + return fn + + def with_default(converter, default=None): + def fn(prop_name, old_obj, new_obj): + if prop_name not in old_obj: + old_obj[prop_name] = default + + converter(prop_name, old_obj, new_obj) + + return fn + + def keep_as_is(prop_name, old_obj, new_obj): + new_obj[prop_name] = old_obj[prop_name] + + def drop_it(prop_name = None, old_obj = None, new_obj = None): + pass + + def ignore(prop_name = None, old_obj = None, new_obj = None): + pass + + def rename_to(new_prop_name): + def rename(prop_name, old_obj, new_obj): + new_obj[new_prop_name] = old_obj[prop_name] + + return rename + + def convert_schema(prop_name, old_dashboard, new_dashboard): + new_dashboard[prop_name] = 2 + + def convert_scope(prop_name, old_dashboard, new_dashboard): + # # TODO! + + scope = old_dashboard[prop_name] + scope_conversion = self.convert_scope_string_to_expression(scope) + + if scope_conversion[0]: + if scope_conversion[1]: + new_dashboard['scopeExpressionList'] = scope_conversion[1] + else: + # the property can be either `null` or a non-empty array + new_dashboard['scopeExpressionList'] = None + else: + raise SyntaxError('scope not supported by the current grammar') + + def convert_events_filter(prop_name, old_dashboard, new_dashboard): + rename_to('eventsOverlaySettings')(prop_name, old_dashboard, new_dashboard) + + if 'showNotificationsDoNotFilterSameMetrics' in new_dashboard['eventsOverlaySettings']: + del new_dashboard['eventsOverlaySettings']['showNotificationsDoNotFilterSameMetrics'] + if 'showNotificationsDoNotFilterSameScope' in new_dashboard['eventsOverlaySettings']: + del new_dashboard['eventsOverlaySettings']['showNotificationsDoNotFilterSameScope'] + + def convert_items(prop_name, old_dashboard, new_dashboard): + def convert_color_coding(prop_name, old_widget, new_widget): + best_value = None + worst_value= None + for item in old_widget[prop_name]['thresholds']: + if item['color'] == 'best': + best_value = item['max'] if not item['max'] else item['min'] + elif item['color'] == 'worst': + worst_value = item['min'] if not item['min'] else item['max'] + + if best_value is not None and worst_value is not None: + new_widget[prop_name] = { + 'best': best_value, + 'worst': worst_value + } + + def convert_display_options(prop_name, old_widget, new_widget): + keep_as_is(prop_name, old_widget, new_widget) + + if 'yAxisScaleFactor' in new_widget[prop_name]: + del new_widget[prop_name]['yAxisScaleFactor'] + + def convert_group(prop_name, old_widget, new_widget): + group_by_metrics = old_widget[prop_name]['configuration']['groups'][0]['groupBy'] + + migrated = [] + for metric in group_by_metrics: + migrated.append({ 'id': metric['metric'] }) + + new_widget['groupingLabelIds'] = migrated + + def convert_override_filter(prop_name, old_widget, new_widget): + if old_widget['showAs'] == 'map': + # override scope always true if scope is set + new_widget['overrideScope'] = True + else: + new_widget['overrideScope'] = old_widget[prop_name] + + + def convert_name(prop_name, old_widget, new_widget): + # + # enforce unique name (on old dashboard, before migration) + # + unique_id = 1 + name = old_widget[prop_name] + + for widget in old_dashboard['items']: + if widget == old_widget: + break + + if old_widget[prop_name] == widget[prop_name]: + old_widget[prop_name] = '{} ({})'.format(name, unique_id) + unique_id += 1 + + keep_as_is(prop_name, old_widget, new_widget) + + def convert_metrics(prop_name, old_widget, new_widget): + def convert_property_name(prop_name, old_metric, new_metric): + keep_as_is(prop_name, old_metric, new_metric) + + if old_metric['metricId'] == 'timestamp': + return 'k0' + + metric_migrations = { + 'metricId': rename_to('id'), + 'aggregation': rename_to('timeAggregation'), + 'groupAggregation': rename_to('groupAggregation'), + 'propertyName': convert_property_name + } + + migrated_metrics = [] + for old_metric in old_widget[prop_name]: + migrated_metric = {} + + for key in metric_migrations.keys(): + if key in old_metric: + metric_migrations[key](key, old_metric, migrated_metric) + + migrated_metrics.append(migrated_metric) + + # Property name convention: + # timestamp: k0 (if present) + # other keys: k* (from 0 or 1, depending on timestamp) + # values: v* (from 0) + sorted_metrics = [] + timestamp_key = filter(lambda m: m['id'] == 'timestamp' and not ('timeAggregation' in m and m['timeAggregation'] is not None), migrated_metrics) + no_timestamp_keys = filter(lambda m: m['id'] != 'timestamp' and not ('timeAggregation' in m and m['timeAggregation'] is not None), migrated_metrics) + values = filter(lambda m: 'timeAggregation' in m and m['timeAggregation'] is not None, migrated_metrics) + if timestamp_key: + timestamp_key[0]['propertyName'] = 'k0' + sorted_metrics.append(timestamp_key[0]) + k_offset = 1 if timestamp_key else 0 + for i in range(0, len(no_timestamp_keys)): + no_timestamp_keys[i]['propertyName'] = 'k{}'.format(i + k_offset) + sorted_metrics.append(no_timestamp_keys[i]) + for i in range(0, len(values)): + values[i]['propertyName'] = 'v{}'.format(i) + sorted_metrics.append(values[i]) + + new_widget['metrics'] = sorted_metrics + + widget_migrations = { + 'colorCoding': when_set(convert_color_coding), + 'compareToConfig': when_set(keep_as_is), + 'customDisplayOptions': with_default(convert_display_options, {}), + 'gridConfiguration': keep_as_is, + 'group': when_set(convert_group), + 'hasTransparentBackground': when_set(rename_to('transparentBackground')), + 'limitToScope': when_set(keep_as_is), + 'isPanelTitleVisible': when_set(rename_to('panelTitleVisible')), + 'markdownSource': when_set(keep_as_is), + 'metrics': with_default(convert_metrics, []), + 'name': with_default(convert_name, 'Panel'), + 'overrideFilter': convert_override_filter, + 'paging': drop_it, + 'scope': with_default(keep_as_is, None), + 'showAs': keep_as_is, + 'showAsType': drop_it, + 'sorting': drop_it, + 'textpanelTooltip': when_set(keep_as_is), + } + + migrated_widgets = [] + for old_widget in old_dashboard[prop_name]: + migrated_widget = {} + + for key in widget_migrations.keys(): + widget_migrations[key](key, old_widget, migrated_widget) + + migrated_widgets.append(migrated_widget) + + new_dashboard['widgets'] = migrated_widgets + + return migrated + + migrations = { + 'autoCreated': keep_as_is, + 'createdOn': keep_as_is, + 'eventsFilter': with_default(convert_events_filter, { + 'filterNotificationsUserInputFilter': '' + }), + 'filterExpression': convert_scope, + 'scopeExpressionList': ignore, # scope will be generated from 'filterExpression' + 'id': keep_as_is, + 'isPublic': rename_to('public'), + 'isShared': rename_to('shared'), + 'items': convert_items, + 'layout': drop_it, + 'modifiedOn': keep_as_is, + 'name': keep_as_is, + 'publicToken': drop_it, + 'schema': convert_schema, + 'teamId': keep_as_is, + 'username': keep_as_is, + 'version': keep_as_is, + } + + # + # Apply migrations + # + migrated = {} + for key in migrations.keys(): + migrations[key](key, copy.deepcopy(dashboard), migrated) + + return True, migrated + # For backwards compatibility SdcClient = SdMonitorClient diff --git a/sdcclient/_monitor_v1.py b/sdcclient/_monitor_v1.py new file mode 100644 index 00000000..ebf5ec24 --- /dev/null +++ b/sdcclient/_monitor_v1.py @@ -0,0 +1,297 @@ +import json +import copy +import requests +import re + +from sdcclient._monitor import SdMonitorClient + +try: + basestring +except NameError: + basestring = str + + +class SdMonitorClientV1(SdMonitorClient): + '''**Description** + Handles dashboards version 1 (ie. up to February 2019). For later Sysdig Monitor versions, please use :class:`~SdMonitorClient` instead. + ''' + + def __init__(self, token="", sdc_url='https://app.sysdigcloud.com', ssl_verify=True): + super(SdMonitorClientV1, self).__init__(token, sdc_url, ssl_verify) + self._dashboards_api_version = 'v1' + self._dashboards_api_endpoint = '/ui/dashboards' + self._default_dashboards_api_endpoint = '/api/defaultDashboards' + + def create_dashboard_from_template(self, dashboard_name, template, scope, shared=False, public=False, annotations={}): + if scope is not None: + if isinstance(scope, basestring) == False: + return [False, 'Invalid scope format: Expected a string'] + + # + # Clean up the dashboard we retireved so it's ready to be pushed + # + template['id'] = None + template['version'] = None + template['schema'] = 1 + template['name'] = dashboard_name + template['isShared'] = shared + template['isPublic'] = public + template['publicToken'] = None + + # set dashboard scope to the specific parameter + scopeExpression = self.convert_scope_string_to_expression(scope) + if scopeExpression[0] == False: + return scopeExpression + template['filterExpression'] = scope + template['scopeExpressionList'] = map(lambda ex: {'operand':ex['operand'], 'operator':ex['operator'],'value':ex['value'],'displayName':'', 'isVariable':False}, scopeExpression[1]) + + if 'widgets' in template and template['widgets'] is not None: + # Default dashboards (aka Explore views) specify panels with the property `widgets`, + # while custom dashboards use `items` + template['items'] = list(template['widgets']) + del template['widgets'] + + # NOTE: Individual panels might override the dashboard scope, the override will NOT be reset + if 'items' in template and template['items'] is not None: + for chart in template['items']: + if 'overrideFilter' not in chart: + chart['overrideFilter'] = False + + if chart['overrideFilter'] == False: + # patch frontend bug to hide scope override warning even when it's not really overridden + chart['scope'] = scope + + # if chart scope is equal to dashboard scope, set it as non override + chart_scope = chart['scope'] if 'scope' in chart else None + chart['overrideFilter'] = chart_scope != scope + + if 'annotations' in template: + template['annotations'].update(annotations) + else: + template['annotations'] = annotations + + template['annotations']['createdByEngine'] = True + + # + # Create the new dashboard + # + res = requests.post(self.url + self._dashboards_api_endpoint, headers=self.hdrs, data=json.dumps({'dashboard': template}), verify=self.ssl_verify) + return self._request_result(res) + + def create_dashboard(self, name): + ''' + **Description** + Creates an empty dashboard. You can then add panels by using ``add_dashboard_panel``. + + **Arguments** + - **name**: the name of the dashboard that will be created. + + **Success Return Value** + A dictionary showing the details of the new dashboard. + + **Example** + `examples/dashboard.py `_ + ''' + dashboard_configuration = { + 'name': name, + 'schema': 2, + 'items': [] + } + + # + # Create the new dashboard + # + res = requests.post(self.url + self._dashboards_api_endpoint, headers=self.hdrs, data=json.dumps({'dashboard': dashboard_configuration}), + verify=self.ssl_verify) + return self._request_result(res) + + def add_dashboard_panel(self, dashboard, name, panel_type, metrics, scope=None, sort_by=None, limit=None, layout=None): + """**Description** + Adds a panel to the dashboard. A panel can be a time series, or a top chart (i.e. bar chart), or a number panel. + + **Arguments** + - **dashboard**: dashboard to edit + - **name**: name of the new panel + - **panel_type**: type of the new panel. Valid values are: ``timeSeries``, ``top``, ``number`` + - **metrics**: a list of dictionaries, specifying the metrics to show in the panel, and optionally, if there is only one metric, a grouping key to segment that metric by. A metric is any of the entries that can be found in the *Metrics* section of the Explore page in Sysdig Monitor. Metric entries require an *aggregations* section specifying how to aggregate the metric across time and groups of containers/hosts. A grouping key is any of the entries that can be found in the *Show* or *Segment By* sections of the Explore page in Sysdig Monitor. Refer to the examples section below for ready to use code snippets. Note, certain panels allow certain combinations of metrics and grouping keys: + - ``timeSeries``: 1 or more metrics OR 1 metric + 1 grouping key + - ``top``: 1 or more metrics OR 1 metric + 1 grouping key + - ``number``: 1 metric only + - **scope**: filter to apply to the panel; must be based on metadata available in Sysdig Monitor; Example: *kubernetes.namespace.name='production' and container.image='nginx'*. + - **sort_by**: Data sorting; The parameter is optional and it's a dictionary of ``metric`` and ``mode`` (it can be ``desc`` or ``asc``) + - **limit**: This parameter sets the limit on the number of lines/bars shown in a ``timeSeries`` or ``top`` panel. In the case of more entities being available than the limit, the top entities according to the sort will be shown. The default value is 10 for ``top`` panels (for ``timeSeries`` the default is defined by Sysdig Monitor itself). Note that increasing the limit above 10 is not officially supported and may cause performance and rendering issues + - **layout**: Size and position of the panel. The dashboard layout is defined by a grid of 12 columns, each row height is equal to the column height. For example, say you want to show 2 panels at the top: one panel might be 6 x 3 (half the width, 3 rows height) located in row 1 and column 1 (top-left corner of the viewport), the second panel might be 6 x 3 located in row 1 and position 7. The location is specified by a dictionary of ``row`` (row position), ``col`` (column position), ``size_x`` (width), ``size_y`` (height). + + **Success Return Value** + A dictionary showing the details of the edited dashboard. + + **Example** + `examples/dashboard.py `_ + """ + panel_configuration = { + 'name': name, + 'showAs': None, + 'showAsType': None, + 'metrics': [], + 'gridConfiguration': { + 'col': 1, + 'row': 1, + 'size_x': 12, + 'size_y': 6 + } + } + + if panel_type == 'timeSeries': + # + # In case of a time series, the current dashboard implementation + # requires the timestamp to be explicitly specified as "key". + # However, this function uses the same abstraction of the data API + # that doesn't require to specify a timestamp key (you only need to + # specify time window and sampling) + # + metrics = copy.copy(metrics) + metrics.insert(0, {'id': 'timestamp'}) + + # + # Convert list of metrics to format used by Sysdig Monitor + # + property_names = {} + k_count = 0 + v_count = 0 + for i, metric in enumerate(metrics): + property_name = 'v' if 'aggregations' in metric else 'k' + + if property_name == 'k': + i = k_count + k_count += 1 + else: + i = v_count + v_count += 1 + property_names[metric['id']] = property_name + str(i) + + panel_configuration['metrics'].append({ + 'metricId': metric['id'], + 'aggregation': metric['aggregations']['time'] if 'aggregations' in metric else None, + 'groupAggregation': metric['aggregations']['group'] if 'aggregations' in metric else None, + 'propertyName': property_name + str(i) + }) + + panel_configuration['scope'] = scope + # if chart scope is equal to dashboard scope, set it as non override + panel_configuration['overrideFilter'] = ('scope' in dashboard and dashboard['scope'] != scope) or ('scope' not in dashboard and scope != None) + + # + # Configure panel type + # + if panel_type == 'timeSeries': + panel_configuration['showAs'] = 'timeSeries' + panel_configuration['showAsType'] = 'line' + + if limit != None: + panel_configuration['paging'] = { + 'from': 0, + 'to': limit - 1 + } + + elif panel_type == 'number': + panel_configuration['showAs'] = 'summary' + panel_configuration['showAsType'] = 'summary' + elif panel_type == 'top': + panel_configuration['showAs'] = 'top' + panel_configuration['showAsType'] = 'bars' + + if sort_by is None: + panel_configuration['sorting'] = [{ + 'id': 'v0', + 'mode': 'desc' + }] + else: + panel_configuration['sorting'] = [{ + 'id': property_names[sort_by['metric']], + 'mode': sort_by['mode'] + }] + + if limit is None: + panel_configuration['paging'] = { + 'from': 0, + 'to': 10 + } + else: + panel_configuration['paging'] = { + 'from': 0, + 'to': limit - 1 + } + + # + # Configure layout + # + if layout != None: + panel_configuration['gridConfiguration'] = layout + + # + # Clone existing dashboard... + # + dashboard_configuration = copy.deepcopy(dashboard) + dashboard_configuration['id'] = None + + # + # ... and add the new panel + # + dashboard_configuration['items'].append(panel_configuration) + + # + # Update dashboard + # + res = requests.put(self.url + self._dashboards_api_endpoint + '/' + str(dashboard['id']), headers=self.hdrs, data=json.dumps({'dashboard': dashboard_configuration}), + verify=self.ssl_verify) + return self._request_result(res) + + def remove_dashboard_panel(self, dashboard, panel_name): + '''**Description** + Removes a panel from the dashboard. The panel to remove is identified by the specified ``name``. + + **Arguments** + - **name**: name of the panel to find and remove + + **Success Return Value** + A dictionary showing the details of the edited dashboard. + + **Example** + `examples/dashboard.py `_ + ''' + # + # Clone existing dashboard... + # + dashboard_configuration = copy.deepcopy(dashboard) + dashboard_configuration['id'] = None + + # + # ... find the panel + # + def filter_fn(panel): + return panel['name'] == panel_name + + panels = list(filter(filter_fn, dashboard_configuration['items'])) + + if len(panels) > 0: + # + # ... and remove it + # + for panel in panels: + dashboard_configuration['items'].remove(panel) + + # + # Update dashboard + # + res = requests.put(self.url + self._dashboards_api_endpoint + '/' + str(dashboard['id']), headers=self.hdrs, data=json.dumps({'dashboard': dashboard_configuration}), + verify=self.ssl_verify) + return self._request_result(res) + else: + return [False, 'Not found'] + + def _get_dashboard_converters(self): + '''**Description** + Internal function to return dashboard converters from one version to another one. + ''' + # There's not really a previous version... + return {}