cid-cmd 4.2.6__tar.gz → 4.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. {cid_cmd-4.2.6/cid_cmd.egg-info → cid_cmd-4.3.0}/PKG-INFO +1 -1
  2. cid_cmd-4.3.0/cid/_version.py +2 -0
  3. cid_cmd-4.3.0/cid/builtin/core/data/permissions/data_set_rls_permissions.json +8 -0
  4. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/cli.py +4 -1
  5. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/common.py +70 -10
  6. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/export.py +1 -1
  7. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/athena.py +7 -4
  8. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/cur.py +1 -1
  9. cid_cmd-4.3.0/cid/helpers/parameter_store.py +88 -0
  10. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/quicksight/__init__.py +5 -2
  11. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/quicksight/dashboard.py +3 -1
  12. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/quicksight/dashboard_patching.py +18 -0
  13. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/quicksight/dataset.py +29 -8
  14. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/utils.py +1 -1
  15. {cid_cmd-4.2.6 → cid_cmd-4.3.0/cid_cmd.egg-info}/PKG-INFO +1 -1
  16. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid_cmd.egg-info/SOURCES.txt +1 -0
  17. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid_cmd.egg-info/top_level.txt +2 -1
  18. cid_cmd-4.2.6/cid/_version.py +0 -2
  19. cid_cmd-4.2.6/cid/helpers/parameter_store.py +0 -51
  20. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/LICENSE +0 -0
  21. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/MANIFEST.in +0 -0
  22. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/README.md +0 -0
  23. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/__init__.py +0 -0
  24. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/base.py +0 -0
  25. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/__init__.py +0 -0
  26. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/__init__.py +0 -0
  27. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/cid/compute.json +0 -0
  28. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/cid/ec2_running_cost.json +0 -0
  29. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/cid/s3_view.json +0 -0
  30. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/cid/summary_view.json +0 -0
  31. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/co/dataset.json +0 -0
  32. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/cudos/hourly_view.json +0 -0
  33. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/cudos/resource_view.json +0 -0
  34. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/kpi/kpi_ebs_snap.json +0 -0
  35. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/kpi/kpi_ebs_storage_all.json +0 -0
  36. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/kpi/kpi_instance_all.json +0 -0
  37. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/kpi/kpi_s3_storage_all.json +0 -0
  38. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/kpi/kpi_tracker.json +0 -0
  39. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/shared/customer_all.json +0 -0
  40. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/tao/dataset.json +0 -0
  41. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/trends/daily_anomaly_detection.json +0 -0
  42. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/trends/monthly_anomaly_detection.json +0 -0
  43. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/datasets/trends/monthly_bill_by_account.json +0 -0
  44. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/permissions/dashboard_permissions.json +0 -0
  45. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/permissions/dashboard_permissions_namespace.json +0 -0
  46. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/permissions/data_set_permissions.json +0 -0
  47. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/permissions/data_source_permissions.json +0 -0
  48. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/permissions/folder_permissions.json +0 -0
  49. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/cid/compute_savings_plan_eligible_spend.sql +0 -0
  50. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/cid/ec2_running_cost.sql +0 -0
  51. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/cid/ri_sp_mapping.sql +0 -0
  52. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/cid/s3.sql +0 -0
  53. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/cid/summary_view.sql +0 -0
  54. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/all_options.sql +0 -0
  55. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/auto_scale.json +0 -0
  56. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/auto_scale_options.sql +0 -0
  57. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/ebs_volume.json +0 -0
  58. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/ebs_volume_options.sql +0 -0
  59. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/ec2_instance.json +0 -0
  60. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/ec2_instance_options.sql +0 -0
  61. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/ecs_service.json +0 -0
  62. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/ecs_service_options.sql +0 -0
  63. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/idle.json +0 -0
  64. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/idle_options.sql +0 -0
  65. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/lambda.json +0 -0
  66. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/lambda_options.sql +0 -0
  67. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/license.json +0 -0
  68. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/license_options.sql +0 -0
  69. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/rds_database.json +0 -0
  70. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/rds_instance_options.sql +0 -0
  71. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/co/rds_storage_options.sql +0 -0
  72. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/cudos/hourly_view.sql +0 -0
  73. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/cudos/resource_view.sql +0 -0
  74. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/kpi/first_kpi_instance_mapping_view.sql +0 -0
  75. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/kpi/kpi_ebs_snap_view.sql +0 -0
  76. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/kpi/kpi_ebs_storage_view.sql +0 -0
  77. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/kpi/kpi_instance_all_view.sql +0 -0
  78. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/kpi/kpi_s3_storage_view.sql +0 -0
  79. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/kpi/last_kpi_tracker_view.sql +0 -0
  80. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/shared/account_map.sql +0 -0
  81. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/shared/account_map_cur2.sql +0 -0
  82. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/shared/account_map_dummy.sql +0 -0
  83. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/shared/aws_accounts.sql +0 -0
  84. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/shared/aws_regions.sql +0 -0
  85. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/shared/aws_service_category_map.sql +0 -0
  86. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/shared/business_units_map.sql +0 -0
  87. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/shared/cur.yaml +0 -0
  88. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/shared/customer_all_unlimited.sql +0 -0
  89. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/shared/payer_account_name_map.sql +0 -0
  90. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/shared/ta_descriptions.sql +0 -0
  91. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/tao/glue_table.json +0 -0
  92. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/tao/ta_org_view.sql +0 -0
  93. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/trends/daily_anomaly_detection.sql +0 -0
  94. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/trends/monthly_anomaly_detection.sql +0 -0
  95. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/queries/trends/monthly_bill_by_account.sql +0 -0
  96. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/builtin/core/data/resources.yaml +0 -0
  97. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/commands/__init__.py +0 -0
  98. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/commands/command_base.py +0 -0
  99. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/commands/init_qs.py +0 -0
  100. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/exceptions.py +0 -0
  101. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/__init__.py +0 -0
  102. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/account_map.py +0 -0
  103. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/cloudformation.py +0 -0
  104. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/csv2view.py +0 -0
  105. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/cur_proxy.py +0 -0
  106. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/diff.py +0 -0
  107. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/glue.py +0 -0
  108. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/iam.py +0 -0
  109. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/organizations.py +0 -0
  110. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/quicksight/datasource.py +0 -0
  111. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/quicksight/definition.py +0 -0
  112. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/quicksight/resource.py +0 -0
  113. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/quicksight/template.py +0 -0
  114. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/quicksight/version.py +0 -0
  115. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/randtime.py +0 -0
  116. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/s3.py +0 -0
  117. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/helpers/timezone.py +0 -0
  118. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/logger.py +0 -0
  119. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/plugin.py +0 -0
  120. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/test/python/test_csv2view.py +0 -0
  121. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/test/python/test_isolated_parameters.py +0 -0
  122. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid/test/python/test_merge.py +0 -0
  123. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid_cmd.egg-info/dependency_links.txt +0 -0
  124. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid_cmd.egg-info/entry_points.txt +0 -0
  125. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/cid_cmd.egg-info/requires.txt +0 -0
  126. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/pyproject.toml +0 -0
  127. {cid_cmd-4.2.6 → cid_cmd-4.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cid-cmd
3
- Version: 4.2.6
3
+ Version: 4.3.0
4
4
  Summary: Cloud Intelligence Dashboards deployment helper tool
5
5
  Home-page: https://github.com/aws-samples/aws-cudos-framework-deployment
6
6
  Author: AWS CUDOS Team
@@ -0,0 +1,2 @@
1
+ __version__ = '4.3.0'
2
+
@@ -0,0 +1,8 @@
1
+ {
2
+ "RowLevelPermissionDataSet": {
3
+ "Arn": "${RlsDataSetArn}",
4
+ "FormatVersion": "VERSION_1",
5
+ "PermissionPolicy": "GRANT_ACCESS",
6
+ "Status": "${RlsStatus}"
7
+ }
8
+ }
@@ -94,7 +94,6 @@ def map(ctx, **kwargs):
94
94
  """
95
95
  ctx.obj.map(**kwargs)
96
96
 
97
-
98
97
  @click.option('-v', '--verbose', count=True)
99
98
  @click.option('-y', '--yes', help='confirm all', is_flag=True, default=False)
100
99
  @cid_command
@@ -141,6 +140,8 @@ def deploy(ctx, **kwargs):
141
140
  --catalog TEXT Comma separated list of catalog files or urls (ex: foundational,advanced )
142
141
  --theme TEXT A QuickSight Theme (CLASSIC|MIDNIGHT|SEASIDE|RAINIER)
143
142
  --currency TEXT A currency symbol instead of default USD (USD|GBP|EUR|JPY|KRW|DKK|TWD|INR)
143
+ --rls TEXT Desired RLS status (CLEAR|ENABLED|DISABLED) Clear = No RLS configuration.
144
+ --rls-dataset-id TEXT ID of RLS Dataset that will be used
144
145
  --share-with-account (yes|no) Make dashboard visible to other users in the same account by default.
145
146
  """
146
147
  ctx.obj.deploy(**kwargs)
@@ -210,6 +211,8 @@ def update(ctx, dashboard_id, force, recursive, **kwargs):
210
211
  --on-drift (show|override) Action if a drift of view and dataset is discovered. 'override' = override drift(will destroy customization) or 'show' (default) = show a diff. In Unattended mode (without terminal on-drift will have allways override behaviour)
211
212
  --theme TEXT A QuickSight Theme (CLASSIC|MIDNIGHT|SEASIDE|RAINIER)
212
213
  --currency TEXT A currency symbol instead of default USD (USD|GBP|EUR|JPY|KRW|DKK|TWD|INR)
214
+ --rls TEXT Desired RLS status (CLEAR|ENABLED|DISABLED) Clear = No RLS configuration.
215
+ --rls-dataset-id TEXT ID of RLS Dataset that will be used
213
216
 
214
217
  """
215
218
  ctx.obj.update(dashboard_id, force=force, recursive=recursive, **kwargs)
@@ -282,16 +282,22 @@ class Cid():
282
282
  export_analysis(self.qs, self.athena, glue=self.glue)
283
283
 
284
284
  def track(self, action, dashboard_id):
285
- """ Send dashboard_id and account_id to adoption tracker """
285
+ """ Send dashboard_id and account_id to CID adoption tracker """
286
286
  method = {'created':'PUT', 'updated':'PATCH', 'deleted': 'DELETE'}.get(action, None)
287
287
  if not method:
288
288
  logger.debug(f"This will not fail the deployment. Logging action {action} is not supported. This issue will be ignored")
289
289
  return
290
- endpoint = 'https://okakvoavfg.execute-api.eu-west-1.amazonaws.com/'
290
+ endpoint = 'https://okakvoavfg.execute-api.eu-west-1.amazonaws.com/' # AWS Managed
291
+ if os.environ.get('AWS_DEPLOYMENT_TYPE'):
292
+ deployment_type = os.environ.get('AWS_DEPLOYMENT_TYPE')
293
+ elif os.environ.get('AWS_EXECUTION_ENV', '').startswith('AWS_Lambda'):
294
+ deployment_type = 'Lambda'
295
+ else:
296
+ deployment_type = 'CID'
291
297
  payload = {
292
298
  'dashboard_id': dashboard_id,
293
299
  'account_id': self.base.account_id,
294
- action + '_via': 'Lambda' if os.environ.get('AWS_EXECUTION_ENV', '').startswith('AWS_Lambda') else 'CID',
300
+ action + '_via': deployment_type,
295
301
  }
296
302
  try:
297
303
  res = requests.request(
@@ -470,17 +476,16 @@ class Cid():
470
476
  set_defaults(defaults)
471
477
 
472
478
  def dump_default_parameters(self):
473
-
474
479
  stop_list = ['profile-name', 'region', 'aws-access-key-id', 'aws-secret-access-key', 'aws-session-token', 'athena-database', 'athena-workgroup']
475
- current_defaults = self.parameters_controller.load_parameters(
476
- context=get_parameters().get('dashboard-id')
477
- )
478
- current_defaults = current_defaults | get_parameters()
480
+ current_defaults = get_parameters()
479
481
  for key in list(current_defaults.keys()):
480
482
  if key in stop_list:
481
483
  del current_defaults[key]
482
484
  logger.trace(f'dumping parameters {current_defaults}')
483
- self.parameters_controller.dump_parameters(current_defaults, context=get_parameters().get('dashboard-id'))
485
+ self.parameters_controller.dump_parameters(
486
+ current_defaults,
487
+ context=get_parameters().get('dashboard-id')
488
+ )
484
489
 
485
490
 
486
491
  def ensure_subscription(self):
@@ -1286,7 +1291,18 @@ class Cid():
1286
1291
  # If there still datasets missing try automatic creation
1287
1292
  if missing_datasets:
1288
1293
  missing_str = ', '.join(missing_datasets)
1289
- print(f'\nThere are still {len(missing_datasets)} datasets missing: {missing_str}')
1294
+ cid_print(f'\nThere are still {len(missing_datasets)} datasets missing: {missing_str}')
1295
+
1296
+ # get rls status of existing datasets
1297
+ found_dataset_objects = [self.qs.describe_dataset(ds_id) for ds_id in found_datasets]
1298
+ rls_dataset_arns = [ds.rls_arn for ds in found_dataset_objects if ds.rls_arn]
1299
+ if rls_dataset_arns:
1300
+ rls_dataset_arn = max(set(rls_dataset_arns), key=rls_dataset_arns.count) #get the most frequent
1301
+ if not get_parameters().get('rls-dataset-id') and not get_parameters().get('rls'):
1302
+ rls_dataset_id = rls_dataset_arn.split('/')[-1]
1303
+ cid_print('Existing datasets are linked to RLS {rls_dataset_id}. We will reuse it for {missing_str} datasets.')
1304
+ set_parameters({'rls-dataset-id': rls_dataset_id})
1305
+
1290
1306
  for dataset_name in missing_datasets[:]:
1291
1307
  dataset_id = known_datasets.get(dataset_name)
1292
1308
  print(f'Creating dataset: {dataset_name}')
@@ -1628,6 +1644,50 @@ class Cid():
1628
1644
  compiled_dataset = Dataset.patch(dataset=compiled_dataset, custom_fields=custom_fields, athena=self.athena)
1629
1645
  logger.trace(f"compiled_dataset = {json.dumps(compiled_dataset)}")
1630
1646
  found_dataset = self.qs.describe_dataset(compiled_dataset.get('DataSetId'), timeout=0)
1647
+
1648
+ rls_dataset_id = get_parameters().get('rls-dataset-id')
1649
+ rls_dataset_status = get_parameters().get('rls')
1650
+ if rls_dataset_status == 'CLEAR':
1651
+ logger.debug('deleting rls')
1652
+ if "RowLevelPermissionDataSet" in compiled_dataset:
1653
+ del compiled_dataset["RowLevelPermissionDataSet"]
1654
+ if "RowLevelPermissionDataSet" in found_dataset:
1655
+ del found_dataset["RowLevelPermissionDataSet"]
1656
+
1657
+ if rls_dataset_id or rls_dataset_status:
1658
+ if not rls_dataset_id:
1659
+ for ds in self.qs.list_data_sets():
1660
+ print(ds)
1661
+ choices = {
1662
+ f"{ds.get('Name')}({ds.get('DataSetId')})":ds.get('DataSetId')
1663
+ for ds in self.qs.list_data_sets()
1664
+ if ds.get('UseAs') == 'RLS_RULES'
1665
+ }
1666
+ if not choices:
1667
+ raise CidCritical(f"Cannot find RLS DataSets")
1668
+ rls_dataset_id = get_parameter('rls-dataset-id', message='Select RLS dataset', choices=choices)
1669
+ rls_dataset = self.qs.describe_dataset(id=rls_dataset_id)
1670
+ if not rls_dataset:
1671
+ raise CidCritical(f"RLS DataSet {rls_dataset_id} not found")
1672
+ if not rls_dataset.is_rls:
1673
+ raise CidCritical(f"DataSet {rls_dataset_id} is not RLS")
1674
+ rls_permissions = {
1675
+ "Arn": rls_dataset.arn,
1676
+ "Status": (rls_dataset_status
1677
+ or (found_dataset and found_dataset.rls_status)
1678
+ or "ENABLED"
1679
+ ),
1680
+ "PermissionPolicy": "GRANT_ACCESS",
1681
+ }
1682
+ cols = [c['Name'] for c in rls_dataset.columns]
1683
+ if 'UserName' in cols or 'GroupName' in cols:
1684
+ rls_permissions['FormatVersion'] = "VERSION_1"
1685
+ elif 'UserARN' in cols or 'GroupARN' in cols:
1686
+ rls_permissions['FormatVersion'] = "VERSION_2"
1687
+ else:
1688
+ raise CidCritical(f"DataSet {rls_dataset_id} must have 'UserName'/'GroupName' or 'UserARN'/'GroupARN' columns.")
1689
+ compiled_dataset["RowLevelPermissionDataSet"] = rls_permissions
1690
+
1631
1691
  if isinstance(found_dataset, Dataset):
1632
1692
  update_dataset = False
1633
1693
  if update:
@@ -362,7 +362,7 @@ def export_analysis(qs, athena, glue):
362
362
  # We can potentially reconsider this and use IDs at some point
363
363
  'datasets': sorted(list(set(list(datasets.keys()) + resources_datasets)))
364
364
  }
365
- dashboard_resource['name'] = dashboard_resource.get('name')
365
+ dashboard_resource['name'] = dashboard_resource.get('name') or analysis['Name']
366
366
  dashboard_resource['dashboardId'] = dashboard_id
367
367
  dashboard_resource['category'] = get_parameters().get('category', dashboard_resource.get('category', 'Custom'))
368
368
 
@@ -601,7 +601,7 @@ class Athena(CidBase):
601
601
  ''')
602
602
 
603
603
  def find_tables_with_columns(self, columns: list, database_name: str=None, catalog_name: str=None, max_items: int=10000):
604
- """ This function searches a table with a given set of columns
604
+ """ Returns an iterator that yields only tables containing all specified columns.
605
605
  """
606
606
  iterator = self.client.get_paginator('list_table_metadata').paginate(
607
607
  DatabaseName=database_name or self.DatabaseName,
@@ -610,6 +610,9 @@ class Athena(CidBase):
610
610
  'MaxItems': max_items, # sometimes customers can have 1'000s of tables (due to a crawler going crazy for example)
611
611
  },
612
612
  )
