Skip to main content
Sisense Community logo
  • Community Feedback
  • Chapters
  • Events
  • Forums
    • Help and How To
    • Product Feedback Forum
    • Strategy & Use Cases
  • Blogs
  • KB Docs
    • KB Docs
    • Add-Ons & Plug-Ins
    • APIs
    • Best Practices
    • Blox
    • CDT
    • Cloud Managed Service
    • Data Models
    • Data Sources
    • Embedding Analytics
    • How-Tos & FAQs
    • Onboarding
    • PySisense
    • Security
    • Sisense Administration
    • Sisense Intelligence & AI
    • Troubleshooting
    • Widget & Dashboard Scripts
  • Support
  • Learning
    • Sisense Academy: Free Courses and Certifications
    • Official Developer Documentation
    • Official Product Documentation
    • Official Sisense Youtube Channel
    • Sisense Compose SDK Playground
  • Use Case Gallery
All PostsDiscussionsBlogsIdeasQuestions
Leaderboards
  •                    
  •                    
  •                    
  •                    
  •                    
  •                    
  •                    
  •                    
  •                    
  •                    
  •                    
  •                    
  •                    
  •                    
  •                    
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
Discussions
  • TagsChevronRightIcon
Deploy: Linux
  • Question
    • Help and How-ToChevronRightIcon
                     
    Create flawless pixel perfect reports on PDF, PPT and XLS directly within Sisense using Report Studio
    Ido Darnell
    Ido Darnell
    Posted 2 months ago
    0
             
  • Blog banner
    • PySisenseChevronRightIcon

    Modifying Sisense Widgets With PySisense

                                                                                                                                     

    Modifying Sisense Widgets With PySisense Overview A chart widget's metadata, including the dimension driving its category axis and the measures driving its values, lives inside the widget's own JSON definition rather than on a separate settings screen. Changing one, through the widget's Edit panel, can be done quickly. Mass modifying the same field across many widgets, or repeating the same change every time a dashboard is rebuilt from a template, is not something the Sisense UI is built to do in bulk. This article describes swap_widget_dimension.py , a Python command-line tool that reads a Sisense chart widget, replaces the field used by one item on a named panel, and writes the result back using the pySisense library. By default it targets the categories panel, a bar chart's category axis, but the same get_widget_by_id and update_widget calls apply to any panel in the widget's metadata.panels structure, including values , so the tool also works for swapping a measure. The worked example throughout is a bar chart with two versions on a dashboard that differ only in their measure: one totals Revenue, the other totals Cost. The script changes only the field of one panel item on one widget per run. It is meant primarily as an example, and can be adapted to cover other widget attributes as needed. The full source is available on GitHub in this repository . About PySisense PySisense is a Python SDK for the Sisense REST API, installed via pip . It provides wrappers around common Sisense operations and handles authentication, session management, and request errors internally. This script uses three methods from pySisense's Dashboard class: get_widget_by_id , which retrieves a widget's full current definition; update_widget , which writes a modified definition back and strips the fields Sisense manages internally ( oid , owner , created , and so on) before sending the request; and, through Dashboard 's access_mgmt attribute, get_my_user , which identifies the Sisense user the API token belongs to. Note: The current release of PySisense on PyPI does not yet include get_widget_by_id , update_widget , or find_widgets_by_type . This functionality is planned for the next PySisense release. The development branch of the PySisense repository already contains it and can be installed directly from there: pip install "git+https://github.com/sisense/pysisense.git@dev" How a chart widget stores its fields A Sisense chart widget's configuration lives under metadata.panels , a list of panel objects. For a bar chart, the panel named categories holds the dimension shown on the category axis, and the panel named values holds the measures. Each panel has an items list, and each item carries a jaql object identifying one field, with table , column , and dim keys ( dim in the form [Table.Column] ), plus a title and other fields, such as datatype or agg , that vary by field type. Panel names and item order can differ by widget type and by how a chart was originally built, so it's worth confirming the structure on the specific widget being changed rather than assuming it. Running the script with --show prints a widget's current panels for exactly this purpose, before anything is changed: python swap_widget_dimension.py --show Prerequisites Python 3.10 or later pySisense (for now, currently the dev branch, this will be merged into the main branch shortly) and PyYAML Network access to the Sisense instance A Sisense API token: Admin > REST API > v1.0 > GET /authentication/tokens/api Only a dashboard's owner can write to its widgets. If the API token's user does not own the dashboard being changed, use a token belonging to the dashboard's owner, or transfer ownership first with pySisense's change_dashboard_owner . Setup pip install "git+https://github.com/sisense/pysisense.git@dev" pyyaml Create a config.yaml with the Sisense instance details, the widget to change, and the field to switch to: sisense: domain: https://your-instance.sisense.com token: YOUR_API_TOKEN_HERE is_ssl: true widget: dashboard_id: YOUR_DASHBOARD_ID widget_id: YOUR_WIDGET_ID dimension: table: Commerce column: Region title: Region Run with --show first to confirm the widget has the panel expected, then continue with the sequence in Usage. Configuration reference sisense.domain , sisense.token , sisense.is_ssl : connection details for the Sisense instance. widget.dashboard_id , widget.widget_id : the widget to change. Both can be overridden for a single run with --dashboard-id and --widget-id without editing config.yaml . widget.admin_access : left unset by default, which auto-detects the right setting (see Troubleshooting below). Set to true or false , or pass --admin-access or --no-admin-access , only to skip that auto-detection. dimension.panel : the metadata.panels entry to change. categories for a bar chart's category axis, values for its measures. Defaults to categories if unset. Can be overridden with --panel . dimension.table , dimension.column , dimension.title : the new field to apply. table and column identify it; title is the label shown on the widget and defaults to column when omitted. All three can be overridden with --table , --column , and --title . Usage Inspect the widget's current panels before changing anything: python swap_widget_dimension.py --show Preview the change without writing it: python swap_widget_dimension.py Apply the change: python swap_widget_dimension.py --apply If the result is not correct, revert to the widget's prior state: python swap_widget_dimension.py --undo Troubleshooting Whether adminAccess is needed depends on ownership, not the Sisense version. get_widget_by_id can fail with a schema validation error mentioning adminAccess , on Sisense instances that reject the parameter outright, or with a permission error, on instances that require it for a widget the API token's user doesn't own. Rather than have the caller guess which applies, the script tries without admin access first, and only retries with admin access if that fails. It also prints a line identifying the token's user and whether that user owns the widget, so the reason either attempt succeeded is visible. --admin-access and --no-admin-access are still available to force one path and skip the auto-detection. Worked example: swapping a measure from Revenue to Cost A dashboard can have two versions of the same bar chart widget that differ only in their values panel, one totaling Commerce.Revenue , the other Commerce.Cost . Pointing the script at the Revenue widget and targeting the values panel swaps it to Cost, keeping its sum aggregation intact: python swap_widget_dimension.py \ --dashboard-id YOUR_DASHBOARD_ID --widget-id YOUR_WIDGET_ID \ --panel values --table Commerce --column Cost --title "Total Cost" \ --apply Running --show against the widget beforehand returns: { "table": "Commerce", "column": "Revenue", "dim": "[Commerce.Revenue]", "datatype": "numeric", "columnTitle": "Revenue", "tableTitle": "Commerce", "agg": "sum", "title": "Total Revenue" } and after --apply : { "table": "Commerce", "column": "Cost", "dim": "[Commerce.Cost]", "datatype": "numeric", "columnTitle": "Cost", "tableTitle": "Commerce", "agg": "sum", "title": "Total Cost" } table , column , dim , columnTitle , tableTitle , and title all changed to Cost; datatype and the sum aggregation in agg carried over unchanged, exactly as build_new_jaql is designed to do. Common use cases Reusing one chart across dashboard variants A bar chart that should show a different measure, or a different category dimension, on each of several dashboards can be built once and adjusted per dashboard with a single command instead of being rebuilt in the widget editor each time. Correcting a field after templating Dashboards built from a template sometimes carry the template's placeholder field. Rather than reopening every affected widget by hand, each one can be pointed at the correct field directly. Working with more than one item on a panel A widget with more than one item on the panel being changed, such as a grouped bar chart with two category dimensions, needs --match-column to identify which one to replace: python swap_widget_dimension.py --apply --match-column Category Full option reference usage: swap_widget_dimension.py [-h] [--config FILE] [--dashboard-id ID] [--widget-id ID] [--panel PANEL] [--table TABLE] [--column COLUMN] [--title TITLE] [--match-column COLUMN] [--show] [--apply] [--undo] [--backup-file FILE] [--admin-access | --no-admin-access] Swap the field used by one item on a Sisense chart widget panel. options: -h, --help show this help message and exit --config FILE Path to config.yaml. --dashboard-id ID Overrides widget.dashboard_id from config.yaml. --widget-id ID Overrides widget.widget_id from config.yaml. --panel PANEL Name of the metadata.panels entry to change, for example categories (a bar chart's category axis) or values (its measures). Overrides dimension.panel from config.yaml. Defaults to 'categories' if neither is set. --table TABLE Overrides dimension.table from config.yaml. --column COLUMN Overrides dimension.column from config.yaml. --title TITLE Overrides dimension.title from config.yaml. --match-column COLUMN Column name of the item to replace within --panel, if it has more than one item. Defaults to the first item. --show Print the widget's current metadata.panels and exit. Use this to confirm panel names and item order before swapping anything. --apply Write the change to Sisense. Without this flag, only a preview is printed. --undo Restore the widget to the state recorded in --backup- file, then exit. --backup-file FILE Where the widget's prior state is saved before a change, and read back from during --undo. --admin-access Always fetch with adminAccess=true, skipping auto- detection. Needed when the API token's user does not own the widget. --no-admin-access Always fetch without adminAccess=true, skipping auto- detection. Needed on Sisense instances that reject adminAccess as an unrecognized query parameter. Use case Using swap_widget_dimension.py instead of manual widget edits offers several advantages: Applies a field change to a widget directly through its JSON definition, without opening the widget editor Works on either a chart's category dimension or its measures, through the same --panel option Provides a way to confirm a widget's panel structure with --show before any change is made Auto-detects whether admin access is needed, rather than requiring the caller to know their token's relationship to the widget in advance Provides a backup of the widget's prior state before every applied change, with --undo to restore it Demonstrates how pySisense's get_widget_by_id and update_widget can be used together to read, modify, and write back any field in a widget's definition, not only its dimension Summary swap_widget_dimension.py gives Sisense administrators and developers a way to change the field used by a chart widget's category axis or its measures from the command line, with a preview step, a way to inspect the widget's actual structure first, auto-detection of whether admin access is needed, and a backup that makes any applied change reversible. The same get_widget_by_id and update_widget pattern used here, fetch the widget, modify the field that needs to change, write it back, applies to other widget fields beyond the ones shown in this example. """swap_widget_dimension.py: swap the field used by one item on a Sisense chart widget panel. Reads a widget with ``Dashboard.get_widget_by_id()``, replaces the field used by one item in a named ``metadata.panels`` entry, such as ``categories`` (a bar chart's category axis) or ``values`` (its measures), and writes the result back with ``Dashboard.update_widget()``. "Dimension" is used loosely here, the way most readers of this script will mean it: the field dropped into that panel, whether it is technically a dimension or a measure. This is a worked example of pySisense's widget read and write functions, not a general purpose widget editor: it changes one field on one panel item on one widget per run, and does not touch anything else on the widget. Note: get_widget_by_id, update_widget, and find_widgets_by_type are not yet in the pySisense release on PyPI. Install from the dev branch to use this script: pip install "git+https://github.com/sisense/pysisense.git@dev" Usage ----- python swap_widget_dimension.py --show # print the widget's panels, change nothing python swap_widget_dimension.py # preview the swap, change nothing python swap_widget_dimension.py --apply # write the change to Sisense python swap_widget_dimension.py --undo # restore from the backup file python swap_widget_dimension.py --panel values --apply # swap a measure instead of the category axis Whether a widget fetch needs adminAccess=true depends on whether the API token's own user already owns the widget. Rather than requiring the caller to know that in advance, fetch_widget() below tries without admin access first and only escalates if that fails. """ from __future__ import annotations import argparse import json import sys from pathlib import Path from typing import Any import yaml from pysisense import Dashboard, SisenseClient SCRIPT_DIR = Path(__file__).parent DEFAULT_CONFIG_PATH = SCRIPT_DIR / "config.yaml" DEFAULT_BACKUP_PATH = SCRIPT_DIR / "widget_backup.json" # The metadata.panels entry this script targets when --panel and # dimension.panel are both left unset: a bar chart's category axis. DEFAULT_PANEL_NAME = "categories" def load_config(path: Path) -> dict[str, Any]: """Load and parse the YAML configuration file. Parameters ---------- path : Path Location of the config.yaml file to read. Returns ------- dict[str, Any] The parsed configuration. Empty dict if the file is empty. Raises ------ SystemExit If no file exists at ``path``. """ if not path.exists(): sys.exit( f"Config file not found: {path}\n" "Copy config.example.yaml to config.yaml and fill in the instance URL, " "token, and widget details." ) with path.open() as config_file: return yaml.safe_load(config_file) or {} def build_client(config: dict[str, Any]) -> SisenseClient: """Construct a SisenseClient from the ``sisense`` section of config.yaml. Parameters ---------- config : dict[str, Any] The full parsed configuration, as returned by :func:`load_config`. Returns ------- SisenseClient A client ready to use with pySisense's Dashboard class. Raises ------ SystemExit If ``sisense.domain`` or ``sisense.token`` is missing. """ sisense_config = config.get("sisense", {}) domain = sisense_config.get("domain", "") token = sisense_config.get("token", "") if not domain or not token: sys.exit("config.yaml must include sisense.domain and sisense.token.") return SisenseClient(domain=domain, token=token, is_ssl=sisense_config.get("is_ssl", True)) def describe_current_user(dashboard: Dashboard) -> dict[str, Any] | None: """Look up the Sisense user the API token belongs to. Used only to print a diagnostic line explaining whether the token's own user already owns the widget being changed, which is what actually determines whether admin access is needed (see :func:`fetch_widget`). A failed lookup here is not fatal to the rest of the script. Parameters ---------- dashboard : Dashboard A pySisense Dashboard instance, whose ``access_mgmt`` attribute exposes ``get_my_user()``. Returns ------- dict[str, Any] | None The logged-in user object (``GET /api/users/loggedin``), or ``None`` if the lookup failed for any reason. """ try: user = dashboard.access_mgmt.get_my_user() except Exception: return None if isinstance(user, dict) and "error" not in user: return user return None def fetch_widget( dashboard: Dashboard, dashboard_id: str, widget_id: str, *, admin_access: bool | None, ) -> tuple[dict[str, Any], bool]: """Fetch a widget, auto detecting whether admin access is required. Whether a fetch needs ``adminAccess=true`` depends on whether the API token's own user already owns the widget, not on the Sisense version. On top of that, some Sisense versions reject ``adminAccess`` outright as an unrecognized query parameter, which looks identical from here to "not the owner". Rather than have the caller guess, this tries the cheaper, no-admin-access request first, and only escalates if that fails. Parameters ---------- dashboard : Dashboard A pySisense Dashboard instance. dashboard_id : str The ``oid`` of the dashboard that contains the widget. widget_id : str The ``oid`` of the widget to fetch. admin_access : bool | None ``True`` or ``False`` forces that mode and skips auto-detection. ``None`` tries without admin access first, then retries with admin access only if the first attempt fails. Returns ------- tuple[dict[str, Any], bool] The fetched widget, and whether admin access was actually used to retrieve it. Raises ------ SystemExit If the widget cannot be fetched in the requested mode(s). """ if admin_access is not None: widget = dashboard.get_widget_by_id(dashboard_id, widget_id, admin_access=admin_access) if "error" in widget: sys.exit(f"Could not fetch widget: {widget['error']}") return widget, admin_access widget = dashboard.get_widget_by_id(dashboard_id, widget_id, admin_access=False) if "error" not in widget: return widget, False error_without_admin_access = widget["error"] widget = dashboard.get_widget_by_id(dashboard_id, widget_id, admin_access=True) if "error" not in widget: return widget, True sys.exit( "Could not fetch widget, with or without admin access.\n" f" Without admin access: {error_without_admin_access}\n" f" With admin access: {widget['error']}" ) def print_fetch_diagnostics(dashboard: Dashboard, widget: dict[str, Any], admin_access_used: bool) -> None: """Print a line explaining whether the token's user owns this widget. Purely informational: makes the reason admin access was, or was not, needed for this particular widget visible, instead of leaving the ``True``/``False`` in ``admin_access_used`` unexplained. Printed to stderr, not stdout, so it never lands in ``--show``'s JSON output and can be piped straight into something like ``jq``. Parameters ---------- dashboard : Dashboard A pySisense Dashboard instance, used to look up the current user. widget : dict[str, Any] The widget returned by :func:`fetch_widget`. admin_access_used : bool Whether the fetch that retrieved ``widget`` used admin access. Returns ------- None """ access_note = "with admin access" if admin_access_used else "without admin access" current_user = describe_current_user(dashboard) if current_user is None: print(f"Fetched widget {access_note} (could not look up the current user for comparison).", file=sys.stderr) return identity = current_user.get("email") or current_user.get("userName") or current_user.get("_id") owns_widget = current_user.get("_id") == widget.get("owner") ownership_note = "owns this widget" if owns_widget else "does not own this widget" print(f"Fetched widget {access_note}. Authenticated as {identity!r}, who {ownership_note}.", file=sys.stderr) def find_panel_item( widget: dict[str, Any], panel_name: str, match_column: str | None, ) -> tuple[list[dict[str, Any]], int]: """Locate the panel item to change. Parameters ---------- widget : dict[str, Any] The full widget object, as returned by ``get_widget_by_id``. panel_name : str The ``metadata.panels`` entry to search, for example ``"categories"`` (a bar chart's category axis) or ``"values"`` (its measures). match_column : str | None If given, selects the item whose current ``jaql.column`` equals it. If ``None``, the first item in the panel is used, which is correct whenever that panel only has one item, such as a simple bar chart's single category dimension or single measure. Returns ------- tuple[list[dict[str, Any]], int] The panel's ``items`` list, and the index within it of the item to change. Raises ------ SystemExit If the widget has no panel named ``panel_name``, the panel has no items, or ``match_column`` does not match any item in it. """ panels = widget.get("metadata", {}).get("panels", []) panel = next((candidate for candidate in panels if candidate.get("name") == panel_name), None) if panel is None or not panel.get("items"): sys.exit(f"No {panel_name!r} panel with items found on this widget. Run --show to inspect its structure.") items = panel["items"] if match_column is None: return items, 0 for index, item in enumerate(items): if item.get("jaql", {}).get("column") == match_column: return items, index sys.exit(f"No item with column {match_column!r} found in the {panel_name!r} panel. Run --show to see the current items.") def build_new_jaql(existing_jaql: dict[str, Any], table: str, column: str, title: str | None) -> dict[str, Any]: """Return a copy of ``existing_jaql`` with its field swapped. ``table``, ``column``, and ``dim`` always change. ``title`` changes to ``title`` or, if that is ``None``, to ``column``. ``columnTitle`` and ``tableTitle`` (Sisense's own display-label mirrors of ``column`` and ``table``, confirmed by inspecting a live widget) are updated to match whenever the existing jaql already had them, so nothing about the field still points at the old one under a different key. ``agg`` and every other key already present (``datatype``, and so on) are carried over unchanged. Preserving ``agg`` in particular is what makes this safe to use on a ``values`` panel item, not only a ``categories`` one: swapping Revenue for Cost keeps the sum aggregation the widget already had, instead of resetting it. Parameters ---------- existing_jaql : dict[str, Any] The jaql object currently on the panel item being replaced. table : str The table of the new field. column : str The column of the new field. title : str | None The label to show on the widget for the new field. Defaults to ``column`` when ``None``. Returns ------- dict[str, Any] A new jaql object with the field swapped. """ new_jaql = dict(existing_jaql) new_jaql["table"] = table new_jaql["column"] = column new_jaql["dim"] = f"[{table}.{column}]" new_jaql["title"] = title or column if "columnTitle" in new_jaql: new_jaql["columnTitle"] = column if "tableTitle" in new_jaql: new_jaql["tableTitle"] = table return new_jaql def write_widget(dashboard: Dashboard, dashboard_id: str, widget_id: str, payload: dict[str, Any]) -> None: """Write ``payload`` back to Sisense via ``Dashboard.update_widget()``. Parameters ---------- dashboard : Dashboard A pySisense Dashboard instance. dashboard_id : str The ``oid`` of the dashboard that contains the widget. widget_id : str The ``oid`` of the widget being written. payload : dict[str, Any] The full widget object to write back. Returns ------- None Raises ------ SystemExit If the API reports a write failure. """ result = dashboard.update_widget(dashboard_id, widget_id, payload) if "error" in result: sys.exit(f"Update failed: {result['error']}") def parse_args() -> argparse.Namespace: """Define and parse this script's command-line arguments. Returns ------- argparse.Namespace The parsed arguments. """ parser = argparse.ArgumentParser(description="Swap the field used by one item on a Sisense chart widget panel.") parser.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), metavar="FILE", help="Path to config.yaml.") parser.add_argument("--dashboard-id", metavar="ID", help="Overrides widget.dashboard_id from config.yaml.") parser.add_argument("--widget-id", metavar="ID", help="Overrides widget.widget_id from config.yaml.") parser.add_argument( "--panel", help="Name of the metadata.panels entry to change, for example categories (a bar " "chart's category axis) or values (its measures). Overrides dimension.panel from " f"config.yaml. Defaults to {DEFAULT_PANEL_NAME!r} if neither is set.", ) parser.add_argument("--table", help="Overrides dimension.table from config.yaml.") parser.add_argument("--column", help="Overrides dimension.column from config.yaml.") parser.add_argument("--title", help="Overrides dimension.title from config.yaml.") parser.add_argument( "--match-column", metavar="COLUMN", help="Column name of the item to replace within --panel, if it has more than one item. " "Defaults to the first item.", ) parser.add_argument( "--show", action="store_true", help="Print the widget's current metadata.panels and exit. Use this to confirm panel " "names and item order before swapping anything.", ) parser.add_argument( "--apply", action="store_true", help="Write the change to Sisense. Without this flag, only a preview is printed.", ) parser.add_argument( "--undo", action="store_true", help="Restore the widget to the state recorded in --backup-file, then exit.", ) parser.add_argument( "--backup-file", default=str(DEFAULT_BACKUP_PATH), metavar="FILE", help="Where the widget's prior state is saved before a change, and read back from during --undo.", ) admin_access_group = parser.add_mutually_exclusive_group() admin_access_group.add_argument( "--admin-access", dest="admin_access", action="store_true", default=None, help="Always fetch with adminAccess=true, skipping auto-detection. Needed when the API " "token's user does not own the widget.", ) admin_access_group.add_argument( "--no-admin-access", dest="admin_access", action="store_false", help="Always fetch without adminAccess=true, skipping auto-detection. Needed on Sisense " "instances that reject adminAccess as an unrecognized query parameter.", ) return parser.parse_args() def main() -> None: """Entry point: parse arguments, then fetch, inspect, swap, or restore a widget's dimension.""" args = parse_args() config = load_config(Path(args.config)) client = build_client(config) dashboard = Dashboard(api_client=client) widget_config = config.get("widget", {}) dashboard_id = args.dashboard_id or widget_config.get("dashboard_id") widget_id = args.widget_id or widget_config.get("widget_id") if not dashboard_id or not widget_id: sys.exit( "dashboard_id and widget_id are required, either in config.yaml under 'widget' " "or via --dashboard-id/--widget-id." ) admin_access = args.admin_access if args.admin_access is not None else widget_config.get("admin_access") if args.undo: backup_path = Path(args.backup_file) if not backup_path.exists(): sys.exit(f"No backup file found at {backup_path}. Nothing to undo.") original_widget = json.loads(backup_path.read_text()) write_widget(dashboard, dashboard_id, widget_id, original_widget) print(f"Widget {widget_id} restored from {backup_path}.") return widget, admin_access_used = fetch_widget(dashboard, dashboard_id, widget_id, admin_access=admin_access) print_fetch_diagnostics(dashboard, widget, admin_access_used) if args.show: print(json.dumps(widget.get("metadata", {}).get("panels", []), indent=2)) return dimension_config = config.get("dimension", {}) panel_name = args.panel or dimension_config.get("panel") or DEFAULT_PANEL_NAME table = args.table or dimension_config.get("table") column = args.column or dimension_config.get("column") title = args.title or dimension_config.get("title") if not table or not column: sys.exit("table and column are required, either in config.yaml under 'dimension' or via --table/--column.") items, index = find_panel_item(widget, panel_name, args.match_column) old_jaql = items[index].get("jaql", {}) new_jaql = build_new_jaql(old_jaql, table, column, title) print(f"\nWidget: {widget.get('title') or widget_id!r} ({widget.get('type')})") print(f" {'Panel:':<15}{panel_name!r}") print(f" {'Current field:':<15}{old_jaql.get('table')}.{old_jaql.get('column')} (title: {old_jaql.get('title')!r})") print(f" {'New field:':<15}{table}.{column} (title: {new_jaql['title']!r})") if not args.apply: print("\n[PREVIEW] No changes made. Re-run with --apply to write this change to Sisense.") return backup_path = Path(args.backup_file) backup_path.write_text(json.dumps(widget, indent=2)) print(f"\nBacked up current widget state to {backup_path}.") items[index]["jaql"] = new_jaql write_widget(dashboard, dashboard_id, widget_id, widget) print(f"Widget {widget_id} updated. To revert: python {Path(__file__).name} --undo --backup-file {backup_path}") if __name__ == "__main__": main()

    Before and After Change Via Tool
    Disclaimer: This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.

    Jeremy Friedel
    Jeremy FriedelPosted 2 months ago
    0
             
  • Blog banner
    • PySisenseChevronRightIcon

    Managing Sisense Plugin States with PySisense

                                                                                                     

    Managing Sisense Plugin States with PySisense Overview Enabling and disabling Sisense plugins through the admin interface becomes time consuming when maintaining consistent plugin configurations across environments, testing against a clean instance, or iterating on plugin development. This article describes plugin_manager_pysisense.py , a Python command-line tool that manages Sisense plugin states using the PySisense library and a configuration file. The script is also a working example of how PySisense can be used for Sisense automation. This tool covers the same functionality as plugin_manager.py , documented in Managing Sisense Plugin States via the REST API . The difference is that this version delegates all API request code to PySisense rather than using the requests library directly. The two scripts share the same config.yaml format and produce identical output. The script enables and disables plugins, isolates a single plugin for faster development builds, and saves and restores named snapshots of plugin state. It does not install, uninstall, or update plugins, and does not roll back plugin versions. About PySisense PySisense is introduced in PySisense: Programmable Sisense Environment Management on the Sisense Community as a Python SDK for the Sisense REST API. It provides wrappers around common Sisense operations, including plugin management, dashboard access, and user administration. It is installed via pip and does not need to be placed alongside the script. This script provides a practical example of PySisense applied to a real automation task. Using PySisense rather than writing directly against the REST API reduces the amount of code required and removes several categories of boilerplate. Authentication headers, HTTP session management, SSL configuration, pagination across large result sets, and request logging are all handled by the library. The consistent SisenseClient and module pattern also makes it straightforward to extend a script to cover other Sisense operations, such as users, dashboards, or data models, without reworking connection and authentication code. Scripts built on PySisense are shorter and easier to read than equivalent scripts built on requests directly, and the library is maintained and tested independently of the scripts that use it. Note: The current release of PySisense on PyPI does not yet include the Plugins module used in this script. This functionality is planned for the next PySisense release. The development branch of the PySisense repository already contains it and can be installed directly from there. This script uses the Plugins class from PySisense: get_all_plugins() returns the full plugin list as a list of dicts, handling the pagination in the underlying API automatically. enable_plugins(names, bulk=True) enables one or more plugins by name. When bulk=True , all changes are sent in a single PATCH request. disable_plugins(names, bulk=True) disables one or more plugins by name, with the same bulk option. A SisenseClient instance is constructed from the config values and passed to the Plugins class. All HTTP calls are then handled by PySisense. from pysisense import Plugins, SisenseClient client = SisenseClient(domain="https://your-sisense-instance.com", token="YOUR_TOKEN") plugins = Plugins(api_client=client) # List all installed plugins all_plugins = plugins.get_all_plugins() # Enable plugins by short name, full name, or folderName result = plugins.enable_plugins(["SwitchDimension", "plugin-CustomPlugin"], bulk=True) # Disable plugins result = plugins.disable_plugins(["UnneededPlugin"], bulk=True) Both enable_plugins and disable_plugins return a summary dict with changed , already_enabled or already_disabled , not_found , and errors keys. Features that require knowing the current plugin state before making changes, such as strict mode, enable-one, and dry-run previews, call get_all_plugins() once at the start of the command and compute the set of changes at the script level. Plugin name matching Plugin names in the configuration file and on the command line accept either the short form ( SwitchDimension ) or the full form ( plugin-SwitchDimension ). Matching is case-insensitive. PySisense normalizes names internally, so both forms work with enable_plugins() and disable_plugins() as well. Prerequisites Python 3.10 or later Network access to the Sisense instance (the tool runs remotely and does not need to be installed on the server) A Sisense API token: Admin > REST API > v1.0 > GET /authentication/tokens/api Setup Install the required Python packages: pip install pysisense pyyaml Create a config.yaml and fill in the Sisense instance URL and API token: sisense: url: https://your-sisense-instance.com token: YOUR_API_TOKEN_HERE Add the plugins to enable to the enable_plugins list. See the Configuration reference section for all available settings. The script is then ready to run. Configuration reference sisense: url: https://your-sisense-instance.com token: YOUR_API_TOKEN_HERE # Admin → REST API → v1.0 → GET /authentication/tokens/api bulk: true # set false to update plugins one at a time instead of a single batch request # Plugins to enable. # Accepts short names (SwitchDimension) or full names (plugin-SwitchDimension). # Matching is case-insensitive. # # Default behavior: enable listed plugins; leave every other plugin unchanged. # With --strict: also disable any currently-enabled plugin NOT in this list, # making the instance exactly match this config. enable_plugins: - examplePlugin # Plugins to explicitly disable (optional). # Only needed in the default sync (no --strict), where unlisted plugins are # left alone. Listing a plugin here ensures it stays off and documents the # intentional exclusion. # disable_plugins: # - anotherExamplePlugin enable_plugins : the list of plugins to enable. By default, plugins not on this list are left in their current state. With --strict , any currently enabled plugin not on this list will be disabled. disable_plugins : optional. Plugins to explicitly disable in the default sync without --strict . Useful for documenting intentional exclusions when strict mode is not being used. bulk : controls whether plugin state changes are sent as a single batch API request or one at a time. Set to false for individual requests, which is slower but can be useful for diagnosing issues with specific plugins. Usage Run the script with no arguments to enable plugins in enable_plugins , disable plugins in disable_plugins , and leave everything else unchanged: python plugin_manager_pysisense.py Before making any changes, the script saves a timestamped snapshot of the current plugin state. To restore the most recent snapshot: python plugin_manager_pysisense.py --restore-snapshot To preview what would change without making any API calls: python plugin_manager_pysisense.py --dry-run Common use cases Maintaining a known good plugin baseline Organizations that need to ensure consistent plugin configuration across upgrades, support sessions, or environment changes can maintain an enable_plugins list in the configuration file. Running the script applies the defined configuration to the instance. To also disable any currently enabled plugin not in the list: python plugin_manager_pysisense.py --strict Testing on a stock Sisense instance Plugin interference is a common source of issues that are difficult to reproduce. To quickly disable all plugins and test against a clean instance, clear enable_plugins in the configuration file and run with --strict . The current state is saved automatically before any changes are made, and can be restored when testing is complete: # with enable_plugins cleared in config.yaml python plugin_manager_pysisense.py --strict # restore when done python plugin_manager_pysisense.py --restore-snapshot Developing and testing a single plugin When many plugins are installed, Sisense rebuilds all of them whenever any plugin changes. Using --enable-one disables all currently enabled plugins and enables only the specified plugin, which can significantly reduce rebuild time on instances with many plugins: python plugin_manager_pysisense.py --enable-one MyPlugin Snapshots The script saves snapshots as YAML files to a snapshots/ folder alongside the script. Snapshots capture the full enabled and disabled plugin state at a point in time. Snapshots are saved automatically before any run that changes state. Named snapshots can also be saved and restored explicitly: # save the current state with a specific name python plugin_manager_pysisense.py --save-snapshot before-upgrade # list all saved snapshots python plugin_manager_pysisense.py --list-snapshots # restore a specific snapshot python plugin_manager_pysisense.py --restore-snapshot before-upgrade Including --log-version when saving a snapshot records the version of each plugin at that point in time. Snapshots saved by plugin_manager_pysisense.py and plugin_manager.py are compatible. Both scripts read from the same snapshots/ directory, and each handles the other's snapshot format. Full option reference usage: plugin_manager_pysisense.py [-h] [--config FILE] [--dry-run] [--no-bulk] [--skip-snapshot] [--log-version] [--strict] [--enable-only | --disable-only] [--enable-one PLUGIN | --save-snapshot [NAME] | --list-snapshots | --restore-snapshot [NAME]] Manage Sisense plugin states via PySisense. options: -h, --help show this help message and exit --config FILE Path to config.yaml (default: config.yaml next to this script) --dry-run Preview what would change; make no API calls and save no snapshot --no-bulk Send one API call per plugin instead of a single batch PATCH. Slower but easier to diagnose partial failures. Overrides sisense.bulk in config. --skip-snapshot Do not automatically save a snapshot before this run --log-version Record each plugin's version field (from the API response) in any snapshot saved during this run --strict Also disable every currently enabled plugin that is not listed in enable_plugins. Without this flag, only plugins in disable_plugins are disabled; everything else is left alone. Always active for --restore-snapshot. --enable-only Run only the enable step; skip all disables --disable-only Run only the disable step; skip all enables --enable-one PLUGIN Enable exactly one plugin by name and disable all currently enabled others. Accepts the short or full plugin name. --save-snapshot [NAME] Save the current plugin state as a named snapshot. Omit NAME for an auto generated timestamp filename. Combine with --log-version to also record versions. --list-snapshots List all saved snapshots with creation date and enabled plugin count --restore-snapshot [NAME] Restore plugin state to exactly match a saved snapshot. Omit NAME to use the most recently created snapshot. Always strict (disables plugins not in snapshot) unless --enable-only. Plugin names accept the short form (SwitchDimension) or the full form (plugin-SwitchDimension) in both CLI args and config.yaml. Default behavior: enable plugins in enable_plugins, disable plugins in disable_plugins, leave everything else unchanged. Use --strict to also disable any enabled plugin not in enable_plugins. A snapshot is saved automatically before each run that modifies state. Use --skip-snapshot to suppress this. Use Case Managing plugins via PySisense and a configuration file offers several advantages over manual administration: Applies plugin active list consistently and repeatably without manual steps Provides automatic rollback through snapshots saved before each change Supports quickly switching between plugin states for testing or development Reduces the time required to restore a known working plugin list after an issue is resolved Demonstrates how PySisense can be used to build automation scripts for Sisense instances Summary With this tool in place, administrators and developers can manage Sisense plugin states quickly and reliably from the command line. The configuration file serves as a single source of truth for the intended plugin set, and the snapshot system ensures that any change can be reversed. The script also serves as a concrete example of how PySisense can be used in automation scripts: constructing a SisenseClient from config values, passing it to a feature class, and using the returned result dicts to drive output and error handling. The code is not compressed or obfuscated and can be modified as needed or used as a reference for similar tools. j""" plugin_manager_pysisense.py — Manage Sisense plugin states via pySisense. Same feature set as plugin_manager.py but delegates all API calls to the pySisense library instead of calling the Sisense REST API directly. Configuration is read from config.yaml. This script is also a working example of how pySisense can be used for Sisense automation. Install pySisense with: pip install pysisense Overview Manages Sisense plugin states via pySisense. Treats plugin configuration as code: define which plugins should be enabled in config.yaml, run the script, and the instance matches, quickly, from anywhere with network access. Before applying any changes, the current plugin state is automatically captured in a timestamped snapshot. If a change needs to be reverted, a single --restore-snapshot command rolls the instance back to exactly how it was. By default only the plugins explicitly listed in config.yaml are enabled or disabled, everything else is left as is. Use --strict to also disable any enabled plugin not in the list, making the instance enabled plugin list exactly match the config. Common use cases Maintain a known good plugin baseline Keep enable_plugins in config.yaml up to date. Run the script after upgrades, support sessions, or any time the instance may have drifted from the expected state. Test against a stock (no plugins) Sisense instance Clear enable_plugins in config.yaml, run --strict to disable everything, test, then restore: python plugin_manager_pysisense.py --strict python plugin_manager_pysisense.py --restore-snapshot Develop and iterate on a single plugin quickly python plugin_manager_pysisense.py --enable-one MyPlugin Roll back any change instantly python plugin_manager_pysisense.py --list-snapshots python plugin_manager_pysisense.py --restore-snapshot before-upgrade Usage # Enable listed plugins, disable listed disable_plugins, leave rest unchanged python plugin_manager_pysisense.py # Strict: also disables any enabled plugin not in enable_plugins python plugin_manager_pysisense.py --strict # Preview what would change (no API calls, no auto snapshot) python plugin_manager_pysisense.py --dry-run python plugin_manager_pysisense.py --strict --dry-run # Run only one half of the sync python plugin_manager_pysisense.py --enable-only # enable listed; skip all disables python plugin_manager_pysisense.py --disable-only # disable listed; skip all enables # Enable exactly one plugin and disable every other currently enabled plugin python plugin_manager_pysisense.py --enable-one SwitchDimension # Snapshots python plugin_manager_pysisense.py --save-snapshot [NAME] python plugin_manager_pysisense.py --save-snapshot [NAME] --log-version python plugin_manager_pysisense.py --list-snapshots python plugin_manager_pysisense.py --restore-snapshot [NAME] # omit NAME for most recent # Other flags (combinable with any of the above) --no-bulk one API call per plugin instead of a single batch request --skip-snapshot do not save an auto-snapshot before this run --log-version record each plugin's version in any snapshot saved this run --config FILE use a config file other than the default config.yaml Plugin names All plugin names accept either the short form (SwitchDimension) or the full form (plugin-SwitchDimension), both in CLI arguments and in config.yaml. Matching is case insensitive. API token Admin → REST API → v1.0 → GET /authentication/tokens/api Setup pip install pysisense pyyaml # Python 3.10+ required cp config.yaml.example config.yaml # Edit config.yaml: set sisense.url, sisense.token, and enable_plugins list """ import sys from datetime import datetime, timezone from pathlib import Path import yaml from pysisense import Plugins, SisenseClient SCRIPT_DIR = Path(__file__).parent DEFAULT_CONFIG = SCRIPT_DIR / "config.yaml" SNAPSHOTS_DIR = SCRIPT_DIR / "snapshots" # Config def load_config(path: Path) -> dict: if not path.exists(): print(f"Config file not found: {path}") print("Copy config.yaml.example to config.yaml and fill in your instance URL and API token.") sys.exit(1) with path.open() as f: return yaml.safe_load(f) def build_client(sisense_cfg: dict) -> SisenseClient: url = sisense_cfg.get("url", "") token = sisense_cfg.get("token", "") if not url or not token: print("config.yaml must include sisense.url and sisense.token.") sys.exit(1) is_ssl = not url.startswith("http://") return SisenseClient(domain=url, token=token, is_ssl=is_ssl) # Name matching — mirrors pySisense internals, used for dry-run previews and # strict-mode computation (pySisense handles matching internally for real calls) def _normalize(name: str) -> str: lower = name.lower() return lower[len("plugin-"):] if lower.startswith("plugin-") else lower def _build_lookup(names: list) -> set: lookup: set = set() for n in names: norm = _normalize(n) lookup.add(norm) lookup.add("plugin-" + norm) return lookup def _matches_lookup(plugin: dict, lookup: set) -> bool: return bool({plugin["folderName"].lower(), plugin["name"].lower()} & lookup) # Snapshot file I/O def write_snapshot_file(name: str, snapshot: dict) -> Path: SNAPSHOTS_DIR.mkdir(exist_ok=True) path = SNAPSHOTS_DIR / f"{name}.yaml" with path.open("w") as f: yaml.dump(snapshot, f, sort_keys=False, default_flow_style=False, allow_unicode=True) return path def read_snapshot_file(name: str) -> dict: path = SNAPSHOTS_DIR / f"{name}.yaml" if not path.exists(): print(f"Snapshot not found: {path}") sys.exit(1) with path.open() as f: data = yaml.safe_load(f) # Translate 'enable_plugins' key (plugin_manager.py snapshot format) to 'plugins' if "enable_plugins" in data and "plugins" not in data: data["plugins"] = data["enable_plugins"] return data def list_snapshot_files() -> list: if not SNAPSHOTS_DIR.exists(): return [] return sorted(SNAPSHOTS_DIR.glob("*.yaml")) def latest_snapshot_name() -> str: files = list_snapshot_files() if not files: print("No snapshots found.") sys.exit(1) def created_at(path: Path) -> str: with path.open() as f: return yaml.safe_load(f).get("created", "") return max(files, key=created_at).stem def _auto_save_snapshot(all_plugins: list, include_version: bool) -> None: name = datetime.now().strftime("auto_%Y%m%d_%H%M%S") print(f"\nSaving snapshot '{name}' before applying changes...") try: enabled = sorted(p["folderName"] for p in all_plugins if p.get("isEnabled")) disabled = sorted(p["folderName"] for p in all_plugins if not p.get("isEnabled")) snapshot: dict = { "plugins": enabled, "created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), } if include_version: versions: dict = {} for p in all_plugins: v = p.get("version") or p.get("pluginVersion") if v: versions[p["folderName"]] = str(v) if versions: snapshot["plugin_versions"] = versions path = write_snapshot_file(name, snapshot) print(f" Saved to {path} ({len(enabled)} enabled / {len(disabled)} disabled)") except Exception as exc: print(f" Warning: auto snapshot failed: {exc}") # Sync computation — determines which plugins need their state changed. # Used for dry-run previews, strict mode, and enable-one. def _compute_updates( all_plugins: list, enable_names: list, disable_names: list, *, strict: bool, enable_only: bool, disable_only: bool, ) -> tuple: enable_lookup = _build_lookup(enable_names) disable_lookup = _build_lookup(disable_names) matched_enable = [p for p in all_plugins if _matches_lookup(p, enable_lookup)] enable_folder_set = {p["folderName"] for p in matched_enable} to_enable: list = [] if disable_only else sorted( p["folderName"] for p in matched_enable if not p.get("isEnabled") ) if enable_only: to_disable: list = [] elif strict: to_disable = sorted( p["folderName"] for p in all_plugins if p.get("isEnabled") and p["folderName"] not in enable_folder_set ) else: to_disable = sorted( p["folderName"] for p in all_plugins if p.get("isEnabled") and _matches_lookup(p, disable_lookup) ) matched_norms = ( {_normalize(p["folderName"]) for p in matched_enable} | {_normalize(p["name"]) for p in matched_enable} ) unmatched = sorted(n for n in enable_names if _normalize(n) not in matched_norms) return to_enable, to_disable, unmatched # Output helpers def _print_update_plan( to_enable: list, to_disable: list, already_correct: int, untouched: int = 0, ) -> None: print(f" To enable: {len(to_enable)}") print(f" To disable: {len(to_disable)}") if already_correct > 0: print(f" Already set: {already_correct}") if untouched > 0: print(f" Untouched: {untouched} (not listed in config; left as-is)") print() def _print_dry_run_preview(to_enable: list, to_disable: list) -> None: if not to_enable and not to_disable: print("Nothing to change.") if to_enable: print("Would enable:") for f in to_enable: print(f" + {f}") if to_disable: print("Would disable:") for f in to_disable: print(f" - {f}") print("\n[DRY RUN] No changes made.") def _report_unmatched(unmatched: list) -> None: if unmatched: print(f"\nNot found in instance ({len(unmatched)}):") for n in unmatched: print(f" - {n}") def _apply_enable(plugins_obj: Plugins, to_enable: list, bulk: bool) -> int: if not to_enable: return 0 result = plugins_obj.enable_plugins(to_enable, bulk=bulk) if "error" in result: print(f"Failed to enable plugins: {result['error']}") sys.exit(1) for f in result["changed"]: print(f" [ENABLED] {f}") return len(result.get("errors", [])) def _apply_disable(plugins_obj: Plugins, to_disable: list, bulk: bool) -> int: if not to_disable: return 0 result = plugins_obj.disable_plugins(to_disable, bulk=bulk) if "error" in result: print(f"Failed to disable plugins: {result['error']}") sys.exit(1) for f in result["changed"]: print(f" [DISABLED] {f}") return len(result.get("errors", [])) # Commands def cmd_sync_to_config( plugins_obj: Plugins, enable_names: list, disable_names: list, *, dry_run: bool, bulk: bool, strict: bool, enable_only: bool, disable_only: bool, skip_snapshot: bool, include_version: bool, ) -> None: print("Fetching plugin list...") all_plugins = plugins_obj.get_all_plugins() if all_plugins and "error" in all_plugins[0]: print(f"Failed to fetch plugins: {all_plugins[0]['error']}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") to_enable, to_disable, unmatched = _compute_updates( all_plugins, enable_names, disable_names, strict=strict, enable_only=enable_only, disable_only=disable_only, ) enable_lookup = _build_lookup(enable_names) disable_lookup = _build_lookup(disable_names) matched = [p for p in all_plugins if _matches_lookup(p, enable_lookup)] already_correct = sum(1 for p in matched if p.get("isEnabled")) untouched = 0 if strict or enable_only else sum( 1 for p in all_plugins if not _matches_lookup(p, enable_lookup) and not _matches_lookup(p, disable_lookup) ) if matched: print(f"Matched {len(matched)} plugin(s) from enable_plugins:\n") print(f" {'Folder name':<45} {'API name':<35} {'Enabled'}") print(f" {'-'*45} {'-'*35} {'-'*7}") for p in sorted(matched, key=lambda x: x["folderName"]): print(f" {p['folderName']:<45} {p['name']:<35} {p['isEnabled']}") print() _print_update_plan(to_enable, to_disable, already_correct, untouched) if dry_run: _print_dry_run_preview(to_enable, to_disable) _report_unmatched(unmatched) return if not skip_snapshot and (to_enable or to_disable): _auto_save_snapshot(all_plugins, include_version) errors = _apply_disable(plugins_obj, to_disable, bulk) + _apply_enable(plugins_obj, to_enable, bulk) if not to_enable and not to_disable: print("Nothing to change.") else: print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {errors}") _report_unmatched(unmatched) def cmd_enable_one( plugins_obj: Plugins, plugin_name: str, *, dry_run: bool, bulk: bool, skip_snapshot: bool, include_version: bool, ) -> None: target_lookup = _build_lookup([plugin_name]) print("Fetching plugin list...") all_plugins = plugins_obj.get_all_plugins() if all_plugins and "error" in all_plugins[0]: print(f"Failed to fetch plugins: {all_plugins[0]['error']}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") target = next((p for p in all_plugins if _matches_lookup(p, target_lookup)), None) if target is None: print(f"Plugin not found: {plugin_name!r}") print("Tip: check spelling against the Sisense admin panel, or run --save-snapshot to capture current names.") sys.exit(1) target_folder = target["folderName"] target_on = target.get("isEnabled", False) to_enable = [] if target_on else [target_folder] to_disable = sorted( p["folderName"] for p in all_plugins if p.get("isEnabled") and p["folderName"] != target_folder ) print(f"Target plugin: {target_folder}") print(f" Currently: {'enabled' if target_on else 'disabled'}\n") _print_update_plan(to_enable, to_disable, already_correct=1 if target_on else 0) if dry_run: _print_dry_run_preview(to_enable, to_disable) return if not skip_snapshot and (to_enable or to_disable): _auto_save_snapshot(all_plugins, include_version) errors = _apply_disable(plugins_obj, to_disable, bulk) + _apply_enable(plugins_obj, to_enable, bulk) if not to_enable and not to_disable: print("Nothing to change.") else: print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {errors}") def cmd_save_named_snapshot(plugins_obj: Plugins, name: str, include_version: bool) -> None: print("Fetching plugin list...") all_plugins = plugins_obj.get_all_plugins() if all_plugins and "error" in all_plugins[0]: print(f"Failed to fetch plugins: {all_plugins[0]['error']}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") enabled = sorted(p["folderName"] for p in all_plugins if p.get("isEnabled")) disabled = sorted(p["folderName"] for p in all_plugins if not p.get("isEnabled")) snapshot: dict = { "plugins": enabled, "created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), } if include_version: versions: dict = {} for p in all_plugins: v = p.get("version") or p.get("pluginVersion") if v: versions[p["folderName"]] = str(v) if versions: snapshot["plugin_versions"] = versions path = write_snapshot_file(name, snapshot) print(f"Snapshot '{name}' saved to {path}") print(f" {len(enabled)} enabled / {len(disabled)} disabled plugin(s) captured.") if include_version and snapshot.get("plugin_versions"): print(f" Versions recorded for {len(snapshot['plugin_versions'])} plugin(s).") def cmd_list_snapshots() -> None: files = list_snapshot_files() if not files: print("No snapshots found.") return print(f"Snapshots ({len(files)}):\n") print(f" {'Name':<32} {'Created':<22} {'Enabled':<9} {'Versions'}") print(f" {'-'*32} {'-'*22} {'-'*9} {'-'*8}") for path in files: with path.open() as f: data = yaml.safe_load(f) enabled_count = len(data.get("plugins") or data.get("enable_plugins", [])) versions_logged = "yes" if data.get("plugin_versions") else "no" print( f" {path.stem:<32} " f"{data.get('created', 'unknown'):<22} " f"{enabled_count:<9} " f"{versions_logged}" ) def cmd_restore_snapshot( plugins_obj: Plugins, name: str, *, dry_run: bool, bulk: bool, enable_only: bool, disable_only: bool, skip_snapshot: bool, include_version: bool, ) -> None: snapshot = read_snapshot_file(name) snapshot_plugins = set(snapshot.get("plugins", [])) print(f"Snapshot '{name}' ({snapshot.get('created', 'unknown')})") print(f" {len(snapshot_plugins)} plugin(s) enabled in snapshot") if snapshot.get("plugin_versions"): print(f" Plugin versions recorded: {len(snapshot['plugin_versions'])}") print() print("Fetching plugin list...") all_plugins = plugins_obj.get_all_plugins() if all_plugins and "error" in all_plugins[0]: print(f"Failed to fetch plugins: {all_plugins[0]['error']}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") by_folder = {p["folderName"]: p for p in all_plugins} to_enable: list = [] if disable_only else sorted( f for f in snapshot_plugins if f in by_folder and not by_folder[f].get("isEnabled") ) to_disable: list = [] if enable_only else sorted( p["folderName"] for p in all_plugins if p.get("isEnabled") and p["folderName"] not in snapshot_plugins ) not_in_instance = sorted(f for f in snapshot_plugins if f not in by_folder) already_correct = max(0, len(snapshot_plugins) - len(to_enable) - len(not_in_instance)) _print_update_plan(to_enable, to_disable, already_correct) if dry_run: _print_dry_run_preview(to_enable, to_disable) if not_in_instance: print(f"\nNot found in instance ({len(not_in_instance)}):") for f in not_in_instance: print(f" - {f}") return if not skip_snapshot and (to_enable or to_disable): _auto_save_snapshot(all_plugins, include_version) errors = _apply_disable(plugins_obj, to_disable, bulk) + _apply_enable(plugins_obj, to_enable, bulk) if not to_enable and not to_disable: print("Nothing to change.") else: print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {errors}") if not_in_instance: print(f"\nNot found in instance ({len(not_in_instance)}):") for f in not_in_instance: print(f" - {f}") # Main def main() -> None: import argparse parser = argparse.ArgumentParser( prog="plugin_manager_pysisense.py", description="Manage Sisense plugin states via pySisense.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Plugin names accept the short form (SwitchDimension) or the full\n" "form (plugin-SwitchDimension) in both CLI args and config.yaml.\n" "\n" "Default behavior: enable plugins in enable_plugins, disable plugins\n" "in disable_plugins, leave everything else unchanged.\n" "Use --strict to also disable any enabled plugin not in enable_plugins.\n" "\n" "A snapshot is saved automatically before each run that\n" "modifies state. Use --skip-snapshot to suppress this." ), ) parser.add_argument( "--config", default=str(DEFAULT_CONFIG), metavar="FILE", help="Path to config.yaml (default: config.yaml next to this script)", ) parser.add_argument( "--dry-run", action="store_true", help="Preview what would change; make no API calls and save no snapshot", ) parser.add_argument( "--no-bulk", action="store_true", help=( "Send one API call per plugin instead of a single batch PATCH. " "Slower but easier to diagnose partial failures. " "Overrides sisense.bulk in config." ), ) parser.add_argument( "--skip-snapshot", action="store_true", help="Do not automatically save a snapshot before this run", ) parser.add_argument( "--log-version", action="store_true", help=( "Record each plugin's version field (from the API response) in any " "snapshot saved during this run (manual --save-snapshot or auto saved snapshot)" ), ) parser.add_argument( "--strict", action="store_true", help=( "Also disable every currently enabled plugin that is not listed in " "enable_plugins. Without this flag, only plugins in disable_plugins are " "disabled; everything else is left alone. " "Always active for --restore-snapshot." ), ) direction_group = parser.add_mutually_exclusive_group() direction_group.add_argument( "--enable-only", action="store_true", help="Run only the enable step; skip all disables", ) direction_group.add_argument( "--disable-only", action="store_true", help="Run only the disable step; skip all enables", ) mode_group = parser.add_mutually_exclusive_group() mode_group.add_argument( "--enable-one", metavar="PLUGIN", help=( "Enable exactly one plugin by name and disable all currently enabled " "others. Accepts the short or full plugin name." ), ) mode_group.add_argument( "--save-snapshot", metavar="NAME", nargs="?", const="__auto__", help=( "Save the current plugin state as a named snapshot. " "Omit NAME for an auto generated timestamp filename. " "Combine with --log-version to also record versions for each plugin." ), ) mode_group.add_argument( "--list-snapshots", action="store_true", help="List all saved snapshots with creation date and enabled plugin count", ) mode_group.add_argument( "--restore-snapshot", metavar="NAME", nargs="?", const="__latest__", help=( "Restore plugin state to exactly match a saved snapshot. " "Omit NAME to use the most recently created snapshot. " "Always strict (disables plugins not in snapshot) unless --enable-only." ), ) args = parser.parse_args() if args.list_snapshots: cmd_list_snapshots() return cfg = load_config(Path(args.config)) sisense_cfg = cfg.get("sisense", {}) bulk = sisense_cfg.get("bulk", True) and not args.no_bulk client = build_client(sisense_cfg) plugins_obj = Plugins(api_client=client) if args.enable_one: cmd_enable_one( plugins_obj, args.enable_one, dry_run=args.dry_run, bulk=bulk, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) elif args.save_snapshot is not None: name = ( datetime.now().strftime("snapshot_%Y%m%d_%H%M%S") if args.save_snapshot == "__auto__" else args.save_snapshot ) cmd_save_named_snapshot(plugins_obj, name, args.log_version) elif args.restore_snapshot is not None: name = ( latest_snapshot_name() if args.restore_snapshot == "__latest__" else args.restore_snapshot ) cmd_restore_snapshot( plugins_obj, name, dry_run=args.dry_run, bulk=bulk, enable_only=args.enable_only, disable_only=args.disable_only, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) else: enable_names = cfg.get("enable_plugins") or cfg.get("plugins", []) disable_names = cfg.get("disable_plugins", []) if not enable_names and not disable_names: print("config.yaml must include at least one entry in 'enable_plugins'.") sys.exit(1) cmd_sync_to_config( plugins_obj, enable_names, disable_names, dry_run=args.dry_run, bulk=bulk, strict=args.strict, enable_only=args.enable_only, disable_only=args.disable_only, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) if __name__ == "__main__": main() Disclaimer: This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.

    Jeremy Friedel
    Jeremy FriedelPosted 4 months ago • Last reply 4 months ago
    1
             
  • Blog banner
    • News & UpdatesChevronRightIcon

    New academy content for administrators!

                                                                                     

    New academy content for administrators! We are pleased to announce the launch of 45 ALL-NEW COURSES that are AVAILABLE NOW for Administrators! These courses will help Admins of all skill levels by expanding their knowledge, providing hands-on opportunities, and covering all of our LATEST features and best practices (approximately 6 to 7 hours of content and hands-on practice). Our new courses are based on brand-new data and assets, divided into microlearning units. They are interactive and accessible! ( yes, yes, we finally have captions! ) To get access to the new Administrators learning path, CLICK HERE and then click on the blue Get Started button to register. If you are new to the Sisense Academy, I encourage you to make an account and sign up for courses based on your role. This is just the beginning of new content releases in Sisense Academy as we are working hard behind the scenes on the next set, and we look forward to sharing more with you soon! This is a continuation of the Academy Content Refresh. During January 2025, we launched 25 ALL-NEW COURSES that are AVAILABLE NOW for Data Designers, and during March 2025, we launched 15 ALL-NEW COURSES that are AVAILABLE NOW for Dashboards Designers. I hope to see you all in the Academy!

    iyyar_sg
    iyyar_sgPosted 1 year ago
    0
             
  • Blog banner
    • Widget & Dashboard ScriptsChevronRightIcon

    Customizing the Offset Size of Selected Categories in Sisense Pie Chart Widgets

                                                                                             

    Customizing the Offset Size of Selected Categories in Sisense Pie Chart Widgets In Sisense, pie chart widgets, including the various pie chart style options such as donut charts, are a common way to visualize data. By default, when a user selects a category within a pie chart, that slice "pops out" to highlight the selection. This article explains how to customize the offset size of selected categories in Sisense pie chart widgets by leveraging the  Highcharts settings of the widget using Sisense widget scripting capabilities. Understanding the Default Behavior When a slice in a Sisense pie chart is selected, it moves outward from the center by a medium-sized offset. The size of this move is set by the slicedOffset property in the Highcharts setting object , Highcharts is the JavaScript visualizing library that Sisense uses for rendering pie charts and many other common widget types.   Default Pie Chat Offset Default Donut Pie Chart Offset The Highcharts slicedOffset Property The slicedOffset property determines the distance, in pixels, that a slice is moved when it is selected or "sliced out." By modifying this property, it is possible to increase or decrease the offset to better fit the visualization requirements. The unit is always set in raw pixels as an integer and not any other unit. Accessing and Editing the Highcharts Options Object via Sisense Widget Scripting To customize the Highcharts settings object within a Sisense widget, the beforeviewloaded event in a widget script can be used. This specific widget event is triggered immediately before Highcharts is used to begin rendering the already fetched and processed data from the Sisense data source, allowing customization of the chart options before the chart is displayed. Implementation Steps   Add a Widget Script Navigate to the pie chart widget in question. Open the widget's script editor to add a custom script. Insert the Custom Script Use the following code as a template:     // Increase or decrease the size of the offset when a slice in the pie/donut chart is selected widget.on('beforeviewloaded', function (widget, env) { // Pixels to move the selected slice when selected var slicedOffset = 50; // Set the desired offset size here // Access Highcharts options for the widget var chartOptions = env.options; // Access pie chart specific Highcharts options var pieOptions = chartOptions.plotOptions.pie; // Set the offset of the selected slice pieOptions.slicedOffset = slicedOffset; });       Customize the Offset Value Modify the slicedOffset variable to the desired number of pixels. For example, to decrease the offset size, it might be set to 3, to increase it might be set to 50. Trial and error can be used to determine the best value for the use case.   Offset Increased to 50   Offset Increased to 50 in Donut Pie Chart   Save and Refresh Save the script and refresh the dashboard. The pie chart will now reflect the new offset size when a slice is selected. Important Considerations   Highcharts Version Compatibility Sisense uses a specific version of Highcharts that may differ between Sisense versions and might not always be the latest available version of Highcharts. To ensure compatibility:   Check the Highcharts Version : In the browser's console , enter Highcharts.version to display the current Highcharts version used by Sisense.   Consult Appropriate Documentation : Use the version number to refer to the correct Highcharts documentation in case any settings have changed between versions. Viewing Current Highcharts Settings For advanced customizations, it can be helpful to view the existing explicitly defined by Sisense Highcharts options for a widget:     widget.on('beforeviewloaded', function (widget, env) { console.log('Highchart Options: ', env.options); // Logs the current Highcharts options to the console });     By printing the env.options paramater, other customizable properties within the Highcharts configuration object can be viewed. Not all possible or relevant Highchart options are by default defined by Sisense when the default value is not being modified, for example, the slicedOffset parameter is not set directly by Sisense, it did not exist in the default Sisense Highchart Setting object before being defined in a script, the Highchart documentation contains the full list of possible Highcharts settings.   Extending Customizations The scripting customization method described here isn't limited to adjusting the slicedOffset property. Any Highcharts setting available for the chart type by altering the env.options object within the beforeviewloaded event. This allows extensive visual customization of Sisense widgets. Conclusion Customizing the offset size of selected slices in Sisense pie chart widgets can enhances the interactivity and readability of pie chart visualizations. By tapping into the Highcharts settings through the beforeviewloaded event, it is possible to have granular control over the chart's behavior and appearance. Other Highcharts settings can also be modified using this script template. References Highcharts Documentation : Highcharts Reference Sisense Documentation on Widget Scripts : Sisense Widget JavaScript Sisense Academy course on Widget Plugins and Scripts: Advanced Dashboards with Plug-ins and Scripts

    Jeremy Friedel
    Jeremy FriedelPosted 1 year ago
    0
             
    • TroubleshootingChevronRightIcon

    Troubleshooting Pods in Kubernetes Clusters

                             

    Troubleshooting Pods in Kubernetes Clusters In Kubernetes, pods can encounter various issues that prevent them from running correctly. Understanding the different pod states and how to troubleshoot them is essential for maintaining a healthy cluster. This guide covers common pod states and provides steps for diagnosing and resolving issues. Common Pod States: Init : The pod is initializing. All init containers must be completed before the main containers start. 0/1 Running : The pod is running, but not all containers are in a ready state. CrashLoopBackOff : The pod repeatedly fails and is restarted. Pending : The pod is waiting to be scheduled on a node. ImagePullBackOff : The pod cannot pull the container image. Troubleshooting Steps 1. Pod in Init State: When a pod is stuck in the Init state, it indicates that one or more init containers haven't completed successfully. Check Pod Description: kubectl -n sisense describe pod <pod_name> In the description, look for the init containers section. All init containers should ideally be in a "Completed" state. If one is still running or has failed, it can block the rest of the pod's containers from starting. Example: <span>Init Containers:</span><br/><span>  init-mongodb:</span><br/><span>    State:          Running</span> This indicates an issue with the init-mongodb container. Check Logs of Init Containers: If the init container is still running or has failed, check its logs: kubectl -n sisense logs <pod_name> -c init-mongodb After identifying issues in the init container, investigate the related services or dependencies, such as the MongoDB pod itself in this example. 2. Pod in 0/1, 1/2 Running State: This state indicates that the pod is running, but not all containers are in a ready state. Describe the Pod: kubectl -n sisense describe pod <pod_name> Check the State section for each container. Look for reasons why a container is not in a ready state, such as CrashLoopBackOff , ImagePullBackOff , or other errors. Check Logs for Previous Failed Container: If a container is in an error state or has restarted, checking the logs can provide more context about the issue. Current Logs: kubectl -n sisense logs <pod_name> -c <container_name> Replace <container_name> with the name of the specific container. Previous Logs: kubectl -n sisense logs <pod_name> -c <container_name> -p This command retrieves logs from the previous instance of the container, which can be particularly useful if the container has restarted. 3. Pod in CrashLoopBackOff State: A pod enters the CrashLoopBackOff state when it repeatedly fails and is restarted. To diagnose this issue: Describe the Pod: kubectl -n sisense describe pod <pod_name> This command provides detailed information, including the events and container statuses. Example: <span>State:          Waiting</span><br/><span>  Reason:       CrashLoopBackOff</span><br/><span>Last State:     Terminated</span><br/><span>  Reason:       OOMKilled</span> The OOMKilled reason indicates that the container was killed due to exceeding its memory limit. Increase the memory limit to fix the issue. Check Events and Container States: At the bottom of the describe output, you'll find the events section, which includes messages about why the pod failed. For example, FailedScheduling may indicate resource constraints or node issues. Review Logs: Logs can provide valuable insights when a pod is in the CrashLoopBackOff state. Check Current Logs: kubectl -n sisense logs <pod_name> This command retrieves the logs from the current running container. Check Previous Logs: kubectl -n sisense logs <pod_name> -p This command retrieves logs from the previous container instance, which is useful if the container was restarted. Specify the container name if the pod has multiple containers: kubectl -n sisense logs <pod_name> -c <container_name> -p 4. Pod in Pending State: If a pod is Pending, it means it hasn't been scheduled on a node yet. Check Pod Scheduling Events: kubectl -n sisense describe pod <pod_name> Look for events like: <span>Warning  FailedScheduling  85m   default-scheduler  0/3 nodes are available: 1 node(s) didn't match Pod's node affinity/selector, 2 node(s) didn't match pod anti-affinity rules.</span> This message indicates that the scheduler couldn't find a suitable node for the pod due to resource constraints, node affinity rules, or other scheduling policies. 5. Pod in ImagePullBackOff State: This state occurs when a pod cannot pull the container image from the registry. Check Pod Description for Image Issues: kubectl -n sisense describe pod <pod_name> Look for messages indicating issues with pulling the image, such as incorrect image names, tag issues, authentication problems, or network errors. For multinode deployments, note the server in the message.  It is possible that the image may not exist on all servers. Verify Image Name and Tag: Ensure that the image name and tag are correct and that the image is available in the specified registry. Check Image Pull Secrets: If the image is in a private registry, ensure that the registry is accessible. Manually Pull the Image: Sometimes, images are not available or cannot be downloaded within the default timeout. To verify the availability of the image and check for any errors, try pulling the image manually on the node specified in the error message: docker pull <image_name>:<tag> Replace <image_name> and <tag> with the appropriate image and tag names. This can help determine if the issue is with the image itself or the registry configuration. Conclusion By understanding these common pod states and following the troubleshooting steps, you can diagnose and resolve many issues in Kubernetes. Regularly monitoring pods and logs is essential for maintaining a stable and reliable Kubernetes environment. Check out this related content:  Documentation

    Vlad Solodkyi
    Vlad SolodkyiPosted 2 years ago
    0
             
    • TroubleshootingChevronRightIcon

    Resolving Issues with Updating EC2EC Passwords in Elasticube Connections

                                                             

    Resolving Issues with Updating EC2EC Passwords in Elasticube Connections Summary This article addresses the issue of updating EC2EC (Elasticube to Elasticube) passwords in the context of employee off-boarding or password changes.  Specifically, it covers the scenario where tables do not appear in the source cube after updating the password, preventing the completion of the update. Main Content Step-by-Step Instructions to Resolve the Issue Identify the Cube Details:   Identify the specific cube you are trying to connect from and to. Verify Connection Specificity: Determine if the issue affects all EC2EC connections or only specific ones. For example, if you can update the connection to one cube (e.g., locationsv2 ) but not another (e.g., ordersv3 ), note this discrepancy. Remove and Re-add the EC2EC Table: Remove the problematic EC2EC table from the source cube. Re-add the table to the source cube. Check for Hidden Columns: Ensure that all required columns in the source Elasticube are visible. If any required columns are hidden, make them visible. Rebuild the Cube: After making the necessary columns visible. Hidden columns in the source Elasticube can often cause issues with table visibility. Always check column visibility if tables do not appear as expected.  Rebuild the cube.  Verify that the tables now appear correctly in the source cube. Feedback and Support: If the issue persists, contact support with detailed information about the error and the context of its occurrence. Providing feedback on your experience can help improve the support process.   Check out this related content:  Academy Course Sisense Documentation

    Vlad Solodkyi
    Vlad SolodkyiPosted 2 years ago
    0
             
    • TroubleshootingChevronRightIcon

    Resolving Installation Failures with "Error occurred during validate_ssl_certs section"

                                     

    Resolving Installation Failures with "Error occurred during validate_ssl_certs section" Issue: During the installation process on a Linux system, you may encounter the error message: "Error occurred during validate_ssl_certs section." This error typically occurs when there is an issue with the SSL certificate being used. Solution: To resolve this issue, you need to validate the SSL certificate and ensure it matches the corresponding private key. This can be done using the openssl command-line tool, which is widely used for managing and validating SSL certificates. NOTE: You can use this guide as a general reference for validating SSL certificates in various contexts, whether during software installations, server setups, or routine security checks. Steps to Validate the SSL Certificate: Check if the Certificate and Key Files Exist: First, ensure that both the certificate and key files are present in the specified locations. <span>CERT_PATH="/path/to/your/certificate.crt"</span><br/><br/><span>KEY_PATH="/path/to/your/private.key"</span> <span>if [[ -f "$CERT_PATH" && -f "$KEY_PATH" ]]; then</span><br/><br/><span>  echo "Certificate and key files exist."</span><br/><br/><span>else</span><br/><br/><span>  echo "Certificate or key file does not exist."</span><br/><br/><span>  exit 1</span><br/><br/><span>fi</span> Validate the SSL Certificate: Use the following command to display the details of the certificate. This helps in verifying whether the certificate is properly formatted and valid. <span>openssl x509 -in "$CERT_PATH" -text -noout</span> <span>if [[ $? -eq 0 ]]; then</span><br/><br/><span>  echo "Certificate is valid."</span><br/><br/><span>else</span><br/><br/><span>  echo "Certificate is invalid."</span><br/><br/><span>  exit 1</span><br/><br/><span>fi</span> Check if the Certificate and Key Match: It's crucial that the SSL certificate matches the private key. The following commands compare the modulus of the certificate and key to ensure they match. <span>CERT_MODULUS=$(openssl x509 -noout -modulus -in "$CERT_PATH" | openssl md5)</span><br/><br/><span>KEY_MODULUS=$(openssl rsa -noout -modulus -in "$KEY_PATH" | openssl md5)</span> <span>if [[ "$CERT_MODULUS" == "$KEY_MODULUS" ]]; then</span><br/><br/><span>  echo "Certificate and key match."</span><br/><br/><span>else</span><br/><br/><span>  echo "Certificate and key do not match."</span><br/><br/><span>  exit 1</span><br/><br/><span>fi</span> Conclusion: If the certificate and key files exist, the certificate is valid, and the certificate matches the key, then the SSL setup is likely correct. If any of these checks fail, you may need to reissue or replace the certificate and key files. This should resolve the "Error occurred during validate_ssl_certs section" error and allow your installation process to proceed. Always ensure you use valid and correctly matched SSL certificates and keys to avoid such issues.   Check out this related content: Academy Course   Sisense Documentation 

    Vlad Solodkyi
    Vlad SolodkyiPosted 2 years ago
    0
             
    • TroubleshootingChevronRightIcon

    Installation fails with /bin/activate: No such file or directory

                                     

    Installation fails with /bin/activate: No such file or directory Sisense installation may fail in the very beginning with the following symptoms: [2024-06-18 18:07:02] Preparing System ...<br/><br/>[2024-06-18 18:07:02] Linux user: sistemas<br/><br/>[2024-06-18 18:07:02] Validating Sudo permissions for user sistemas ...<br/><br/>[2024-06-18 18:07:02] User sistemas has sufficient sudo permissions<br/><br/>[2024-06-18 18:07:02] Detecting Host OS  ...<br/><br/>[2024-06-18 18:07:02] OS: Ubuntu, Version: 23.04<br/><br/>[2024-06-18 18:07:02] Validating OS and its version<br/><br/>[2024-06-18 18:07:03] Validating that namespace name is all lower case ...<br/><br/>[2024-06-18 18:07:03] detected lock on dpkg, releasing it<br/><br/>Enter the password for user sisense and press [ENTER]:<br/><br/>Using password for Ansible Installation ...<br/><br/>[2024-06-18 18:07:15] Verifying Python packages exist ...<br/><br/>installer/01_general_preparation/functions.sh: line 615: sisense-installer/bin/activate: No such file or directory<br/><br/>[2024-06-18 18:08:39] ** Error occurred during generic_pre_install section **<br/><br/>[2024-06-18 18:08:39] Exiting Installation ... The issue is related to the wrong Python packages or not being installed. To resolve the issue it's needed to run the verification commands from the Minimum System Requirements:  https://docs.sisense.com/main/SisenseLinux/linux-minimum-requirements.htm#DefaultPackagesRepository <span>For Ubuntu:<br/></span><span>sudo apt-get update</span><br/><br/><span>sudo apt-get install python3 netcat sshpass python3-apt python3-pip dbus</span><br/><br/><span>sudo apt-get install jq</span><br/><br/><span>sudo python3 -m pip install --upgrade --force-reinstall pip</span><br/><br/><span>sudo python3 -m pip install configparser zipp</span><br/><br/><span>sudo python3 -m pip install -r kubespray/requirements.txt --ignore-installed (from the installation folder)</span> Check out this related content:  Academy Course Sisense Documentation

    Vlad Solodkyi
    Vlad SolodkyiPosted 2 years ago
    0
             
    • Sisense AdministrationChevronRightIcon

    How to Check SSL Ciphers

                                                     

    How to Check SSL Ciphers If you have enabled SSL on Sisense side, the Nginx controller will be deployed in the default namespace. To check the currently configured ciphers run the following command and check the "nginx.ingress.kubernetes.io/ssl-ciphers:" row:  <em>kubectl -n sisense describe ingress</em> <span>Name:             sisense-ingress</span><br/><br/><span>Labels:           app=api-gateway</span><br/><br/><span>                  app.kubernetes.io/managed-by=Helm</span><br/><br/><span>                  chart=api-gateway-2024.2.077</span><br/><br/><span>                  release=sisense</span><br/><br/><span>                  sisense-version=2024.2.077</span><br/><br/><span>Namespace:        sisense</span><br/><br/><span>Address:</span><br/><br/><span>Ingress Class:    <none></span><br/><br/><span>Default backend:  <default></span><br/><br/><span>TLS:</span><br/><br/><span>  sisense-tls terminates</span><br/><br/><span>Rules:</span><br/><br/><span>  Host                         Path  Backends</span><br/><br/><span>  ----                         ----  --------</span><br/><br/><span>  paragoninsgroup.sisense.com</span><br/><br/><span>                               /   api-gateway-external:8456 (10.42.140.227:8456)</span><br/><br/><span>Annotations:                   kubernetes.io/ingress.class: nginx</span><br/><br/><span>                               kubernetes.io/tls-acme: true</span><br/><br/><span>                               meta.helm.sh/release-name: sisense</span><br/><br/><span>                               meta.helm.sh/release-namespace: sisense</span><br/><br/><span>                               nginx.ingress.kubernetes.io/configuration-snippet: more_clear_headers Server;</span><br/><br/><span>                               nginx.ingress.kubernetes.io/proxy-body-size: 0m</span><br/><br/><span>                               nginx.ingress.kubernetes.io/proxy-read-timeout: 300</span><br/><br/><span>                               nginx.ingress.kubernetes.io/ssl-ciphers:</span><br/><br/><span>                                 </span><strong>ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS:!AESCCM</strong><br/><br/><span>                               nginx.ingress.kubernetes.io/ssl-prefer-server-ciphers: true</span> To decrypt the full list of the currently used ciphers use the string from the mentioned row with the following command: <span>openssl ciphers -v 'ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS:!AESCCM' | column -t</span>   Output Example: <span>ECDHE-RSA-AES256-GCM-SHA384  TLSv1.2 Kx=ECDH    Au=RSA  Enc=AESGCM(256) Mac=AEAD</span><br/><br/><span>ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH    Au=ECDSA Enc=AESGCM(256) Mac=AEAD</span><br/><br/><span>ECDH-RSA-AES256-GCM-SHA384   TLSv1.2 Kx=ECDH/RSA  Au=ECDH  Enc=AESGCM(256) Mac=AEAD</span><br/><br/><span>ECDH-ECDSA-AES256-GCM-SHA384  TLSv1.2 Kx=ECDH/ECDSA Au=ECDH  Enc=AESGCM(256) Mac=AEAD</span><br/><br/><span>ECDHE-RSA-AES128-GCM-SHA256  TLSv1.2 Kx=ECDH    Au=RSA  Enc=AESGCM(128) Mac=AEAD</span><br/><br/><span>ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH    Au=ECDSA Enc=AESGCM(128) Mac=AEAD</span><br/><br/><span>ECDH-RSA-AES128-GCM-SHA256   TLSv1.2 Kx=ECDH/RSA  Au=ECDH  Enc=AESGCM(128) Mac=AEAD</span><br/><br/><span>ECDH-ECDSA-AES128-GCM-SHA256  TLSv1.2 Kx=ECDH/ECDSA Au=ECDH  Enc=AESGCM(128) Mac=AEAD</span><br/><br/><span>DH-DSS-AES256-GCM-SHA384    TLSv1.2 Kx=DH/DSS   Au=DH   Enc=AESGCM(256) Mac=AEAD</span><br/><br/><span>DH-RSA-AES256-GCM-SHA384    TLSv1.2 Kx=DH/RSA   Au=DH   Enc=AESGCM(256) Mac=AEAD</span><br/><br/><span>DHE-RSA-AES256-GCM-SHA384   TLSv1.2 Kx=DH     Au=RSA  Enc=AESGCM(256) Mac=AEAD</span><br/><br/><span>DH-DSS-AES128-GCM-SHA256    TLSv1.2 Kx=DH/DSS   Au=DH   Enc=AESGCM(128) Mac=AEAD</span><br/><br/><span>DH-RSA-AES128-GCM-SHA256    TLSv1.2 Kx=DH/RSA   Au=DH   Enc=AESGCM(128) Mac=AEAD</span><br/><br/><span>DHE-RSA-AES128-GCM-SHA256   TLSv1.2 Kx=DH     Au=RSA  Enc=AESGCM(128) Mac=AEAD</span><br/><br/><span>ECDHE-RSA-AES256-SHA384    TLSv1.2 Kx=ECDH    Au=RSA  Enc=AES(256)   Mac=SHA384</span><br/><br/><span>ECDHE-ECDSA-AES256-SHA384   TLSv1.2 Kx=ECDH    Au=ECDSA Enc=AES(256)   Mac=SHA384</span><br/><br/><span>ECDHE-RSA-AES256-SHA      SSLv3  Kx=ECDH    Au=RSA  Enc=AES(256)   Mac=SHA1</span><br/><br/><span>ECDHE-ECDSA-AES256-SHA     SSLv3  Kx=ECDH    Au=ECDSA Enc=AES(256)   Mac=SHA1</span><br/><br/><span>ECDH-RSA-AES256-SHA384     TLSv1.2 Kx=ECDH/RSA  Au=ECDH  Enc=AES(256)   Mac=SHA384</span><br/><br/><span>ECDH-ECDSA-AES256-SHA384    TLSv1.2 Kx=ECDH/ECDSA Au=ECDH  Enc=AES(256)   Mac=SHA384</span><br/><br/><span>ECDH-RSA-AES256-SHA      SSLv3  Kx=ECDH/RSA  Au=ECDH  Enc=AES(256)   Mac=SHA1</span><br/><br/><span>ECDH-ECDSA-AES256-SHA     SSLv3  Kx=ECDH/ECDSA Au=ECDH  Enc=AES(256)   Mac=SHA1</span><br/><br/><span>DHE-RSA-AES256-SHA256     TLSv1.2 Kx=DH     Au=RSA  Enc=AES(256)   Mac=SHA256</span><br/><br/><span>DH-RSA-AES256-SHA256      TLSv1.2 Kx=DH/RSA   Au=DH   Enc=AES(256)   Mac=SHA256</span><br/><br/><span>DH-DSS-AES256-SHA256      TLSv1.2 Kx=DH/DSS   Au=DH   Enc=AES(256)   Mac=SHA256</span><br/><br/><span>DHE-RSA-AES256-SHA       SSLv3  Kx=DH     Au=RSA  Enc=AES(256)   Mac=SHA1</span><br/><br/><span>DH-RSA-AES256-SHA       SSLv3  Kx=DH/RSA   Au=DH   Enc=AES(256)   Mac=SHA1</span><br/><br/><span>DH-DSS-AES256-SHA       SSLv3  Kx=DH/DSS   Au=DH   Enc=AES(256)   Mac=SHA1</span><br/><br/><span>ECDHE-RSA-AES128-SHA256    TLSv1.2 Kx=ECDH    Au=RSA  Enc=AES(128)   Mac=SHA256</span><br/><br/><span>ECDHE-ECDSA-AES128-SHA256   TLSv1.2 Kx=ECDH    Au=ECDSA Enc=AES(128)   Mac=SHA256</span><br/><br/><span>ECDHE-RSA-AES128-SHA      SSLv3  Kx=ECDH    Au=RSA  Enc=AES(128)   Mac=SHA1</span><br/><br/><span>ECDHE-ECDSA-AES128-SHA     SSLv3  Kx=ECDH    Au=ECDSA Enc=AES(128)   Mac=SHA1</span><br/><br/><span>ECDH-RSA-AES128-SHA256     TLSv1.2 Kx=ECDH/RSA  Au=ECDH  Enc=AES(128)   Mac=SHA256</span><br/><br/><span>ECDH-ECDSA-AES128-SHA256    TLSv1.2 Kx=ECDH/ECDSA Au=ECDH  Enc=AES(128)   Mac=SHA256</span><br/><br/><span>ECDH-RSA-AES128-SHA      SSLv3  Kx=ECDH/RSA  Au=ECDH  Enc=AES(128)   Mac=SHA1</span><br/><br/><span>ECDH-ECDSA-AES128-SHA     SSLv3  Kx=ECDH/ECDSA Au=ECDH  Enc=AES(128)   Mac=SHA1</span><br/><br/><span>DHE-RSA-AES128-SHA256     TLSv1.2 Kx=DH     Au=RSA  Enc=AES(128)   Mac=SHA256</span><br/><br/><span>DH-RSA-AES128-SHA256      TLSv1.2 Kx=DH/RSA   Au=DH   Enc=AES(128)   Mac=SHA256</span><br/><br/><span>DH-DSS-AES128-SHA256      TLSv1.2 Kx=DH/DSS   Au=DH   Enc=AES(128)   Mac=SHA256</span><br/><br/><span>DHE-RSA-AES128-SHA       SSLv3  Kx=DH     Au=RSA  Enc=AES(128)   Mac=SHA1</span><br/><br/><span>DH-RSA-AES128-SHA       SSLv3  Kx=DH/RSA   Au=DH   Enc=AES(128)   Mac=SHA1</span><br/><br/><span>DH-DSS-AES128-SHA       SSLv3  Kx=DH/DSS   Au=DH   Enc=AES(128)   Mac=SHA1</span><br/><br/><span>AES256-GCM-SHA384       TLSv1.2 Kx=RSA     Au=RSA  Enc=AESGCM(256) Mac=AEAD</span><br/><br/><span>AES128-GCM-SHA256       TLSv1.2 Kx=RSA     Au=RSA  Enc=AESGCM(128) Mac=AEAD</span><br/><br/><span>AES256-SHA256         TLSv1.2 Kx=RSA     Au=RSA  Enc=AES(256)   Mac=SHA256</span><br/><br/><span>AES256-SHA           SSLv3  Kx=RSA     Au=RSA  Enc=AES(256)   Mac=SHA1</span><br/><br/><span>AES128-SHA256         TLSv1.2 Kx=RSA     Au=RSA  Enc=AES(128)   Mac=SHA256</span><br/><br/><span>AES128-SHA           SSLv3  Kx=RSA     Au=RSA  Enc=AES(128)   Mac=SHA1</span> Check out this related content: Academy course Sisense Documentation      

    Vlad Solodkyi
    Vlad SolodkyiPosted 2 years ago
    0