From a433fcb57a861ef698fd13b8cc29945bbc25049a Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Wed, 13 Oct 2021 17:03:01 +0100 Subject: [PATCH] v1.1.1 commit --- .github/stale.yml | 18 - .gitignore | 14 +- CHANGELOG.md | 22 +- README.md | 20 +- deployment/build-s3-dist.sh | 11 +- deployment/perspective-setup.yaml | 67 +- docs/architecture-diagrams/arch-diagram.png | Bin 468009 -> 455769 bytes package-lock.json | 1107 ++++++ package.json | 6 + source/backend/discovery/package-lock.json | 2964 ++++++++++------- source/backend/discovery/package.json | 8 +- .../discovery/src/discovery/dataClient.js | 70 +- .../discovery/src/discovery/rdsCluster.js | 1 + .../backend/functions/api/package-lock.json | 903 ++--- source/backend/functions/api/package.json | 4 +- .../api/src/global-resources.template | 2 +- .../api/src/regional-resources.template | 2 +- .../functions/cost-parser/package-lock.json | 295 +- .../functions/cost-parser/package.json | 2 +- .../functions/graph-api/package-lock.json | 461 ++- .../backend/functions/graph-api/package.json | 4 +- .../lambda-layers/aws_sdk/requirements.txt | 1 - .../boto_utils/{python => }/boto_utils.py | 0 .../lambda-layers/boto_utils/requirements.txt | 1 - .../decorators/{python => }/decorators.py | 0 .../functions/search/package-lock.json | 883 ++--- source/backend/functions/search/package.json | 2 +- .../functions/secured-edge/cff-hsts.js | 13 + .../create_regional_edge_lambda.py | 73 - .../functions/settings/package-lock.json | 885 ++--- .../backend/functions/settings/package.json | 2 +- source/backend/functions/setup/cfn-handler.js | 19 +- source/backend/functions/setup/config.js | 10 +- .../cfn/templates/perspective-cloudfront.yaml | 132 +- source/cfn/templates/perspective-layers.yaml | 14 - ...yaml => perspective-opensearch-roles.yaml} | 10 +- ...earch.yaml => perspective-opensearch.yaml} | 29 +- source/cfn/templates/perspective-search.yaml | 33 +- source/cfn/templates/zoom-api-gateway.yaml | 8 +- .../cfn/templates/zoom-discovery-crawler.yaml | 9 +- .../templates/zoom-import-and-aggregator.yaml | 2 +- source/cfn/templates/zoom-main.yaml | 99 +- .../templates/zoom-server-api-gateway.yaml | 6 +- source/frontend/.nsprc | 5 +- source/frontend/package-lock.json | 470 +-- source/frontend/package.json | 8 +- ...n-Aurora_instance_mysql_light-bg-error.svg | 27 + ...on-Aurora_instance_mysql_light-bg-menu.svg | 14 + ...Aurora_instance_mysql_light-bg-warning.svg | 26 + ..._Amazon-Aurora_instance_mysql_light-bg.svg | 25 + ...ora_instance_postgresql_light-bg-error.svg | 34 + ...rora_instance_postgresql_light-bg-menu.svg | 19 + ...a_instance_postgresql_light-bg-warning.svg | 33 + ...on-Aurora_instance_postgresql_light-bg.svg | 32 + ...zon-RDS_Amazon-RDS_instance_light-bg 2.svg | 1 - ...on-RDS_MariaDB_instance_light-bg-error.svg | 32 + ...zon-RDS_MariaDB_instance_light-bg-menu.svg | 23 + ...-RDS_MariaDB_instance_light-bg-warning.svg | 25 + .../Amazon-RDS_MariaDB_instance_light-bg.svg | 31 +- ...RDS_SQL-Server_instance_light-bg-error.svg | 32 + ...-RDS_SQL-Server_instance_light-bg-menu.svg | 23 + ...S_SQL-Server_instance_light-bg-warning.svg | 25 + ...mazon-RDS_SQL-Server_instance_light-bg.svg | 42 +- .../src/API/NodeFactory/NodeParserHandler.js | 2 + .../CloudFrontDistributionItem.js | 13 +- .../DatabaseInstanceItem.js | 12 +- .../InstanceDetails/InstanceItem.js | 11 +- .../LoadBalancerDetails/LoadBalancerHover.js | 11 +- .../LoadBalancerDetails/LoadBalancerItem.js | 11 +- .../src/API/Processors/EdgeProcessors.js | 2 + source/frontend/src/Utils/ImageSelector.js | 31 + .../Utils/Resources/ResourceStateParser.js | 2 +- .../Drawer/Costs/Report/CostOverview.js | 62 +- .../Drawer/Costs/TreeMenuCostsMenu.js | 1 + .../src/components/Graph/Cytoscape.js | 16 +- .../Graph/DetailsDialog/DetailsDialog.js | 1 - .../DetailsDialog/ResourceDetailsPanel.js | 15 +- .../ResourceSelector/data/resources.json | 4 +- 78 files changed, 5301 insertions(+), 4032 deletions(-) delete mode 100644 .github/stale.yml create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 source/backend/functions/lambda-layers/aws_sdk/requirements.txt rename source/backend/functions/lambda-layers/boto_utils/{python => }/boto_utils.py (100%) delete mode 100644 source/backend/functions/lambda-layers/boto_utils/requirements.txt rename source/backend/functions/lambda-layers/decorators/{python => }/decorators.py (100%) create mode 100644 source/backend/functions/secured-edge/cff-hsts.js delete mode 100644 source/backend/functions/secured-edge/create_regional_edge_lambda.py rename source/cfn/templates/{perspective-elasticsearch-roles.yaml => perspective-opensearch-roles.yaml} (84%) rename source/cfn/templates/{zoom-elasticsearch.yaml => perspective-opensearch.yaml} (92%) create mode 100644 source/frontend/public/icons/Amazon-RDS_Amazon-Aurora_instance_mysql_light-bg-error.svg create mode 100644 source/frontend/public/icons/Amazon-RDS_Amazon-Aurora_instance_mysql_light-bg-menu.svg create mode 100644 source/frontend/public/icons/Amazon-RDS_Amazon-Aurora_instance_mysql_light-bg-warning.svg create mode 100644 source/frontend/public/icons/Amazon-RDS_Amazon-Aurora_instance_mysql_light-bg.svg create mode 100644 source/frontend/public/icons/Amazon-RDS_Amazon-Aurora_instance_postgresql_light-bg-error.svg create mode 100644 source/frontend/public/icons/Amazon-RDS_Amazon-Aurora_instance_postgresql_light-bg-menu.svg create mode 100644 source/frontend/public/icons/Amazon-RDS_Amazon-Aurora_instance_postgresql_light-bg-warning.svg create mode 100644 source/frontend/public/icons/Amazon-RDS_Amazon-Aurora_instance_postgresql_light-bg.svg delete mode 100644 source/frontend/public/icons/Amazon-RDS_Amazon-RDS_instance_light-bg 2.svg create mode 100644 source/frontend/public/icons/Amazon-RDS_MariaDB_instance_light-bg-error.svg create mode 100644 source/frontend/public/icons/Amazon-RDS_MariaDB_instance_light-bg-menu.svg create mode 100644 source/frontend/public/icons/Amazon-RDS_MariaDB_instance_light-bg-warning.svg create mode 100644 source/frontend/public/icons/Amazon-RDS_SQL-Server_instance_light-bg-error.svg create mode 100644 source/frontend/public/icons/Amazon-RDS_SQL-Server_instance_light-bg-menu.svg create mode 100644 source/frontend/public/icons/Amazon-RDS_SQL-Server_instance_light-bg-warning.svg diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 22ca1d5a..00000000 --- a/.github/stale.yml +++ /dev/null @@ -1,18 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -# Setting this to 100 years, because we do not want to automatically mark issues as stale -daysUntilStale: 30 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Label to use when marking an issue as stale -staleLabel: closing-soon-if-no-response -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: > - This issue has been automatically closed because of inactivity. - Please open a new issue if are still encountering problems. -# Limit to only `issues` or `pulls` -only: issues diff --git a/.gitignore b/.gitignore index f6f41526..f841cf1c 100644 --- a/.gitignore +++ b/.gitignore @@ -37,10 +37,16 @@ # build source/backend/functions/lambda-layers/aws_sdk/python source/backend/functions/lambda-layers/cr_helper/python -!source/backend/functions/lambda-layers/decorators/python/decorators.py -!source/backend/functions/lambda-layers/boto_utils/python/boto_utils.py -# source/backend/functions/lambda-layers/decorators/python/* -# source/backend/functions/lambda-layers/boto_utils/python/* +source/backend/functions/lambda-layers/decorators/python/* +source/backend/functions/lambda-layers/boto_utils/python/* source/backend/functions/cleanup-bucket/package +source/backend/functions/cost-parser/src/setting-up-athena-integration.md local-deploy-perspective.sh + +# codebuild local testing +codebuild_build.sh +source/backend/functions/cost-parser/test/local-deploy.sh +source/backend/functions/cost-parser/test/local-invoke-read-s3.sh +source/backend/functions/cost-parser/test/local-invoke-service-cost.sh +source/backend/functions/cost-parser/test/local-invoke.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 715aec11..c1349ae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,27 @@ All notable changes to this project are documented in this file. Based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.1] - 2021-09-28 + +### Added + +- Missing icons for MariaDB, Aurora, SQL-Server RDS types. +- OpensearchMultiAz parameter to CloudFormation template to set Amazon OpenSearch Service up with a single instance. + +### Changed + +- Migrated from Lambda@Edge to CloudFront Functions to handle secure headers for web requests to the frontend. +- References to Amazon Elasticsearch Service to Amazon OpenSearch Service + +### Fixed + +- Fixed a bug causing a blank screen when expanding nodes whilst filters are enabled - https://github.com/awslabs/aws-perspective/issues/201 +- Fixed a bug that meant the time period for cost report queries was not persisted - https://github.com/awslabs/aws-perspective/issues/200 +- Fixed a bug that could result in python files being incorrectly excluded - https://github.com/awslabs/aws-perspective/issues/64 +- A bug causing some resource types to throw an exception when clicking "Show more details" + ## [1.1.0] - 2021-08-26 + ### Added - Support for newer ECS task ARNs @@ -49,4 +69,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0] - 2020-09-21 -- Initial release \ No newline at end of file +- Initial release diff --git a/README.md b/README.md index eb5ac319..4d6500d6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# AWS Perspective (v1.1.0) +# AWS Perspective (v1.1.1) AWS Perspective is a tool that quickly visualizes AWS Cloud workloads as architecture diagrams. You can use the solution to build, customize, and share detailed workload visualizations based on live data from AWS. This solution works by maintaining an inventory of the AWS resources across your accounts and Regions, mapping relationships between them, and displaying them in a web user interface (web UI). -v1.1.0 brings a new feature that uses AWS Cost & Usage Reports (AWS CUR) to help you identify AWS resources that have incurred a cost. You can build architecture diagrams displaying this cost information and generate Cost Reports which graph the overall cost of your workload over a configurable time period. These reports can be exported in CSV format. +v1.1.1 brings a new feature that uses AWS Cost & Usage Reports (AWS CUR) to help you identify AWS resources that have incurred a cost. You can build architecture diagrams displaying this cost information and generate Cost Reports which graph the overall cost of your workload over a configurable time period. These reports can be exported in CSV format. The new release includes many UX improvements among them a Grouped Resources ** view which displays an inventory of your workloads. Resource type coverage has also been improved with Perspective now supporting your Amazon Redshift Clusters. @@ -217,9 +217,10 @@ Parameters required by the template: * **OptOutOfSendingAnonymousUsageMetrics** - Yes/No depending on whether you are happy to send anonymous usage metrics back to AWS. * **CreateNeptuneReplica** - Yes/No depending on whether you want a read-replica created for Amazon Neptune. Note, that this will increase the cost of running the solution. * **NeptuneInstanceClass** - Select from a range of instance types that will be provisioned for the Amazon Neptune database. Note, the selection could increase the cost associated with running the solution. -* **ElasticsearchInstanceType** - Select the instance type that will be provisioned for the Amazon ElasticSearch Domain. +* **OpensearchInstanceType** - Select the instance type that will be provisioned for the Amazon ElasticSearch Domain. * **CreateAPIGatewayCloudWatchLogsRole** - If set to Yes, the solution creates a role and overwrites the existing APIGatewayCloudWatchLogsLogsRole property. Set to No if you already have an existing role set. * **AthenaWorkgroup** - The Workgroup that will be used to issue the Athena query when the Cost feature is enabled. +* **OpensearchMultiAz** - Choose whether to create an Opensearch cluster that spans multiple Availability Zone. Choosing Yes improves resilience; however, increases the cost of this solution. **Note** - You will need to deploy in the same account and region as the S3 bucket that the deployment artefacts are uploaded to. @@ -301,6 +302,7 @@ curl -X POST "https://${DRAWIO_API_URL}.execute-api.${AWS_REGION}.amazonaws.com/ --data-raw '{"elements":{"nodes":[], "edges": []}}' ``` + ##### Response You will receive a URL that when clicked will open up DrawIO in the browser and show your graph. @@ -309,7 +311,17 @@ You will receive a URL that when clicked will open up DrawIO in the browser and ## Collecting Anonymous Operational Metrics -This solution collects anonymous operational metrics to help AWS improve the quality of features of the solution. For more information, including how to disable this capability, please see the [Implementation Guide](https://docs.aws.amazon.com/solutions/latest/aws-perspective/appendix-g-collection-of-operational-metrics.html). +This solution collects anonymous operational metrics to help AWS improve the quality of features of the solution. For more information, including how to disable this capability, please see the [Implementation Guide](https://docs.aws.amazon.com/solutions/latest/aws-perspective/collection-of-operational-metrics.html). + +## Acknowledgements + +AWS Perspective is able to generate its architecture diagrams thanks to these libraries developed and maintained by the [Info Visualization Research Lab](https://www.cs.bilkent.edu.tr/~ivis/) over at Bilkent University: + +* [cytoscape.js-fcose](https://github.com/iVis-at-Bilkent/cytoscape.js-fcose) +* [cytoscape.js-grid-guide](https://github.com/iVis-at-Bilkent/cytoscape.js-grid-guide) +* [cytoscape.js-context-menus](https://github.com/iVis-at-Bilkent/cytoscape.js-context-menus) +* [cytoscape.js-expand-collapse](https://github.com/iVis-at-Bilkent/cytoscape.js-expand-collapse) + Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/deployment/build-s3-dist.sh b/deployment/build-s3-dist.sh index f9d00737..44dfd46d 100755 --- a/deployment/build-s3-dist.sh +++ b/deployment/build-s3-dist.sh @@ -124,19 +124,22 @@ echo "[Rebuild] Layers" echo "------------------------------------------------------------------------------" cd $source_dir/backend/functions/lambda-layers for i in `ls -d */ | sed 's#/##'` ; do - pip install -r $i/requirements.txt -t $i/python/ + mkdir $i/python + [ -f "$i/$i.py" ] && cp $i/$i.py $i/python + [ -f "$i/requirements.txt" ] && pip install -r $i/requirements.txt -t $i/python/ cd $i zip -q -r9 ../$i.zip ./python cd .. + rm -rf $i/python done cp ./*.zip $build_dist_dir/ echo "------------------------------------------------------------------------------" -echo "[Rebuild] Secured Edge Lambda" +echo "[Rebuild] HSTS CloudFront Function" echo "------------------------------------------------------------------------------" cd $source_dir/backend/functions/secured-edge -mkdir dist && zip -q -r9 dist/create_regional_edge_lambda.zip create_regional_edge_lambda.py -cp ./dist/create_regional_edge_lambda.zip $build_dist_dir/create_regional_edge_lambda.zip +rm -rf dist && mkdir dist && cp cff-hsts.js dist/cff-hsts.js +cp ./dist/cff-hsts.js $build_dist_dir/cff-hsts.js echo "------------------------------------------------------------------------------" echo "[Rebuild] Cleanup Bucket Lambda" diff --git a/deployment/perspective-setup.yaml b/deployment/perspective-setup.yaml index 5109391e..249efd65 100644 --- a/deployment/perspective-setup.yaml +++ b/deployment/perspective-setup.yaml @@ -28,20 +28,20 @@ Parameters: AlreadyHaveConfigSetup: Type: String Default: 'No' - Description: 'Is AWS Config set-up within this Account or Region?' + Description: 'Is AWS Config set-up within this Region?' AllowedValues: - 'No' - 'Yes' - ConstraintDescription: 'Please specify if this account has config set-up (Yes / No)' - CreateElasticsearchServiceRole: + ConstraintDescription: 'Please specify if this Region has AWS Config set-up (Yes / No)' + CreateOpensearchServiceRole: Type: String Default: 'Yes' - Description: 'Do you need an ElasticSearch Service Role to be created? + Description: 'Do you need an OpenSearch Service Role to be created? You can check for a Role called AWSServiceRoleForAmazonElasticsearchService in your account. If it exists then you do NOT need one creating' AllowedValues: - 'No' - 'Yes' - ConstraintDescription: 'Please specify if this account has config set-up (Yes / No)' + ConstraintDescription: 'Please specify if this account has AWS Config set-up (Yes / No)' AdminUserEmailAddress: Type: String AllowedPattern: "^[\\w!#$%&’*+/=?`{|}~^-]+(?:\\.[\\w!#$%&’*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$" @@ -71,8 +71,8 @@ Parameters: - 'Yes' Default: 'No' Description: If you would like a read replica creating in a separate AZ. Please select 'Yes'. This will increase the cost of running the solution. - ElasticsearchInstanceType: - Description: The instance type for Elasticsearch data nodes + OpensearchInstanceType: + Description: The instance type for OpenSearch data nodes Type: String Default: m6g.large.elasticsearch AllowedValues: @@ -136,7 +136,17 @@ Parameters: - i3.4xlarge.elasticsearch - i3.8xlarge.elasticsearch - i3.16xlarge.elasticsearch - + + + OpensearchMultiAz: + Description: Deploys the OpenSearch cluster across two Availability Zones (AZs) in the same region to prevent + data loss and minimize downtime in the event of node or data center failure. This will increase the cost of running the solution + Type: String + Default: "No" + AllowedValues: + - 'Yes' + - 'No' + CreateAPIGatewayCloudWatchLogsRole: Type: String Default: "Yes" @@ -390,6 +400,18 @@ Resources: - cloudfront:TagResource - cloudfront:GetDistribution - cloudfront:CreateInvalidation + - cloudfront:CreateFunction + - cloudfront:DeleteFunction + - cloudfront:DescribeFunction + - cloudfront:GetFunction + - cloudfront:ListFunctions + - cloudfront:UpdateFunction + - cloudfront:TestFunction + - cloudfront:PublishFunction + - cloudfront:GetDistribution + - cloudfront:GetDistributionConfig + - cloudfront:ListTagsForResource + - cloudfront:UpdateDistribution Resource: '*' - Effect: Allow Action: @@ -509,7 +531,8 @@ Resources: ANONYMOUS_METRIC_OPT_OUT: !Ref OptOutOfSendingAnonymousUsageMetrics NEPTUNE_INSTANCE_CLASS: !Ref NeptuneInstanceClass CREATE_READ_REPLICA: !Ref CreateNeptuneReplica - ELASTICSEARCH_INSTANCE_TYPE: !Ref ElasticsearchInstanceType + OPENSEARCH_INSTANCE_TYPE: !Ref OpensearchInstanceType + OPENSEARCH_MULTI_AZ: !Ref OpensearchMultiAz ACCOUNT_ID: !Ref AWS::AccountId API_GATEWAY: !Sub https://${PerspectiveWebRestAPI}.execute-api.${AWS::Region}.amazonaws.com/Prod SERVER_API_GATEWAY: !Sub https://${ServerGremlinAPI}.execute-api.${AWS::Region}.amazonaws.com/Prod/ @@ -530,7 +553,7 @@ Resources: DISCOVERY_ARN: !GetAtt PerspectiveDiscoveryRole.Arn CONFIG_AGGREGATOR: !Sub aws-perspective-${AWS::Region}-${AWS::AccountId}-aggregator EXISTING_CONFIG: !Ref AlreadyHaveConfigSetup - CREATE_ES_SERVICE_ROLE: !Ref CreateElasticsearchServiceRole + CREATE_OPENSEARCH_SERVICE_ROLE: !Ref CreateOpensearchServiceRole ATHENA_WORKGROUP: !Ref AthenaWorkgroup APPSYNC_API_ARN: !GetAtt PerspectiveAppSyncApi.Arn APPSYNC_API_ID: !GetAtt PerspectiveAppSyncApi.ApiId @@ -1113,7 +1136,9 @@ Resources: - Sid: HttpsOnly Action: '*' Effect: Deny - Resource: !Sub arn:aws:s3:::${WebUIBucket}/* + Resource: + - !Sub arn:aws:s3:::${WebUIBucket}/* + - !Sub arn:aws:s3:::${WebUIBucket} Principal: '*' Condition: Bool: @@ -1154,7 +1179,9 @@ Resources: - Sid: HttpsOnly Action: '*' Effect: Deny - Resource: !Sub arn:aws:s3:::${AmplifyStorageBucket}/* + Resource: + - !Sub arn:aws:s3:::${AmplifyStorageBucket}/* + - !Sub arn:aws:s3:::${AmplifyStorageBucket} Principal: '*' Condition: Bool: @@ -1197,7 +1224,9 @@ Resources: - Sid: HttpsOnly Action: '*' Effect: Deny - Resource: !Sub arn:aws:s3:::${AccessLogsBucket}/* + Resource: + - !Sub arn:aws:s3:::${AccessLogsBucket}/* + - !Sub arn:aws:s3:::${AccessLogsBucket} Principal: '*' Condition: Bool: @@ -1238,7 +1267,9 @@ Resources: - Sid: HttpsOnly Action: '*' Effect: Deny - Resource: !Sub arn:aws:s3:::${CostAndUsageAthenaResultsBucket}/* + Resource: + - !Sub arn:aws:s3:::${CostAndUsageAthenaResultsBucket}/* + - !Sub arn:aws:s3:::${CostAndUsageAthenaResultsBucket} Principal: '*' Condition: Bool: @@ -1273,7 +1304,9 @@ Resources: - Sid: HttpsOnly Action: '*' Effect: Deny - Resource: !Sub arn:aws:s3:::${CostAndUsageReportBucket}/* + Resource: + - !Sub arn:aws:s3:::${CostAndUsageReportBucket}/* + - !Sub arn:aws:s3:::${CostAndUsageReportBucket} Principal: '*' Condition: Bool: @@ -1308,7 +1341,9 @@ Resources: - Sid: HttpsOnly Action: '*' Effect: Deny - Resource: !Sub arn:aws:s3:::${DiscoveryBucket}/* + Resource: + - !Sub arn:aws:s3:::${DiscoveryBucket}/* + - !Sub arn:aws:s3:::${DiscoveryBucket} Principal: '*' Condition: Bool: diff --git a/docs/architecture-diagrams/arch-diagram.png b/docs/architecture-diagrams/arch-diagram.png index 68c48c9a7a37354b601f66a642bfb930ae5e642b..a37b7b7f5c44622364a7c80c698dfe9d2768306b 100644 GIT binary patch literal 455769 zcma%j2Urv7_CBD9$|AZ7CQTiSs4c<*uQtu`!oCXZ;}8 zer{1N-fu%VZ*6X|Ki2iRWx4kKYda4YSF8uu{{M_I;Vi$s(m3yL$NYP_FOT~_qdD91 zczz$vhsxXc`}%&fZ-<#bxH!&P9C~!q)}M<@- zgJ<<0w5Q#V`?=&@lmM^X5DG)2!&>@#Rw&|-cucfLu+HvEkLE_t#r~!s&;)T+`(mWj zPc=aPA4X>4#}R@r3$Af-??0;hKR=u?M^Ba%WMvYSj-*Bta(EBG!PMPkkjCcp{10U+ z6@gw;FrQ2fr6UKwdls(9C;JcjslNSy242l;@>j*VEpX$1dv`+ew@-oPxt2)~6W>!C z2hT)s@%--(?*bmF00-?wgCvL;^od#p&?(OnC&3?(B7|qX^n3ij5B%3@8)Ggswx7NV zIrGzZFY@mx3>@Rk7W~-lnfA~BJUW;=ijZ~iA1?pxyyE&1XTY%`Ws9=kxy#>Wpa<&v zzC{1EH}Z&-ZW88$rd7a+e;yriMs?wP0{+jXbz_g6=_KCEkyn5I&!csdGX4Lj?fiE~ z-iL!n=cvD!3W?wN@5cYNcBZpe?VrVdX5ij8CA}_wp78&nq@#25r~kK+|1p!rUw9^t z3|p!{*A+WFw;qCP^^pYcyx~K0LssU47AHRN>V0+$0ltLH<(f9SbldEZOtP`W2rV#} zHlf3O>2AZBK7mWZU5uS zEd!K`?s*Cdi8QbFuUM?Ss!uyVmG8KczmIabDlkg!AB-H|%%w-AuugF0*;MCHf1A}= zxyp-}4)%Yyjt7lqTm~f)gM1lOtxf^anpjNux-y~EEsG-DOi!~%f||*t&O7al)dhpF zYP;r7Qb)%9Rduj3R$Y{@pdOmLCt(*=4m)4n2AD7u{6ZiYf zN61&58hX0cL3~x%9fpsUsBiVbl9iWGQWM2Gw-+^3$5Ln0aP-An>;ge%=l@-ud0LBf^aDCq74kQ*RFQ^0Sv zb=9%TXrT|(z%!?X27~$Jf|)fuIAKkkRTr@|_vA_p`=U>zJ)I>FUWrWzh&{c@4U^kc z&ep;Rc4i)TJE+#`tctmeQ@O9Z@7n5wysAaDJE_DuG5S*DQc8ckbd_MRw_c^OIv2D&UHy405=sjKV1j(X#EUpSY@+7M z8F=?c+8h;EjnsI=hB~B9!dzJ!EXOI0bNESS8*o38GR*TOjWV=QtrYg5(#eOL7L>eg zV|v8MuPD+nP@xQme71GOBQK|N(r98E2rW?-7%KK)YRfZ~?7rEI?(#~tH*7vcm{k-kCkG4r$XOX8RA1Sxi4v70Mwu`ol z^PvPa7P)YxO$0@!wH8T52@iMLans)6yS@uSD>xkSSF#z<;t7n-r>E7UHP&-)>~@str0aFm+llN%N79L4clsiU`;m6 z!HNl1A^yY9CcpY%`BQ;xcJZD+New&3mPyg6bWpXeVV`m-f6y_RGkkeDYinb7n_#&6 zt?QNGcl|`<5rqK^?g(prrb7}^Uw3TsWx^a3U_sEoyV^qTV>SL9--vh^Nwo(t z>clpBtI-8Vv^ouV8SV@%+W2VYUDPe9_acq#md!Gy8!fGPSSvKZiHal6#7Zn0JZY_a zSPEf%vEa`rm8}iAGa4+`sXO`x|0p5XJTr(>7k*PMbLa?aX7Bhwe@(H?? z3x2E?DX916CRr@`3RPKC--J1o9yXjP4IG|vLqlkUok~LyOQpC=v@Gm zmQwKgnP3Y4q95#uIYCS zf4qI19Wut(56p~DO{HSXeFB8w>`z*oiw~;M=t=Rwou8=qf);A{MqM0{*19Et^NP)} zZqRz`|H|Z!%&@6D8muK=Zb6UkQs?Kg&qIOaZQ47QPmBqK7MJT}^ts!ep3~Or_K0A# zW+2Z5iIAqSh<>Tm)jNoQMn8W8IhYOJA>X??S-75ES{v`aTHaJ%OL1$ek2eeZ_jX8W+AMD$j9+s^!{~#BfyZ7FP=;3sF#zc*I z9?ZVR{u+Cq)`c+2un#3uzyy9a^yAj*G>(GlKZ9g|Fgp>O1?z4gJ;MVle=x%AaH1fS4(E#Hw z6E`d`H=gw=v;6WcMsA`GDb7@DsUZA1*b%Qq-wc!d&2e+rp2fn)y5HGhi5_dgD`}u; zX^LQWI?bW-4ASRJ$lZp}hRk0VOsNIM;S)|(gy*K3)RXZC`$cTmDq#$g?XA@W+k1ZX+1l?MNXCba2H!OK#9`N4~~#irVp zyt)L0E8z}1$q-!?ZVN2*JZu$iM7$hQm9wvD$gKtBFkDG!kCwwz<_Lzhqz$+*ZCLj! zF#~pI0;COMs2hh=`DJ(?+-vE&x>ks-T(l(>q|nR|+pHo7`H7p7AXLbt$ZATA5jX&#pK34)az8#LUIXCcM%;J3GQZnY8L*x6GOj z>e@_BrNkcGJ}p!{WB0n~bne&7UZ7ELc*c$S??IHvZN;OARqCL|{z>aI1za0()#o=g z=$#z+zyVSvN1tl%c3va?7N;D5@jQVjY!39HPN81}XE*|Oo&cbt!fj!R3PeR{uy;v(~Ewwo-cE;D~;py}C|LO4x1Xeg>{@ep3tZUKwd{jq81b%Bg2ys+I*6N%hamIy9d zs0%|cB3|36a!;CMp>y5rA8ZoFV}ChxjCEykaQj?R*FypjJVl8fC0@2*M1SILZ7|ENDn-0S_vp(U&&jy`5K&6W~`I3bQh!gJn zw>jWpV?($xhrJgETEiIa`RyG=Ich=B%Hga_s*v4ODv7PRIM1B*0Zr+w2HD&eT~&U` zqVY)t!C;?{O@|LYu@BwOdKlY%x%pzcQ{j?iatMDy5jXj5$uI9-PN(h4+kd`doO|2# zd~nu*_rqL>pIHq`AF~tm9gEhBUr~m-Bro~9)H)Yb8^})=cte?(_5CD4IT60`hH>}F zh7x_z03~_PwF=$=;H9Q!R!jAs2kAj-tb*j*J#iQQFq#=lhm=+%KGFunW~WYDBcKyc z>HYGOMFioF5?90TrNFxQgAegVwstr2W)C)PV!c3n(yam4TE5D~k4b#qHxf4$+})eW zZpsWHXM-r{K=k>L!n#n?y@iSQk+xiWV>-;yN{0q)rEOiG)^4@To^)qr6fa5=vV5wt zwcFq>5D;k98{upZo9)qQ{1#`wWhfe}O$FphR9$(W6$U&7SeAfXm>Q$bHVY*K&h0&Z z$i8??>I!>7`ge;w~a zP_$(~H+XA5;ghO(#YfMlZZ~gz7Mq!=oi9MUsgjdd8h_Mu*em1R!fJbc#CLP1fhk^2 zFs8XZWi_B8h)#?dnD+d%`r(I>%MdDToC7sl z1D|a)l~(^g`lM(2^{zUY600$8iBy&aDJYj} zRgOcG6s(1L<{Lzbwfsx6AUrufFbLY3CL$+jhH$0{rs8BpWcZcXHuQa1l`jlY(u-Q& zTty*1grXUc8>fdA9&d3(<@%`yuq1kMaF&fikvu*~_ypoiZTwS{WZq!lG=kcVRU