613
- return iterator.search(f"""
614
- TableMetadataList[?{' && '.join(["contains(Columns[].Name, '"+col+"' )" for col in columns])}].Name
615
- """)
613
+ # We cannot rely on search to find directly columns as there might be Nulls. So iterating old fashion.
614
+ for table in iterator.search('TableMetadataList'):
615
+ column_names = [c['Name'] for c in table.get('Columns', [])]
616
+ if all([(col in column_names) for col in columns]):
617
+ yield table
618
+
@@ -271,7 +271,7 @@ class CUR(AbstractCUR):
271
271
  columns=self.cur_minimal_required_columns,
272
272
  database_name=database,
273
273
  )
274
- all_cur_tables += [(database, table) for table in tables]
274
+ all_cur_tables += [(database, table['Name']) for table in tables]
275
275
  except self.athena.client.exceptions.ClientError as exc:
276
276
  if 'AccessDenied' in str(exc):
277
277
  logger.info(f'Cannot read from athena database {database}')
@@ -0,0 +1,88 @@
1
+ import logging
2
+ from datetime import datetime
3
+
4
+ from cid.exceptions import CidCritical
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class AthenaStore():
9
+ def __init__(self, athena, view_name='cid_parameters'):
10
+ self.athena = athena
11
+ self.view_name = view_name
12
+
13
+ def dump(self, data):
14
+ ''' dump data to athena
15
+ '''
16
+ try:
17
+ # FIXME: make it multi view
18
+ self.athena.query(self._generate_view_query(data, self.view_name))
19
+ except (CidCritical, Exception) as exc:
20
+ logger.debug(f'failed to save parameters store: {exc}')
21
+
22
+ def load(self):
23
+ ''' load from athena
24
+ '''
25
+ try:
26
+ res = self.athena.query(f'''select * from {self.view_name}''', include_header=True)
27
+ except CidCritical as exc:
28
+ if 'TABLE_NOT_FOUND' in str(exc):
29
+ res = []
30
+ else:
31
+ raise
32
+ return [{k:v for k, v in zip(res[0], row)} for row in res[1:]]
33
+
34
+ def _to_sql_str(self, val):
35
+ if val is None:
36
+ return "''"
37
+ return "'" + str(val).replace("'", "''") + "'"
38
+
39
+ def _generate_view_query(self, data, name):
40
+ all_keys = {key for dictionary in data for key in dictionary.keys()}
41
+ lines = ',\n '.join([f'''ROW({','.join([self._to_sql_str(line.get(k)) for k in all_keys])})''' for line in data])
42
+ query = f"""
43
+ CREATE OR REPLACE VIEW {name} AS
44
+ SELECT *
45
+ FROM (
46
+ VALUES
47
+ {lines}
48
+ ) ignored_table_name ({','.join([key for key in all_keys])})
49
+ """
50
+ return query
51
+
52
+ class ParametersController(AthenaStore):
53
+ def load_parameters(self, context):
54
+ data = self.load()
55
+ context_parameters = {}
56
+ any_parameters = {}
57
+ # get any context and then override with specific context
58
+ for line in sorted(data, key=lambda x: x.get('date', '')): # latest should override
59
+ any_parameters[line.get('parameter')] = line.get('value')
60
+ if line.get('context') == str(context):
61
+ context_parameters[line.get('parameter')] = line.get('value')
62
+ return any_parameters | context_parameters
63
+
64
+ def dump_parameters(self, params, context=None):
65
+ data = self.load()
66
+ logger.trace(f'loaded parameters {data}')
67
+ params = dict(params)
68
+ date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
69
+
70
+ #Update parameters if they are present already
71
+ for line in data:
72
+ for key, val in list(params.items()):
73
+ if line.get('context') == str(context) and line.get('parameter') == key:
74
+ line['value'] = val
75
+ line['date'] = date
76
+ del params[key]
77
+
78
+ #add parameters that are new
79
+ for key, val in list(params.items()):
80
+ data.append({
81
+ 'parameter': key,
82
+ 'value': ','.join([str(v) for v in val]) if isinstance(val, list) else str(val),
83
+ 'context': str(context),
84
+ 'date': date
85
+ })
86
+
87
+ logger.trace(f'dumping parameters {data}')
88
+ self.dump(data)
@@ -14,7 +14,7 @@ from cid.base import CidBase
14
14
  from cid.helpers import diff, timezone, randtime
15
15
  from cid.helpers.quicksight.dashboard import Dashboard
16
16
  from cid.helpers.quicksight.dataset import Dataset
17
- from cid.helpers.quicksight.dashboard_patching import add_filter_to_dashboard_definition, patch_currency, patch_group_by
17
+ from cid.helpers.quicksight.dashboard_patching import add_filter_to_dashboard_definition, patch_currency, patch_group_by, patch_spaces
18
18
  from cid.helpers.quicksight.datasource import Datasource
19
19
  from cid.helpers.quicksight.template import Template as CidQsTemplate
20
20
  from cid.helpers.quicksight.definition import Definition as CidQsDefinition
@@ -735,6 +735,7 @@ class QuickSight(CidBase):
735
735
  'AwsAccountId': self.account_id,
736
736
  'DashboardId': dashboard_id
737
737
  }
738
+ logger.debug(f'describe_dashboard_definition({dashboard_id})')
738
739
  result = self.client.describe_dashboard_definition(**parameters)
739
740
  self._definitions.update({f'{self.account_id}:{self.identityRegion}:{dashboard_id}': CidQsDefinition(result.get('Definition'))})
740
741
  logger.debug(result)
@@ -1289,6 +1290,7 @@ class QuickSight(CidBase):
1289
1290
  'DashboardId': definition.get('dashboardId'),
1290
1291
  'VersionNumber': created_version
1291
1292
  }
1293
+ logger.debug(f"describe_dashboard { definition.get('dashboardId')}")
1292
1294
  dashboard = self.describe_dashboard(poll=True, **describe_parameters)
1293
1295
  logger.debug('dashboard={dashboard}')
1294
1296
  self.set_tags(dashboard.arn, cid_version_tag=dashboard.cid_version) # try to update version tag
@@ -1306,7 +1308,7 @@ class QuickSight(CidBase):
1306
1308
  definition: cid dashboard definition. NOT QS DEFINITION.
1307
1309
  returns: what we need tof create or update API
1308
1310
  """
1309
-
1311
+ cid_print('Preparing dashboard definition')
1310
1312
  create_parameters = {
1311
1313
  'AwsAccountId': self.account_id,
1312
1314
  'DashboardId': definition.get('dashboardId'),
@@ -1338,6 +1340,7 @@ class QuickSight(CidBase):
1338
1340
  create_parameters['Definition'],
1339
1341
  currency_symbol=get_parameters().get('currency-symbol', 'USD')
1340
1342
  )
1343
+ create_parameters['Definition'] = patch_spaces(create_parameters['Definition'])
1341
1344
  dataset_references = []
1342
1345
  for identifier, arn in definition.get('datasets', {}).items():
1343
1346
  # Fetch dataset by name (preferably) OR by id
@@ -323,8 +323,10 @@ class Dashboard(CidQsResource):
323
323
  if self.datasets:
324
324
  cid_print(f" <BOLD>Datasets:<END>")
325
325
  for dataset_name, dataset_id in sorted(self.datasets.items()):
326
+ ds = self.qs.describe_dataset(dataset_id)
327
+ rls = {'ENABLED': '🔐', 'DISABLED': '🔓', None: ' '}.get(ds.rls_status)
326
328
  status = self.qs.get_dataset_last_ingestion(dataset_id) or '<BLUE>DIRECT<END>' #todo fix this Blue using dataset import type.
327
- cid_print(f' {dataset_name: <36} ({dataset_id: <36}) {status}')
329
+ cid_print(f' {rls} {dataset_name: <36} ({dataset_id: <36}) {status}')
328
330
 
329
331
  def display_url(self, url_template: str, launch: bool = False, **kwargs) -> None:
330
332
  url = url_template.format(dashboard_id=self.id, **kwargs)
@@ -429,6 +429,24 @@ def detect_global_filter_fields(dashboard_definition):
429
429
  cols.append(col_name)
430
430
 
431
431
 
432
+ def patch_spaces(definition):
433
+ ''' put spaces in places
434
+ '''
435
+ def _patch(data):
436
+ """Recursively set spaces in text fields"""
437
+ SPACE = '\u00A0' # special space: non-breaking space
438
+ if isinstance(data, dict):
439
+ return {k: _patch(v) if k not in ['Expression'] else v for k, v in data.items()} # skip patching code
440
+ elif isinstance(data, list):
441
+ return [_patch(item) for item in data]
442
+ elif isinstance(data, str):
443
+ text = re.sub(r'([^>\s])(\s*)(\s)(<[^/])', rf'\1{SPACE}\2\3\4', data, re.MULTILINE|re.DOTALL)
444
+ if text != data:
445
+ logger.trace(f'patch_spaces: {repr(text)} {repr(data)}')
446
+ return text
447
+ return data
448
+ return _patch(definition)
449
+
432
450
  def patch_currency(definition, currency_symbol):
433
451
  ''' patch dashboard by adding currency symbol where the currency is configured already.
434
452
  '''
@@ -1,5 +1,5 @@
1
+ import uuid
1
2
  import logging
2
- from uuid import uuid4
3
3
  from copy import deepcopy
4
4
 
5
5
  import yaml
@@ -15,8 +15,14 @@ DATASET_PROPERTIES = [
15
15
  ]
16
16
 
17
17
 
18
- class Dataset(CidQsResource):
18
+ def string_to_uuid(input_string):
19
+ """Generate a UUID from a string using hash.
20
+ """
21
+ return str(uuid.uuid5(uuid.NAMESPACE_DNS, input_string))
19
22
 
23
+ class Dataset(CidQsResource):
24
+ """ Dataset
25
+ """
20
26
  def __init__(self, raw: dict, qs=None, athena=None) -> None:
21
27
  super().__init__(raw)
22
28
  self.qs = qs
@@ -31,6 +37,19 @@ class Dataset(CidQsResource):
31
37
  def id(self) -> str:
32
38
  return self.get_property('DataSetId')
33
39
 
40
+ @property
41
+ def is_rls(self):
42
+ return self.get_property('UseAs') == 'RLS_RULES'
43
+
44
+ @property
45
+ def rls_status(self):
46
+ return (self.get_property('RowLevelPermissionDataSet') or {}).get('Status')
47
+
48
+ @property
49
+ def rls_arn(self):
50
+ return (self.get_property('RowLevelPermissionDataSet') or {}).get('Arn')
51
+
52
+
34
53
  @property
35
54
  def columns(self) -> list:
36
55
  if not 'OutputColumns' in self.raw: self.describe()
@@ -121,11 +140,13 @@ class Dataset(CidQsResource):
121
140
  renames[key] = dt["RenameColumnOperation"]['NewColumnName']
122
141
  logger.trace(f'renames = {renames}')
123
142
 
124
- projected_cols = next( # get the first DataTransform with ProjectOperation
125
- ds['ProjectOperation']["ProjectedColumns"]
126
- for ds in root_lt['DataTransforms']
127
- if 'ProjectOperation' in ds
128
- )
143
+ projected_cols = []
144
+ if root_lt.get('DataTransforms'):
145
+ projected_cols = next( # get the first DataTransform with ProjectOperation
146
+ ds['ProjectOperation']["ProjectedColumns"]
147
+ for ds in root_lt.get('DataTransforms', [])
148
+ if 'ProjectOperation' in ds
149
+ )
129
150
 
130
151
  # Update each PhysicalTableMap with all columns from athena views
131
152
  all_columns = []
@@ -179,7 +200,7 @@ class Dataset(CidQsResource):
179
200
  "Columns": [
180
201
  {
181
202
  "ColumnName": col_name,
182
- "ColumnId": str(uuid4()),
203
+ "ColumnId": string_to_uuid(dataset['Name'] + col_name),
183
204
  "Expression": expression
184
205
  }
185
206
  ]
@@ -276,7 +276,7 @@ def get_parameter(param_name, message, choices=None, default=None, none_as_disab
276
276
  if multi:
277
277
  default = default or []
278
278
  default = default if isinstance(default, list) else [default]
279
- default = [c for c in defaults if c in choices]
279
+ default = [c for c in default if c in choices]
280
280
  if not isatty():
281
281
  result = default
282
282
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cid-cmd
3
- Version: 4.2.6
3
+ Version: 4.3.0
4
4
  Summary: Cloud Intelligence Dashboards deployment helper tool
5
5
  Home-page: https://github.com/aws-samples/aws-cudos-framework-deployment
6
6
  Author: AWS CUDOS Team
@@ -36,6 +36,7 @@ cid/builtin/core/data/datasets/trends/monthly_bill_by_account.json
36
36
  cid/builtin/core/data/permissions/dashboard_permissions.json
37
37
  cid/builtin/core/data/permissions/dashboard_permissions_namespace.json
38
38
  cid/builtin/core/data/permissions/data_set_permissions.json
39
+ cid/builtin/core/data/permissions/data_set_rls_permissions.json
39
40
  cid/builtin/core/data/permissions/data_source_permissions.json
40
41
  cid/builtin/core/data/permissions/folder_permissions.json
41
42
  cid/builtin/core/data/queries/cid/compute_savings_plan_eligible_spend.sql
@@ -5,4 +5,5 @@ cid
5
5
  dashboards
6
6
  dist
7
7
  docs
8
- terraform-modules
8
+ legacy-terraform
9
+ terraform
@@ -1,2 +0,0 @@
1
- __version__ = '4.2.6'
2
-
@@ -1,51 +0,0 @@
1
- from cid.exceptions import CidCritical
2
-
3
- class AthenaStore():
4
- def __init__(self, athena, view_name='cid_parameters'):
5
- self.athena = athena
6
- self.view_name = view_name
7
-
8
- def dump(self, data):
9
- ''' load from athena
10
- '''
11
- # FIXME: make it multi view
12
- self.athena.query(self._generate_view_query(data, self.view_name))
13
-
14
- def load(self):
15
- ''' load from athena
16
- '''
17
- try:
18
- res = self.athena.query(f'''select * from {self.view_name}''', include_header=True)
19
- except CidCritical as exc:
20
- if 'TABLE_NOT_FOUND' in str(exc):
21
- res = []
22
- else:
23
- raise
24
- return [{k:v for k, v in zip(res[0], row)} for row in res[1:]]
25
-
26
- def _to_sql_str(self, val):
27
- if val is None:
28
- return "''"
29
- return "'" + str(val).replace("'", "''") + "'"
30
-
31
- def _generate_view_query(self, data, name):
32
- all_keys = {key for dictionary in data for key in dictionary.keys()}
33
- lines = ',\n '.join([f'''ROW({','.join([self._to_sql_str(line.get(k)) for k in all_keys])})''' for line in data])
34
- query = f"""
35
- CREATE OR REPLACE VIEW {name} AS
36
- SELECT *
37
- FROM (
38
- VALUES
39
- {lines}
40
- ) ignored_table_name ({','.join([key for key in all_keys])})
41
- """
42
- return query
43
-
44
- class ParametersController(AthenaStore):
45
- def load_parameters(self, context):
46
- data = self.load()
47
- return { line.get('parameter'):line.get('value') for line in data}
48
-
49
- def dump_parameters(self, params, context=None):
50
- data = [{'parameter': key, 'value': ','.join(val) if isinstance(val, list) else val, 'context': str(context) } for key, val in params.items()]
51
- self.dump(data)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes